Skip to content

App Container

The app container is the bridge between the module layer and the transport layer. It collects handler instances from domain modules and makes them available to REST routers and GraphQL resolvers on a per-request basis.

Overview

There are three concepts:

ConceptWhat it is
appModules()Creates all foundation and domain module instances
appContainer()Selects the handlers from those modules that transports need
request.containerThe per-request container attached by the onRequest hook

appModules

appModules is a plain function that instantiates every module in dependency order. Foundations first, then domains.

ts
// app/container.ts

export function appModules() {
	// Foundations, no dependencies
	const database = databaseModule();
	const email = emailModule();

	// Domains, receive their foundation and domain dependencies
	const library = libraryModule({ database, email });
	const events = eventsModule({ database });

	return { database, email, library, events };
}

appModules is called in three distinct contexts: the request hook, the GraphQL context factory, and startup. Each call produces a fresh set of module instances.

appContainer

appContainer receives the output of appModules and returns a flat object of handlers. This is the shape that request handlers and resolvers see.

ts
export function appContainer(modules: ReturnType<typeof appModules>) {
	return {
		library: modules.library.handlers,
		events: modules.events.handlers,
	};
}

export type AppContainer = ReturnType<typeof appContainer>;

Only handlers are exposed, not providers. Providers are implementation details of the module. REST routers and GraphQL resolvers should only know about the CQRS interface.

Request container

In app.ts, an onRequest Fastify hook creates a fresh set of modules and attaches the container to request.container on every incoming request:

ts
// app/app.ts

app.fastify.addHook("onRequest", (request, _, done) => {
	const requestModules = appModules();
	const requestContainer = appContainer(requestModules);
	request.container = requestContainer;
	done();
});

This means each request gets isolated module instances. No cross-request state leaks between handlers.

Type augmentation

To make request.container type-safe across all REST handlers, declare it via module augmentation in container.ts:

ts
// app/container.ts

declare module "@repo/core-server" {
	interface FastifyRequest {
		container: ReturnType<typeof appContainer>;
	}
}

Using the container in REST handlers

REST routers are plain FastifyRouter functions that read from request.container:

ts
// domains/library/shell/transport/rest/reservations/router.ts

export const reservationRouter: FastifyRouter = (fastify) => {
	fastify.post("/reserve", {}, async (request, reply) => {
		const result = await request.container.library.reserveBook.execute({
			bookId: request.body.bookId,
			memberId: request.user.id,
		});
		reply.send(result);
	});
};

Using the container in GraphQL resolvers

GraphQL resolvers receive the container through the GraphQL context. The context is set up separately using createGraphqlContext:

ts
// app/app.ts

await addGraphql(app, {
  typeDefs: [...],
  resolvers: { ... },
  context: createGraphqlContext({
    container: appContainer(appModules()),
  }),
});

Resolvers access handlers via the ctx argument:

ts
// domains/library/shell/transport/graphql/fields/search-books.ts

export const searchBooks: QueryResolvers["searchBooks"] = async (_, args, ctx) => {
	return ctx.container.library.searchBooks.execute({ query: args.query });
};

Startup

Modules that implement ModuleStartup expose a start() method. These are called explicitly in app.ts after building a dedicated set of modules for startup:

ts
// app/app.ts

const startupModules = appModules();

await startupModules.database.start();
await startupModules.email.start();

await app.start();

INFO

Startup modules are a separate appModules() call from the request hook. The connections established here (database pool, SDK clients) are typically managed inside the foundation module itself and shared via closures, not via the container.

Full wiring at a glance

appModules()
  ├── databaseModule()         → providers: { pool }
  ├── emailModule()            → providers: { client }
  ├── libraryModule(imports)   → providers: { books, members }
  │                              handlers: { searchBooks, reserveBook, getMembers }
  └── eventsModule(imports)    → providers: { queue }
                                 handlers: { publishEvent }

appContainer(modules)
  ├── library → libraryModule.handlers
  └── events  → eventsModule.handlers

request.container  (attached per request via onRequest hook)
  ├── library.searchBooks     ← SearchBooksQuery
  ├── library.reserveBook     ← ReserveBookCommand
  ├── library.getMembers      ← GetMembersQuery
  └── events.publishEvent     ← PublishEventCommand