Feature injection
Use injectFeature() in a DI-managed constructor or field initializer to access a configured feature from user code. HTTP, commands, schedulers, event buses, and custom features use the same mechanism.
Start HTTP from a command
@Controller('/')
class HelloController {
@Get('/')
hello() {
return { message: 'Hello' };
}
}
const web = http({ controllers: [HelloController] });
const listener = httpServer();
@Command({ name: 'serve' })
class ServeCommand {
constructor(
private readonly http = injectFeature(web),
private readonly server = injectFeature(listener),
) {}
async run() {
const server = await this.server.listen(this.http, { port: 3000 });
await server.closed;
}
}
const runtime = await onNode(createApp([command([ServeCommand]), web, listener]));
const result = await runtime.command.execCommand(['serve']);
Injection alone does not start a listener. listen() returns its actual address, an idempotent shutdown() function, and a closed promise. Runtime shutdown closes listeners started from both injected features and the public runtime API. Node handles SIGINT/SIGTERM through its normal shutdown integration.
Declare httpServer() to request listening support from the platform. Node, Bun, txiki.js and Electron provide the environment implementation. A runtime without one rejects that server feature during initialization. HTTP fetch() and request() remain available without a server feature. The old HTTP listen() and serve() methods have been removed.
Select a feature
Pass the exact definition object registered in createApp():
@Injectable()
class Servers {
private readonly admin = injectFeature(admin);
private readonly user = injectFeature(user);
}
Alternatively, select a class-based feature by its class and namespace:
@Injectable()
class FeatureConsumer {
private readonly http = injectFeature(HttpFeature);
private readonly admin = injectFeature(HttpFeature, 'admin');
private readonly commands = injectFeature(CommandFeature);
private readonly scheduler = injectFeature(SchedulerFeature);
private readonly events = injectFeature(EventBusFeature);
}
Omitting the namespace uses the class's static defaultKey, which matches its factory name: http, httpServer, command, scheduler, eventbus, tinyjsBridge, electronIpc, or lambdaHandler. It never selects the first registered instance or an arbitrary named instance. Custom feature classes can declare static readonly defaultKey; structural feature definitions can be injected directly.
The selected definition must be registered at the top level of this runtime. An HTTP child is a route composition unit and is not automatically an independently injectable feature. A newly created definition with identical options does not match the registered object. Duplicate namespaces are still rejected by createApp().
Feature names cannot use the keys in RESERVED_APP_KEYS, exported by @zeltjs/core. This single list covers built-in app operations and environment properties, including shutdown, get, and args, across all adapters. Named feature factories reject these names at compile time; createApp() checks custom feature keys too. Built-in app API definitions use ReservedAppProperties so adding an unlisted property or method is also a type error. JavaScript and dynamically named features are checked against the same list when the app is created.
A namespace selects a feature's capabilities; it does not create a separate DI container or duplicate its underlying services. For example, named EventBus features configured with the same adaptor class share that runtime's adaptor, including its events and subscriptions. This also keeps direct adaptor injection and feature injection connected. Different runtimes have separate instances. For isolated buses, configure separate adaptors and the required backend/channel separation; changing name alone does not isolate a bus.
Initialization and lifetime
Each runtime registers its injection handles before realizing any features. A constructor can hold an injected handle even if that feature has not finished realization. Configurations explicitly supplied through configs are constructed and their resource startup hooks complete first, so feature constructors can read values prepared by those hooks. All features are then realized, warmup constructors run when enabled, and finally feature lifecycle startup hooks execute. ServiceResolver.get() during realization constructs an instance without running its startup hook yet.
Do not call feature operations during constructors, configuration startup, or realize(). An operation attempted before all features are prepared throws FeatureInjectionError with reason not_ready; it does not wait on an initialization cycle. Feature lifecycle hooks can use prepared operations, for example registering an event subscription. This does not make mutually dependent startup operations safe.
Injected handles delegate to the same capabilities as the public runtime namespace, but their object identity is not guaranteed to equal that namespace. Handles are stable within a runtime, separate across runtimes, and reject access after runtime shutdown, including calls through previously extracted methods. Shutdown hooks can use the handles until shutdown completes. Missing definitions report not_registered; a class with no default namespace reports default_not_defined.
Object-valued properties such as http.router retain the same lifetime checks even if stored separately. Aliases and cycles preserve their identity within the injected handle. Values returned by operations, such as an HTTP response or a server handle, retain their own resource contracts.
Feature injection does not make all application initialization lazy. A version command does not implicitly listen, but creating the runtime still performs feature realization and normal lifecycle startup. Put a version-only fast path before runtime creation if those steps must also be avoided.
Public runtime namespaces and injectFeature() share the same guarded feature handle. Access through either path is rejected after runtime shutdown.
Runtime shutdown
Adapters use the runtime's common shutdown() operation. Concurrent calls wait for the same completion and receive the same failure. SIGINT/SIGTERM handling uses this operation too, so repeated signals do not start another shutdown.
runtime.registerShutdown(callback) registers cleanup before service lifecycle shutdown. Use runtime.registerShutdown(callback, { phase: 'after' }) for cleanup that must run after services have stopped, such as detaching process signal handlers. Callbacks within each phase run concurrently and execute at most once. Failures are collected while the remaining cleanup continues; after callbacks still run when lifecycle cleanup fails.
If a lifecycle guard refuses shutdown before disposal, the runtime remains usable and shutdown can be retried. After callbacks remain registered until disposal actually occurs. RuntimeShutdownService, available through DI or runtime.get(), exposes the common state (ready, shuttingDown, or disposed) for integrations that must reject new work as soon as shutdown is requested.