@nodeboot/engine is the package that turns Node-Boot controller metadata into real runtime behavior.
It provides the driver abstraction used by server integrations such as:
@nodeboot/express-server@nodeboot/fastify-server@nodeboot/koa-server@nodeboot/http-server@nodeboot/ghost-server
In practice, the engine sits between Node-Boot's decorator metadata and a concrete server runtime. It imports controllers, middlewares, and interceptors, builds ActionMetadata, resolves action parameters, runs interceptors, and delegates request/response handling to a framework-specific NodeBootDriver.
✅ Driver-based architecture – One controller model, many server implementations.
✅ Framework-agnostic action execution – NodeBootEngine works through NodeBootDriver.
✅ Automatic controller registration – Loads controllers, middlewares, and interceptors through ComponentImporter.
✅ Typed parameter resolution – ActionParameterHandler extracts params from the request and normalizes them.
✅ Class transformation and validation – Integrates with class-transformer and class-validator.
✅ Interceptor pipeline – Runs global, controller, and action interceptors in sequence via runInSequence.
✅ Reusable response/error helpers – ResultTransformer and GlobalErrorHandler reduce driver boilerplate.
✅ Works beyond HTTP – GhostDriver shows that the contract also supports non-network execution.
Main exports from @nodeboot/engine:
NodeBootDriverNodeBootEngineNodeBootToolkitGlobalErrorHandlerResultTransformerMetadataArgsStorageMetadataBuilderServerConfigParamrunInSequenceisPromiseLike
These are the building blocks used by Node-Boot server packages and custom framework adapters.
Most application developers do not install @nodeboot/engine directly. Instead, install a server package that already depends on it:
pnpm add @nodeboot/express-serveror:
pnpm add @nodeboot/fastify-server
pnpm add @nodeboot/koa-server
pnpm add @nodeboot/http-server
pnpm add @nodeboot/ghost-serverIf you are implementing a custom Node-Boot server integration, install the engine directly together with its peer dependencies:
pnpm add @nodeboot/engine class-transformer class-validator
@nodeboot/enginedepends on@nodeboot/contextand@nodeboot/error, and usesclass-transformer/class-validatorfor request normalization and DTO validation.
Applications usually use the engine indirectly through a server package. This is the real pattern used in the repository samples:
import "reflect-metadata";
import {Container} from "typedi";
import {NodeBoot, NodeBootApp, NodeBootApplication, NodeBootAppView} from "@nodeboot/core";
import {EnableDI} from "@nodeboot/di";
import {EnableComponentScan} from "@nodeboot/aot";
import {ExpressServer} from "@nodeboot/express-server";
@EnableDI(Container)
@EnableComponentScan()
@NodeBootApplication()
export class FactsServiceApp implements NodeBootApp {
start(): Promise<NodeBootAppView> {
return NodeBoot.run(ExpressServer);
}
}In this flow, ExpressServer eventually creates an ExpressDriver and calls:
NodeBootToolkit.createServer(driver, engineOptions);The same pattern is used by FastifyServer, KoaServer, HttpServer, and GhostServer.
NodeBootToolkit.createParamDecorator lets you create framework-aware parameter decorators backed by the current Action:
import {NodeBootToolkit} from "@nodeboot/engine";
export const TenantId = () =>
NodeBootToolkit.createParamDecorator({
required: true,
value: action => action.request.headers["x-tenant-id"],
});Internally, this adds metadata into MetadataArgsStorage.params, and ActionParameterHandler resolves it before the controller method is invoked.
If you are integrating Node-Boot with a new server framework, extend NodeBootDriver and pass your driver to NodeBootToolkit.createServer.
import {GlobalErrorHandler, NodeBootDriver, NodeBootToolkit, ResultTransformer, ServerConfig} from "@nodeboot/engine";
import {Action, ActionMetadata, MiddlewareMetadata, NodeBootEngineOptions, ParamMetadata} from "@nodeboot/context";
type ExampleRequest = {
method: string;
url: string;
body?: any;
params?: Record<string, string>;
query?: Record<string, string>;
headers: Record<string, string>;
};
type ExampleResponse = {
statusCode?: number;
headers: Record<string, string>;
body?: any;
};
type ExampleApp = {
use: (handler: Function) => void;
route: (method: string, path: string, handler: Function) => void;
};
class ExampleDriver extends NodeBootDriver<ExampleApp, Action<ExampleRequest, ExampleResponse>> {
private readonly errors = new GlobalErrorHandler();
private readonly results = new ResultTransformer(this);
constructor(app: ExampleApp) {
super();
this.app = app;
}
initialize(): void {
ServerConfig.of(undefined);
}
registerMiddleware(middleware: MiddlewareMetadata, _options: NodeBootEngineOptions): void {
if ((middleware.instance as any).use) {
this.app.use(async (request: ExampleRequest, response: ExampleResponse) => {
await (middleware.instance as any).use({request, response});
});
}
}
registerAction(
actionMetadata: ActionMetadata,
executeCallback: (action: Action<ExampleRequest, ExampleResponse>) => Promise<any>,
): void {
const route = ActionMetadata.appendBaseRoute(this.routePrefix, actionMetadata.fullRoute);
this.app.route(actionMetadata.type.toUpperCase(), route.toString(), async (request, response) => {
await executeCallback({request, response});
});
}
registerRoutes(): void {}
getParamFromRequest(action: Action<ExampleRequest, ExampleResponse>, param: ParamMetadata): any {
switch (param.type) {
case "body":
return action.request.body;
case "param":
return action.request.params?.[param.name];
case "params":
return action.request.params;
case "query":
return action.request.query?.[param.name];
case "queries":
return action.request.query;
case "header":
return action.request.headers[param.name.toLowerCase()];
case "headers":
return action.request.headers;
default:
return undefined;
}
}
async handleError(error: any, action: Action<ExampleRequest, ExampleResponse>): Promise<any> {
action.response.statusCode = error.httpCode || 500;
action.response.body = this.errors.handleError(error);
}
handleSuccess(result: any, action: Action<ExampleRequest, ExampleResponse>, actionMetadata: ActionMetadata): void {
action.response.statusCode = actionMetadata.successHttpCode || 200;
action.response.body = this.results.transformResult(result, actionMetadata);
}
}
const app: ExampleApp = {
use: () => {},
route: () => {},
};
const driver = new ExampleDriver(app);
NodeBootToolkit.createServer(driver, {
routePrefix: "/api",
controllers: [],
middlewares: [],
interceptors: [],
});The abstract contract you must implement is defined by NodeBootDriver:
initialize()registerMiddleware()registerAction()registerRoutes()getParamFromRequest()handleError()handleSuccess()
The server packages in this repository are concrete examples of the engine contract in action.
servers/express-server/src/driver/ExpressDriver.ts
- Extends
NodeBootDriver<Application> - Calls
this.app[actionMetadata.type.toLowerCase()](...)to register routes - Adds body parsing with
body-parser - Adds file upload handling with
multer - Uses
ResultTransformerbefore sending JSON/text/binary responses - Uses
GlobalErrorHandlerplus optional customErrorHandlerInterface
servers/fastify-server/src/driver/FastifyDriver.ts
- Extends
NodeBootDriver<FastifyInstance, Action<FastifyRequest, FastifyReply>> - Uses
this.app.route({...})to register each action - Maps Node-Boot action types to Fastify
HTTPMethods - Registers global hooks and plugins through
ServerConfig - Runs after-middlewares with
onSendand error middlewares withonError
servers/koa-server/src/driver/KoaDriver.ts
- Extends
NodeBootDriver<Koa, Action<Request, Response>> - Registers actions on
@koa/router - Finalizes route setup in
registerRoutes()viarouter.routes()androuter.allowedMethods() - Exposes Koa-specific state through
getParamFromRequest()(state,session,cookies, etc.)
servers/http-server/src/driver/HttpDriver.ts
- Adapts plain Node.js
http.Server - Uses
find-my-wayfor route matching - Stores before/after middleware lists and runs them manually
- Parses JSON bodies itself in
parseJsonBody() - Converts results/errors directly into
ServerResponse
servers/ghost-server/src/driver/GhostDriver.ts
- A non-HTTP driver used for CLI/test/auto-configuration scenarios
- Does not register routes or listen on a socket
- Exposes
executeAction(...)to run a controller action in memory - Proves the engine contract is broader than HTTP frameworks
createServer(driver, options) calls createEngine(driver, options) and returns driver.app.
Before registration starts, NodeBootToolkit.configureDriver(...) pushes engine options into the driver instance:
routePrefixauthorizationCheckercurrentUserCheckerclassTransformerclassToPlainTransformOptionsplainToClassTransformOptionsvalidationdevelopment
NodeBootToolkit also imports:
- controllers via
ComponentImporter.importControllers(...) - middlewares via
ComponentImporter.importMiddlewares(...) - interceptors via
ComponentImporter.importInterceptors(...)
If those values are provided as directory globs, ClassFiles.loadFromDirectories(...) requires the matching files and collects exported classes.
MetadataArgsStorage is the global metadata registry used by the engine.
MetadataBuilder reads from it and creates:
ControllerMetadataActionMetadataMiddlewareMetadataInterceptorMetadataParamMetadataResponseHandlerMetadata
This is where controller-level and action-level uses, interceptors, params, and response handler metadata are assembled.
NodeBootEngine orchestrates the boot order:
initialize()registerInterceptors(...)registerMiddlewares("before", ...)registerControllers(...)registerMiddlewares("after", ...)
For each controller action, NodeBootEngine.registerControllers(...) calls driver.registerAction(...) with an execution callback that eventually reaches executeAction(...).
Before a controller method is called, ActionParameterHandler.handle(...) resolves all declared parameters.
It can:
- inject raw
request,response, orcontext - resolve
body,body-param,param,params,query,queries,header,headers,cookie,cookies,session,session-param,file, andfiles - run custom transforms attached to parameter metadata
- resolve
current-userusingdriver.currentUserChecker - normalize primitive values with
Param - parse JSON strings for object-like values
- run
plainToInstance(...)when transformation is enabled - run
validateOrReject(...)when validation is enabled
This is the layer that makes controller method signatures feel declarative while still remaining framework-agnostic.
Once the controller method returns, NodeBootEngine.handleResult(...) runs interceptor functions with runInSequence(...).
The interceptor chain includes:
- global interceptors registered through
registerInterceptors(...) - controller interceptors
- action interceptors
If an interceptor class implements intercept(...), it is resolved from the DI container through getFromContainer(...).
After interceptors complete, the engine delegates the final output to the driver:
handleSuccess(result, action, actionMetadata)for successful executionhandleError(error, action, actionMetadata)for failures
ResultTransformer helps drivers serialize class instances with instanceToPlain(...), while GlobalErrorHandler converts Error objects into safe JSON-friendly payloads.
This separation is the core idea of the package:
The engine understands Node-Boot metadata and execution flow; the driver understands the target server runtime.
ServerConfig is a small helper used by the official drivers to activate optional server features in a consistent way.
It supports:
ifCors(...)ifCookies(...)ifSession(...)ifMultipart(...)ifTemplate(...)
This is why ExpressDriver, FastifyDriver, and KoaDriver can conditionally enable framework-specific plugins or middleware without duplicating option parsing logic.
Use it to normalize Error and HttpError instances before sending them to the client.
Use it when your driver needs to serialize class instances while respecting Node-Boot response transformation settings.
Useful in tests or hot-reload scenarios. It resets MetadataArgsStorage and internal bootstrap state so metadata does not accumulate across runs.
Use NodeBootToolkit.createServer(driver, options) when you want the standard bootstrap flow and want the framework app returned as driver.app.
Use NodeBootToolkit.createEngine(driver, options) when you already own the app instance and only need Node-Boot registration side effects.
@nodeboot/engine is the execution core behind Node-Boot server integrations.
It:
- reads global metadata from
MetadataArgsStorage - builds controller/action/middleware/interceptor metadata through
MetadataBuilder - resolves action parameters through
ActionParameterHandler - runs interceptors through
NodeBootEngine - delegates transport-specific behavior to a
NodeBootDriver
If you want Node-Boot to work with a new framework, this is the package you build on.