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:
| Concept | What it is |
|---|---|
appModules() | Creates all foundation and domain module instances |
appContainer() | Selects the handlers from those modules that transports need |
request.container | The 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.
// 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.
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:
// 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:
// 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:
// 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:
// app/app.ts
await addGraphql(app, {
typeDefs: [...],
resolvers: { ... },
context: createGraphqlContext({
container: appContainer(appModules()),
}),
});Resolvers access handlers via the ctx argument:
// 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:
// 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