Appearance
Introduction
Znight is a backend framework for TypeScript in the spirit of NestJS and Spring Boot: you write providers (services, repositories, controllers, consumers...) and let a container wire them together. The difference is when the wiring happens.
In Znight, dependency injection is resolved at compile time. A compiler (a Vite plugin, also usable from the CLI and Bun) reads your TypeScript sources, binds every constructor parameter to a provider, validates the whole graph and generates a plain TypeScript module. At runtime, the container only calls the generated factories, in an order computed ahead of time.
ts
@Injectable()
export class NotificationService {
constructor(
private readonly email: EmailService, // bound by the compiler
private readonly users: UserRepository, // an interface: bound to its implementation
) {}
}It is the approach of Micronaut and Dagger in the JVM world, brought to TypeScript.
Principles
- Compile-time DI. No
reflect-metadata, noemitDecoratorMetadata, no runtime type lookup. - Decorators are markers. Every Znight decorator is a no-op at runtime: the compiler reads them from the source code, and the Vite plugin strips them from the bundle.
- Flat DI. One container, no modules. Providers are discovered by scanning your sources (and provider libraries).
- Composable. HTTP, OpenAPI, messaging, scheduling, configuration and observability are separate packages, each made of marker decorators, a compiler extension and a runtime plugin.
- Runtime agnostic. Runtime packages run on Node.js 24+ and Bun 1.3+ (ESM only), using standard APIs only.
- Fast. The compiler is built on oxc with per-file caches; the runtime creates independent async singletons concurrently.
- Fail loudly, early, precisely. Everything that can be detected at build time is a
ZNIGHTxxxdiagnostic with a source location and hints. Runtime errors carry a code, the dependency path and the originalcause.
Runtime DI vs compile-time DI
With a runtime container, the dependency graph is discovered while the application boots: decorators record metadata, TypeScript emits constructor parameter types (design:paramtypes), and the container reflects on all of it to create instances. Every wiring mistake becomes a startup crash, and interfaces (erased at runtime) need string tokens.
With Znight, that work happens once, in the compiler:
| Concern | Runtime DI (NestJS, InversifyJS, tsyringe) | Znight |
|---|---|---|
| How types are known | emitDecoratorMetadata + reflect-metadata at runtime | Read from the source by the compiler |
| Interfaces | Erased: you inject string or symbol tokens with @Inject() | Injected by type; the compiler finds the implementation (implements / extends) |
| Missing provider, cycle | Discovered at boot (or on first use) | ZNIGHT001, ZNIGHT002 at build time |
| Singleton → request scope | Silent bug or runtime error | ZNIGHT003 at build time |
| Organisation | Modules with imports/exports/providers | Flat: every @Injectable() in src/ is a provider |
| Startup work | Reflection, graph resolution, sorting | Call generated factories in a precomputed order |
| Debugging the wiring | Read framework internals | Read container.generated.ts |
| Bundle | Decorator calls and metadata kept at runtime | Decorators stripped; unused providers can be pruned |
What you keep from runtime frameworks: decorators, constructor injection, scopes, lifecycle hooks, plugins, guards and interceptors, testing utilities with auto-mocking.
How it fits together
@znight/compileranalyses the code and generates__ZNIGHT_METADATA__.@znight/coreexecutes it: container, scopes, lifecycle, plugins, hooks, AOP.- Feature packages (HTTP, OpenAPI, messaging, scheduling, config, observability) add decorators, a compiler extension and a runtime plugin each.
@znight/testingand@znight/clicover tests, scaffolding and the development loop.
When to use Znight
Znight is a good fit for HTTP APIs, message consumers, scheduled jobs and workers, serverless functions where cold starts matter, and teams that want NestJS-style structure without modules and runtime reflection.
It is not a frontend framework, and it does not try to hide the build step: the compiler is part of your toolchain (through Vite, the CLI or the Bun preload). It does not ship an ORM either: you provide the database client of your choice, and Znight wires it (Data access).
Project status
Before 1.0
Znight is preparing its first release (v0.1). Every package is implemented and tested on Node.js 24 and Bun: the runtime, the compiler (Vite plugin, Bun preload, CLI), HTTP (with access decisions, CORS and security headers, files, cookies and Server-Sent Events), OpenAPI, messaging, scheduling, configuration, observability and the testing utilities, with seven runnable example applications. WebSocket gateways and GraphQL are planned for v0.2 (see the roadmap). Until 1.0, minor versions may still change APIs. Feedback and contributions are welcome on GitHub.
Next steps
- Getting started: create your first application.
- Dependency injection: providers, interfaces, qualifiers, factories.
- The compiler: what happens at build time, options and the generated module.
- Examples: complete applications, from a REST API on Drizzle to an uptime monitor.
- Migrating from NestJS: a decorator-by-decorator mapping.