Docs Home | Previous: Response Validation | Next: Modules
bun-openapi includes a lightweight DI container with provider tokens, scopes, and visibility enforcement.
The container has two scopes that form a parent-child relationship:
+--------------------------------------+ createRequestScope() +----------------------------------+
| Container | -----------------------------> | RequestScope |
| | | |
| providers: Map<token, factory> | | cache: Map<token, instance> |
| singletons: Map<token, instance> | | parent: Container |
| | | |
| register(provider, visibility) | | resolve(token) |
| resolve(token) | | instantiate(ctor) |
+--------------------------------------+ +----------------------------------+
A new RequestScope is created for every incoming HTTP request. It inherits singleton instances from the parent Container and maintains its own cache for request-scoped providers.
Register providers in createApp({ providers }) or inside a @Module. Five forms are supported:
| Form | Usage |
|---|---|
| Class shorthand | UserService — registers the class as its own token |
useClass |
{ provide: Token, useClass: UserService } |
useValue |
{ provide: Token, useValue: value } — wraps an existing instance |
useFactory |
{ provide: Token, useFactory: (a, b) => val, inject: [A, B] } |
useExisting |
{ provide: NewToken, useExisting: ExistingToken } — alias |
A token can be a class constructor, a string, or a symbol.
// class token — resolved automatically via reflect-metadata
providers: [UserService];
// string token — requires @Inject("APP_NAME") at the injection site
providers: [{ provide: "APP_NAME", useValue: "My App" }];
// symbol token — requires @Inject(DB_TOKEN) at the injection site
const DB_TOKEN = Symbol("DB");
providers: [{ provide: DB_TOKEN, useValue: db }];Mark every class you want the container to manage with @Injectable():
@Injectable()
export class UserService {
constructor(private readonly db: Database) {}
}Register both the service and its dependencies in providers:
const app = createApp({
schema: classValidator(),
controllers: [UserController],
providers: [
UserService,
{ provide: "APP_NAME", useValue: "DI Example App" },
],
});For class tokens (the common case), the container resolves the dependency automatically using reflect-metadata — no decorator needed on the parameter:
@Injectable()
export class OrderService {
constructor(private readonly userService: UserService) {}
}For string or symbol tokens, use @Inject(token) on the constructor parameter:
@Injectable()
export class MailService {
constructor(@Inject("APP_NAME") private readonly appName: string) {}
}@Inject(token) also works on class fields:
@Route("/items")
export class ItemsController extends Controller {
@Inject(UserService)
userService!: UserService;
@Inject("APP_NAME")
appName!: string;
}Use useValue to hand an existing object — for example a TypeORM DataSource — into the container so services can receive it instead of importing the singleton directly:
// server.ts
import { DataSource } from "typeorm";
import { AppDataSource } from "./data-source.js";
await AppDataSource.initialize();
const app = createApp({
providers: [
UserService,
{ provide: DataSource, useValue: AppDataSource },
],
});// user.service.ts
import { DataSource } from "typeorm";
@Injectable()
export class UserService {
#repo: Repository<User>;
constructor(dataSource: DataSource) {
this.#repo = dataSource.getRepository(User);
}
}Because DataSource is a class token, reflect-metadata resolves it without @Inject. See examples 11–14 for working implementations of this pattern.
Use useFactory when the value depends on other providers:
providers: [
{ provide: "DB_HOST", useValue: "localhost" },
{
provide: "DB_URL",
useFactory: (host: string) => `postgres://${host}/mydb`,
inject: ["DB_HOST"],
},
];| Scope | Behaviour |
|---|---|
"singleton" (default) |
One instance for the lifetime of the app, shared across all requests |
"request" |
A fresh instance per request, isolated to that request's scope |
@Injectable({ scope: "request" })
export class RequestContext { ... }When the container resolves a token, it follows this decision tree:
resolve(token, visibleTokens)
|
+-- token not visible? ---------- yes -> error: token not accessible
|
+-- singleton cached? ----------- yes -> return cached instance
|
+-- provider registered? -------- no -> error: unknown provider token
|
+-- currently resolving? -------- yes -> error: circular dependency
|
`-- call provider.factory(resolver)
|
+-- useValue -> return value directly
+-- useExisting -> resolve the aliased token
+-- useFactory -> resolve inject[] deps, then call factory
`-- useClass -> instantiateAndInject()
|
+-- read design:paramtypes via reflect-metadata
+-- for each param:
| +-- @Inject(token)? -> use explicit token
| `-- otherwise -> use reflected class type
+-- resolve each dependency recursively
+-- new Ctor(...resolvedDeps)
`-- inject @Inject() decorated fields
For singleton providers, the resolved instance is cached in the root Container after the first resolution. For request-scoped providers, the instance is cached in the RequestScope and discarded when the request ends.
The container tracks which tokens are currently being resolved. If a token is encountered again during its own resolution chain, a circular dependency error is thrown:
Circular dependency detected: UserService → OrderService → UserService
Break cycles by using useFactory with lazy resolution or restructuring your dependencies.
When using Modules, each provider and controller has a visibility set — the tokens it is allowed to resolve. If a controller tries to inject a provider from another module that was not exported, the container throws an error at startup. This enforces module encapsulation.
- 04_dependency-injection — service tokens,
@Inject, value providers - 11_jwt-auth —
DataSourceValueProvider, JWT guard as injectable - 12_form-auth —
DataSourceValueProvider, injectable guard - 13_typeorm-relations —
DataSourceValueProvider, multiple repositories from one injected source - 14_session-auth —
DataSourceValueProvider alongside an in-memorySessionStore - 15_request-scope —
@Injectable({ scope: "request" })for per-request state isolation