Compile-time DI
A compiler reads your TypeScript at build time, binds every constructor parameter to a provider and generates the container. No reflect-metadata, no runtime type lookup.
How the compiler works
Appearance
A backend framework whose compiler resolves and validates your dependency graph at build time. The runtime only runs generated code.
$ npx @znight/cli new my-serviceBenchmarks
No metadata to reflect, no graph to sort: a Znight application imports its container and calls its factories.
Medians on an Apple M5 with Node.js 24.21.0 · HTTP applications on Fastify · Every result, and where Znight is slower →
You write services and controllers with familiar decorators. At build time, the compiler turns them into a plain module: every provider, its dependencies, its scope and the order to create them in.
import { Injectable } from '@znight/core'
import { UserRepository } from './user.repository.js'
@Injectable()
export class UserService {
constructor(private readonly users: UserRepository) {}
findById(id: number) {
return this.users.findById(id)
}
}import { Controller, Get, Param } from '@znight/http'
import { UserService } from './user.service.js'
@Controller('/users')
export class UserController {
constructor(private readonly users: UserService) {}
@Get('/:id')
getUser(@Param('id') id: number) {
return this.users.findById(id)
}
}import { Application } from '@znight/core'
import { HttpPlugin } from '@znight/http'
import { __ZNIGHT_METADATA__ } from './.znight/container.generated.js'
const app = await Application.create({
metadata: __ZNIGHT_METADATA__,
plugins: [HttpPlugin.configure({ port: 3000 })],
})
await app.start()// AUTO-GENERATED by @znight/compiler. Do not edit.
export const __ZNIGHT_METADATA__: ZnightMetadata = {
version: 1,
providers: {
['src/users/pg-user.repository.ts#PgUserRepository']: {
scope: 'singleton',
class: PgUserRepository,
deps: ['src/db/db.factory.ts#DbFactory.createPool'],
factory: d => new PgUserRepository(d[0]),
},
['src/users/user.service.ts#UserService']: {
scope: 'singleton',
class: UserService,
deps: ['src/users/pg-user.repository.ts#PgUserRepository'],
factory: d => new UserService(d[0]),
},
// ...
},
resolutionOrder: [
'src/db/db.factory.ts#DbFactory',
'src/db/db.factory.ts#DbFactory.createPool',
'src/users/pg-user.repository.ts#PgUserRepository',
'src/users/user.service.ts#UserService',
'src/users/user.controller.ts#UserController',
],
aliases: { ['src/users/user.repository.ts#UserRepository']: 'src/users/pg-user.repository.ts#PgUserRepository' },
}UserRepository is an interface: the compiler found its only implementation, PgUserRepository, and bound it. Nothing is looked up by name or reflected at runtime.
Runtime DI containers (NestJS, InversifyJS, tsyringe...) discover your dependency graph while the application boots, using reflect-metadata and decorator side effects. Znight does that work once, in the compiler.
| Runtime DI | Znight | |
|---|---|---|
| Dependency resolution | Reflection at boot (emitDecoratorMetadata) | Static analysis at build time |
| Wiring mistakes | Crash at startup, or on the first request | Build error with a code, a location and a fix |
| What runs in prod | Decorators, metadata maps, resolution logic | Generated factories called in a precomputed order |
| Interfaces as tokens | Erased at runtime: string tokens + @Inject | Injected by type, implementation found by the compiler |
| Structure | Modules, imports/exports, forwardRef | One flat container, providers discovered by scanning |
| Cold start | Graph discovered and sorted on every boot | Only your constructors run: the graph was solved at build |
| Debugging | Framework internals | A readable container.generated.ts you can step through |
$ vite build
✗ ZNIGHT001: Missing provider for 'EmailService' required by NotificationService
--> src/notification/notification.service.ts:12:29
|
12 | private readonly email: EmailService,
| ^^^^^^^^^^^^
= hint: EmailService (src/email/email.service.ts:3) is not a provider: add @Injectable()
Build failed with 1 errorEvery diagnostic has a stable code documented in the error reference, with an example and the usual fixes.
Znight wires your data layer; it does not replace it. A @Factory() opens the client at boot (async, awaited before the first request), closes it on shutdown, and repositories behind interfaces keep services testable with mocks. Use the library that fits your data: for SQL, we recommend Drizzle. Read the data access guide →
The repository ships seven production-shaped example applications, each with unit, application and end-to-end tests on Node.js and Bun:
Ready? Create your first application → · Coming from NestJS? Read the migration guide → · What comes next? Read the roadmap →