Dependency Injection

Docs Home | Previous: Response Validation | Next: Modules

bun-openapi includes a lightweight DI container with provider tokens and scopes.

Provider Forms

Register providers in createApp({ providers }) or inside a @Module. Five forms are supported:

FormUsage
Class shorthandUserService — 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

Provider Tokens

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 }];

Registering Services

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" }, ], });

Constructor Injection

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) {} }

Field Injection

@Inject(token) also works on class fields:

@Route("/items") export class ItemsController extends Controller { @Inject(UserService) userService!: UserService; @Inject("APP_NAME") appName!: string; }

Injecting External Instances (ValueProvider)

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.

Factory Provider

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"], }, ];

Scopes

ScopeBehaviour
"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 { ... }

Examples