Thank you for considering a contribution to Node-Boot! This guide is the detailed companion to the Integration Points section in the root README. Read that section first for the high-level map, then come here for concrete, step-by-step guidance and code examples for each contribution type.
- Getting Started
- 1. Server Integrations
- 2. Core Feature Contributions
- 3. Runtimes
- 4. Starter Packages
- Flavour 1 — SDK/Client Auto-Configuration
- Flavour 2 — Method Decorators with a Lifecycle Adapter
- Flavour 3 — Class Decorators with a Lifecycle Adapter
- Flavour 4 — Registering New Decorators with AOT / Component Scan
- Flavour 5 — Conditional Clients Based on Configuration
- Flavour 6 — Multiple Beans via a Factory Configuration
- Application-Level Custom Decorators
- Pull Request Checklist
git clone https://github.com/nodejs-boot/node-boot.git
cd node-boot
pnpm install
pnpm dev # builds & watches every package with Turborepo + NodemonBefore opening a PR, always run:
pnpm lint-format # lint + format check
pnpm tsc # type-check every package
pnpm test # run the full test suiteCommit messages must follow Conventional Commits.
Server integrations bind Node-Boot's decorator/engine model to a concrete runtime. Your application code
(controllers, beans, starters) never changes — only the adapter package does. All adapters implement the
NodeBootDriver contract from @nodeboot/engine and extend BaseServer from
@nodeboot/core.
Location: servers/* — e.g. express-server, fastify-server,
koa-server, http-server, encore-server.
An HTTP server adapter has two responsibilities:
XxxServer— extendsBaseServer<TFramework, TRouter>, creates the underlying framework app/router (e.g.express(),Fastify(),new Koa()), and exposes it toNodeBoot.run(XxxServer).XxxDriver— extendsNodeBootDriver<TFramework>from@nodeboot/engineand implements:initialize()— any framework-specific bootstrapping (body parsers, CORS, etc).registerMiddleware(middleware, options)— wires a Node-Boot@Middlewareinto the framework's middleware chain.registerAction(actionMetadata, executeCallback)— maps a Node-Boot controller action onto a framework route handler.registerRoutes()— flushes all registered routes onto the framework router.getParamFromRequest(action, param)— extracts@Param,@Body,@QueryParam, etc. from the framework's request object.handleError(...)/handleSuccess(...)— map Node-Boot's response/error handling onto the framework's response object.
Steps to add a new HTTP server adapter:
- Scaffold a new package under
servers/your-framework-server(copyservers/koa-serveras a starting template — it's a good, compact reference). - Implement
YourFrameworkServer extends BaseServerandYourFrameworkDriver extends NodeBootDriver. - Support the full request/response parameter surface: path params, query params, headers, body, files, and the authorization/
@CurrentUserhooks from@nodeboot/authorization. - Add a sample under
samples/sample-your-frameworkthat mirrorssamples/sample-expressso all starters (persistence, validation, OpenAPI, scheduling, ...) are exercised against your adapter. - Document the package with a README following the pattern used by existing adapters (overview, features, usage, configuration).
Location: serverless/* — e.g. lambda-server, cloudflare-server,
vercel-server, netlify-server,
google-cloud-functions-server.
Serverless adapters follow the same NodeBootDriver contract, but instead of binding to a long-lived HTTP server,
they typically:
- Build the Node-Boot app once (outside the handler, so it's reused across warm invocations).
- Expose a platform-specific handler function (e.g. AWS Lambda's
(event, context) => ..., Cloudflare'sfetch(request, env, ctx), Vercel's(req, res) => ...) that translates the platform's request/response shape into theNodeBootDriveraction lifecycle. - Take care of cold-start performance — avoid unnecessary work inside the handler body.
Steps to add a new serverless adapter:
- Scaffold a new package under
serverless/your-platform-server(useserverless/lambda-serverorserverless/vercel-serveras references — both are minimal, focused implementations). - Implement a driver that translates the platform's native request/response objects to/from Node-Boot's
Action. - Export a handler factory, e.g.
export function createHandler(AppClass) { ... }, so users can do:export const handler = createHandler(MyNodeBootApp);
- Add a sample under
samples/sample-your-platformdeploying a minimal Node-Boot app to the target platform, including any requiredplatform.json/config files for local emulation.
Location: planned — no packages published yet. This is an open contribution opportunity.
The goal is to embed a Node-Boot application inside a native desktop app shell such as Electron or Tauri, so the same controllers/services/starters that power a web API can run as the backend of a desktop application (e.g. exposing an internal HTTP/IPC API to the renderer process).
If you want to pioneer this integration:
- Open an issue describing the target framework (Electron first is recommended, since it's Node.js-native).
- Propose whether the adapter binds Node-Boot to Electron's main process directly (IPC-based
NodeBootDriver) or wraps one of the existing HTTP adapters (e.g.http-server) running embedded in the main process. - Follow the same
BaseServer/NodeBootDrivercontract used by the HTTP adapters above so the integration stays consistent with the rest of the framework. - Add a sample under
samples/sample-electrondemonstrating a minimal desktop app powered by Node-Boot.
Core contributions touch the framework itself — packages/core, packages/context, packages/di,
packages/engine, packages/config, packages/aot, packages/authorization, packages/error, or
packages/tools — rather than an integration point.
This includes:
- Adding or improving a core decorator (e.g. new controller/param/config decorators).
- Extending the application lifecycle (
@Lifecycle,ApplicationFeatureAdapter) with new phases or hooks. - Improving the DI container integration, AOT scanning, or configuration loading.
- Reporting and fixing bugs anywhere in the framework — this is one of the most valuable and accessible ways to contribute, even without deep framework knowledge.
Steps:
- Open an issue first for anything beyond a small bug fix, describing the motivation and proposed API.
- Add/update unit tests in the affected package (
packages/*/testor co-located*.test.tsfiles). - Update the package's own README with the new decorator/API and a usage example.
- Run
pnpm tsc && pnpm testfor the whole workspace — core changes often ripple into starters and samples.
"Runtimes" contributions are about how and where a Node-Boot application is deployed and operated once it's built — they don't change framework code at all. Anything that can wrap/manage a Node.js process can wrap a Node-Boot application. Contributions here typically live as documentation, examples, and infra templates rather than published npm packages, and are a great entry point for infra/DevOps-minded contributors.
Examples of welcome contributions:
- Kubernetes — production-ready
Dockerfiles, Helm charts or plain manifests, health-check wiring using the@nodeboot/starter-actuator/actuator/healthendpoint for liveness/readiness probes, and a reference infra project. - Platformatic (Watt) — a wrapper/guide showing how to run a Node-Boot app as a Watt service.
- PM2 — an ecosystem file (
ecosystem.config.js) and guide for running a Node-Boot app as a managed PM2 process (clustering, zero-downtime reload, log management). - Any other process manager, container runtime, or PaaS (Docker Compose, Nomad, Fly.io, Render, etc).
Steps:
- Add a new folder under
samples/(e.g.samples/sample-kubernetes,samples/sample-pm2) with a minimal Node-Boot app plus the runtime-specific configuration (Dockerfile, k8s manifests,ecosystem.config.js, etc). - Include a README explaining prerequisites, how to build, and how to run/deploy locally.
- Where relevant, wire up
@nodeboot/starter-actuatorhealth/metrics endpoints so the sample demonstrates production-grade operational readiness (liveness/readiness probes, Prometheus scraping, graceful shutdown).
Starters live under starters/* and are how third-party SDKs, clients, and platforms get integrated into
Node-Boot through auto-configuration and AOT scanning. There are several distinct "flavours" of starter,
depending on what you're integrating. Pick the flavour(s) that match your integration — most starters combine
more than one.
The simplest flavour: wrap a third-party SDK/client so it's configured from app-config.yaml and registered in
the IoC container, ready to be @Inject()-ed into services. Reference: @nodeboot/starter-openai.
// config/OpenAIConfiguration.ts
import {Bean, Configuration} from "@nodeboot/core";
import {BeansContext} from "@nodeboot/context";
import OpenAI from "openai";
@Configuration()
export class OpenAIConfiguration {
@Bean()
public openAiConfig({logger, config, iocContainer}: BeansContext): void {
const openAiConfigs = config.getOptional<{baseURL: string; apiKey: string}>("integrations.openai");
if (openAiConfigs) {
iocContainer.set(OpenAI, new OpenAI(openAiConfigs));
logger.info("OpenAI client successfully configured");
} else {
logger.warn('No "integrations.openai" config found in app-config.yaml');
}
}
}// decorator/EnableOpenAI.ts
import {OpenAIConfiguration} from "../config";
export const EnableOpenAI = (): ClassDecorator => {
return () => {
new OpenAIConfiguration();
};
};Users then enable it with a single decorator on their application class:
@EnableOpenAI()
@EnableDI(Container)
@NodeBootApplication()
export class MyApp implements NodeBootApp {
start() {
return NodeBoot.run(ExpressServer);
}
}Starters that introduce a method decorator (like @Scheduler(...)) need a runtime adapter tied to a specific
application lifecycle phase, since the decorated method must be wired up once the application (and its
dependencies) are ready. Reference: @nodeboot/starter-scheduler.
// decorator/Scheduler.ts — collects metadata at decoration time
import {ApplicationContext} from "@nodeboot/context";
import {SchedulerAdapter} from "../adapter";
export function Scheduler(cronExpression: string): MethodDecorator {
return function (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
const schedulerAdapter = new SchedulerAdapter({
target,
cronExpression,
cronFunction: descriptor.value,
});
ApplicationContext.get().applicationFeatureAdapters.push(schedulerAdapter);
};
}// adapter/SchedulerAdapter.ts — does the real work once the app lifecycle reaches this phase
import {ApplicationFeatureAdapter, ApplicationFeatureContext, Lifecycle} from "@nodeboot/context";
import cron from "node-cron";
@Lifecycle("persistence.started")
export class SchedulerAdapter implements ApplicationFeatureAdapter {
constructor(private readonly options: {target: any; cronFunction: Function; cronExpression: string}) {}
bind({logger, iocContainer}: ApplicationFeatureContext): void {
const {target, cronFunction, cronExpression} = this.options;
const componentBean = iocContainer.get(target.constructor);
cron.schedule(cronExpression, () => cronFunction.apply(componentBean));
logger.info(`Registered scheduler ${target.constructor.name}::${cronFunction.name}`);
}
}Pair the method decorator with an @Enable...() feature-flag decorator (see EnableScheduling)
so the feature can be toggled on/off, and check allowedProfiles(target) in your adapter's bind() method to
respect @Profile(...) filtering.
Available @Lifecycle(...) phases (from @nodeboot/context):
| Phase | Runs |
|---|---|
application.initialized |
After core application setup but before services start |
persistence.started |
After the persistence/data layer is initialized |
application.started |
After all services are up and the application is ready |
application.stopped |
During graceful shutdown |
Starters that introduce a class decorator (like @HttpClient(...)) follow the same lifecycle-adapter pattern
as Flavour 2, but register a whole class instance (typically a client/service) rather than a single method.
Reference: @nodeboot/starter-http.
// decorator/HttpClient.ts
import {ApplicationContext} from "@nodeboot/context";
import {HttpClientAdapter} from "../adapter";
export function HttpClient(config: HttpClientConfig | string, plugins?: PluginConfigs): ClassDecorator {
return function (target: any) {
const adapter = new HttpClientAdapter(target, config, plugins);
ApplicationContext.get().applicationFeatureAdapters.push(adapter);
};
}// adapter/HttpClientAdapter.ts
import {ApplicationFeatureAdapter, ApplicationFeatureContext, Lifecycle} from "@nodeboot/context";
import axios from "axios";
@Lifecycle("application.started")
export class HttpClientAdapter implements ApplicationFeatureAdapter {
constructor(
private readonly targetClass: new (...args: any[]) => any,
private clientConfig: HttpClientConfig | string,
) {}
bind({logger, iocContainer, config}: ApplicationFeatureContext): void {
const resolvedConfig = this.resolveConfig(config);
const client = axios.create(resolvedConfig);
iocContainer.set(this.targetClass, client);
logger.info(`Registered HTTP client ${this.targetClass.name}`);
}
// ...
}Usage — the decorated class becomes an injectable client:
@HttpClient({baseURL: "https://api.example.com"})
export class ExampleApiClient extends HttpClientStub {}
@Service()
class MyService {
constructor(private readonly client: ExampleApiClient) {}
}Any new framework decorator you introduce (except validation decorators, which are handled by
class-validator) must be recognized by @nodeboot/aot so it gets discovered and
pre-processed when the application is decorated with @EnableComponentScan().
- If the decorator is part of the core framework, add its name to
MAIN_DECORATORSinpackages/aot/src/decorators.main.js. - If the decorator belongs to a starter package you're contributing, you don't need to touch
@nodeboot/aotdirectly — instead, document that consumers should register it as a custom decorator:
@EnableComponentScan({
customDecorators: [YourNewDecorator],
})
@NodeBootApplication()
export class MyApp implements NodeBootApp {
start() {
return NodeBoot.run(FastifyServer);
}
}This tells the AOT scanner to also treat classes annotated with @YourNewDecorator as beans to import during
startup (either via the prebuilt node-boot-beans.json manifest or live scanning in development).
You don't have to contribute to the framework to create your own decorators — application owners can define project-specific decorators following the same Node-Boot patterns and register them the same way:
/**
* **ScheduledProvider Decorator**
*
* Registers a **scheduled provider**, which runs based on a cron schedule.
* It integrates with **NodeBoot** by adding the provider to the application feature adapters.
*/
export function ScheduledProvider<T extends ProviderClass>(options: ScheduledProviderOptions) {
return (providerClass: T) => {
ApplicationContext.get().applicationFeatureAdapters.push(
new ProviderAdapter(providerClass, ProviderType.SCHEDULED, options),
);
Service()(providerClass); // Mark the class as a NodeBoot service
};
}Then register it manually on your application class:
@EnableComponentScan({
customDecorators: [ScheduledProvider],
})
@NodeBootApplication()
export class ProvidersRunnerApplication implements NodeBootApp {
start(injectedConfig?: JsonObject): Promise<NodeBootAppView> {
return NodeBoot.run(FastifyServer, injectedConfig);
}
}And use it on a provider class:
@ScheduledProvider({
slug: "catalog-s3-exporter",
name: "Catalog S3 Exporter Provider",
collection: "catalog-export-facts",
description: "Exports catalog services and systems to S3 for downstream Datadog Workflow consumption",
cron: "55 * * * 1-5", // Every hour at minute 55, Mon–Fri — runs BEFORE Datadog Workflows at minute 0
})
export class CatalogExporterProvider extends BaseProvider {
// Inject beans from DI container (e.g., S3Client) using the @Inject decorator
@Inject("S3Client")
private readonly s3Client: S3Client;
async run(): Promise<void> {
this.logger.info("Running CatalogExporterProvider...");
// ...implementation...
}
}This same pattern (decorator + ApplicationFeatureAdapter + @Lifecycle(...)) is exactly what Flavours 2 and 3
above use — so once you're comfortable writing a custom application-level decorator, you already know how to
build a starter package decorator too.
Some starters register their client(s) only if the relevant configuration is present, using @Configuration's
onConfig option. This avoids failing or noisy startup logs when a given integration isn't configured for the
current environment. Reference: @nodeboot/starter-aws (S3ClientConfiguration).
@Configuration({onConfig: "integrations.aws.s3.region"})
export class S3ClientConfiguration {
@Bean()
public async s3Client({logger, config, iocContainer}: BeansContext) {
const {S3Client} = await import("@aws-sdk/client-s3");
const region = config.getString("integrations.aws.s3.region");
const credentials = config.getOptional<AwsCredentialIdentity>("integrations.aws.credentials");
iocContainer.set(S3Client, new S3Client({region, credentials}));
}
}The @Bean method is only invoked when config.has("integrations.aws.s3.region") is true, so multiple
conditional clients (S3, SQS, SNS, DynamoDB, Secrets Manager, ...) can live side-by-side in the same starter and
only the ones actually configured get initialized — as seen across starters/aws/src/config/*.
Some starters expose several related clients/services from a single @Configuration class, following a beans
factory approach — one @Bean per capability, each independently injectable. Reference:
@nodeboot/starter-firebase (FirebaseAdminConfiguration).
@Configuration()
export class FirebaseAdminConfiguration {
@Bean()
public initFirebase({logger, config}: BeansContext) {
const serviceAccountConfig = config.get<FirebaseIntegrationConfig>("integrations.firebase");
admin.initializeApp({credential: admin.credential.cert(serviceAccountConfig.serviceAccount)});
}
@Bean(FIREBASE_AUTH_BEAN)
public firebaseAuth(): auth.Auth {
return admin.auth();
}
@Bean(FIREBASE_FIRESTORE_BEAN)
public firestoreClient(): firestore.Firestore {
return admin.firestore();
}
@Bean(FIREBASE_STORAGE_BEAN)
public firebaseStorage(): storage.Storage {
return admin.storage();
}
// ...messaging, remoteConfig, appCheck, machineLearning, etc.
}Each named bean (FIREBASE_AUTH_BEAN, FIREBASE_FIRESTORE_BEAN, ...) can then be injected independently:
@Service()
class UserService {
constructor(@Inject(FIREBASE_AUTH_BEAN) private readonly auth: auth.Auth) {}
}Use this flavour when your integration's SDK exposes multiple independent sub-services that consumers may want to inject selectively, rather than a single monolithic client.
- Change is scoped to a single concern (one adapter, one starter, one core fix).
-
pnpm lint-format,pnpm tsc, andpnpm testpass locally. - New/changed decorators are documented in the package README with a usage example.
- New framework decorators are registered per Flavour 4 if applicable.
- A sample app was added/updated to demonstrate the change, where relevant.
- Commit messages follow Conventional Commits.
- PR description explains the motivation and links any related issue.
Thank you for helping grow Node-Boot! 🚀









