Skip to main content

Tutorial: Request scope with Fastify

This tutorial shows how to wire DiCaf's request scope into a Fastify application. By the end you will have a container that creates a fresh RequestContext per HTTP request and makes it available to any service that needs it.

Prerequisites: Node.js ≥ 20, TypeScript 5+, a Fastify project.


1. Install

npm install @caffeine-projects/dicaf fastify

2. TypeScript config

DiCaf uses TC39 stage 3 decorators — no reflect-metadata needed.

{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"experimentalDecorators": false,
"strict": true
}
}

3. Create a request-scoped service

Scopes.REQUEST tells DiCaf to create a new instance for each request and discard it when the request ends.

// src/request.context.ts
import { Injectable, Lifetime, PostConstruct } from '@caffeine-projects/dicaf/decorators'
import { Scopes } from '@caffeine-projects/dicaf'

@Injectable()
@Lifetime(Scopes.REQUEST)
export class RequestContext {
readonly correlationId = crypto.randomUUID()

@PostConstruct()
onCreated(): void {
console.log(`[${this.correlationId}] request started`)
}
}

4. Wire the container into Fastify

The onRequest hook calls requestScopeManager.run(). Everything inside that callback runs within a single request's async context — DiCaf uses it to isolate request-scoped instances between concurrent requests.

// src/app.ts
import fastify from 'fastify'
import type { Container } from '@caffeine-projects/dicaf'

export async function buildServer(container: Container) {
const server = fastify({ logger: true })

server
.addHook('onRequest', (_req, _reply, done) => {
container.requestScopeManager.run(() => done())
})
.addHook('onClose', async () => {
await container.dispose()
})

return server
}

onClose disposes the container when Fastify shuts down, running any @PreDestroy hooks on singleton services.


5. Inject RequestContext into a singleton

Singleton services cannot directly receive a request-scoped dependency — their instance is created once, before any request arrives. Use provide() to get a Provider<T> instead: a thin wrapper that resolves the current request's instance on each call.

// src/cats.service.ts
import { provide, type Provider } from '@caffeine-projects/dicaf'
import { Injectable } from '@caffeine-projects/dicaf/decorators'
import { RequestContext } from './request.context.js'

@Injectable([provide(RequestContext)])
export class CatsService {
constructor(private readonly ctx: Provider<RequestContext>) {}

findAll(): string[] {
console.log(`[${this.ctx.get().correlationId}] findAll`)
return ['Whiskers', 'Shadow']
}
}

this.ctx.get() is called at request time, not at construction time, so it always returns the instance that belongs to the current request.


6. Register routes and start

// src/index.ts
import { DiCaf } from '@caffeine-projects/dicaf'
import { buildServer } from './app.js'
import { CatsService } from './cats.service.js'

const container = new DiCaf()
await container.init()

const server = await buildServer(container)

server.get('/cats', async () => {
return container.get(CatsService).findAll()
})

await server.listen({ port: 3000, host: '0.0.0.0' })

How it works

HTTP request arrives

├─ onRequest hook → requestScopeManager.run()
│ Creates an isolated async context for this request

├─ route handler runs
│ container.get(CatsService) → singleton (reused)
│ CatsService.ctx.get() → RequestContext for this request

└─ response sent
Request scope is discarded — RequestContext is garbage collected

Each concurrent request gets its own RequestContext. They never share state.


What's next

  • Add more request-scoped services the same way — annotate with @Lifetime(Scopes.REQUEST) and inject via provide().
  • Use scan() to auto-import decorated files instead of listing them manually. See the Scanning Files guide.
  • See the Mixing Scopes guide for the full explanation of why provide() is required.
  • A complete working example with PostgreSQL, Redis, and health checks is available in the examples/05-fastify-cats-complete directory.