Building Services with Node.js
Create your first BSB service plugin. This guide covers setup, event handling, and configuration. Docker build and production deployment are covered in separate guides.
Requirements
- Node.js 24.0.0 or higher
- npm 11.0.0 or higher
- Docker (optional, for image builds and production deployment)
Quick Start
The fastest way to get started is to install BSB and create a service plugin.
Create a new project
mkdir my-bsb-service
cd my-bsb-service
npm init -y
npm install @bsb/base
npm install -D typescript @types/node
npx tsc --init
@bsb/base is a runtime dependency for Node plugins. Do not put it in peerDependencies; published plugins import it when BSB loads them.
Create your service plugin
Create a plugin directory structure:
mkdir -p src/plugins/service-hello
src/plugins/service-hello/index.ts
import {
BSBService,
BSBServiceConstructor,
Observable,
bsb,
createConfigSchema,
createEventSchemas,
createReturnableEvent,
} from "@bsb/base";
import * as av from "anyvali";
const Config = createConfigSchema(
{
name: "service-hello",
description: "Hello service plugin",
tags: ["service", "example"],
documentation: ["./docs/service-hello.md"],
},
av.object({}),
);
const EventSchemas = createEventSchemas({
emitEvents: {},
onEvents: {},
emitReturnableEvents: {},
onReturnableEvents: {
"hello.greet": createReturnableEvent(
bsb.object({ name: bsb.string() }, "Greeting input"),
bsb.object({
message: bsb.string(),
timestamp: bsb.datetime(),
}, "Greeting response"),
"Greet a user by name",
),
},
emitBroadcast: {},
onBroadcast: {},
});
export class Plugin extends BSBService<InstanceType<typeof Config>, typeof EventSchemas> {
static Config = Config;
static EventSchemas = EventSchemas;
constructor(cfg: BSBServiceConstructor<InstanceType<typeof Config>>) {
super(cfg);
}
async init(obs: Observable) {
obs.log.info("Initializing {plugin}", { plugin: this.pluginName });
await this.events.onReturnableEvent("hello.greet", obs, async (handlerObs, input) => {
handlerObs.log.info("Greeting {name}", { name: input.name });
return {
message: `Hello, ${input.name}!`,
timestamp: new Date().toISOString(),
};
});
}
async run(obs: Observable) {
obs.log.info("Hello service is running");
}
async dispose() {
// Cleanup if needed
}
}
Keep lifecycle observables scoped
The obs passed to init or run belongs to that lifecycle invocation. Use it for operations called directly from that method, but do not store it as this.obs or reuse it later. Event handlers receive a handler observable from BSB. Start each independent HTTP request, queue message, scheduled job, or background iteration with this.createTrace(...), then create child spans for nested work in that flow.
Create the configuration
sec-config.yamldefault: # deployment profile
observable:
observable-default:
plugin: observable-default
enabled: true
config: {}
events:
events-default:
plugin: events-default
enabled: true
services:
service-hello:
plugin: service-hello
enabled: true
config: {}
Run your service
# Development mode (TypeScript plugins, hot reload)
npm run dev
# Or production mode (compiled JavaScript only)
npm run build
npm start
Understanding the Structure
Plugin Directory
BSB discovers plugins by scanning the src/plugins/ directory. Each plugin must:
- Be in a folder named
service-*,events-*,observable-*, orconfig-* - Export a
Pluginclass that extends the appropriate base class - Optionally export a
Configclass for plugin configuration
Event Schemas
The EventSchemas object defines your service's event contract:
emitEvents- Events you emit (fire-and-forget, first listener)onEvents- Events you listen to (fire-and-forget)emitReturnableEvents- Events you emit and expect a responseonReturnableEvents- Events you handle and return a responseemitBroadcast- Events you broadcast (all listeners)onBroadcast- Broadcasts you listen to
Each event name must appear in only one EventSchemas category. Do not declare the same key in emitEvents, onEvents, emitReturnableEvents, onReturnableEvents, emitBroadcast, or onBroadcast. If a plugin emits an event and also needs to listen to that service contract, instantiate the generated service client and register the listener through the client onX API instead of adding a matching on* entry to the same plugin.
Working with Events
Emitting Events
// Fire-and-forget (first listener receives)
await this.events.emitEvent("user.created", obs, {
userId: "123",
email: "user@example.com"
});
// Request-response (wait for result, optional timeout in seconds, default 5)
const result = await this.events.emitEventAndReturn(
"user.validate",
obs,
{ email: "user@example.com" },
5
);
// Broadcast (all listeners receive)
await this.events.emitBroadcast("cache.invalidate", obs, {
keys: ["user:123"]
});
Handling Events
// Handle fire-and-forget
await this.events.onEvent("user.created", obs, async (handlerObs, input) => {
handlerObs.log.info("User created: {userId}", { userId: input.userId });
});
// Handle request-response
await this.events.onReturnableEvent("user.validate", obs, async (handlerObs, input) => {
const isValid = await validateEmail(input.email);
return { valid: isValid };
});
// Handle broadcast
await this.events.onBroadcast("cache.invalidate", obs, async (handlerObs, input) => {
for (const key of input.keys) {
await cache.delete(key);
}
});
Adding Configuration
To add configuration to your plugin, create a config schema with AnyVali. Unknown object keys are stripped by default, so omit the redundant { unknownKeys: "strip" } option.
import * as av from "anyvali";
import { createConfigSchema } from "@bsb/base";
const Config = createConfigSchema(
{
name: "service-hello",
description: "Hello service",
tags: ["hello", "example"]
},
av.object({
greeting: av.string().default("Hello"),
maxNameLength: av.int32().default(100)
})
);
export class Plugin extends BSBService<InstanceType<typeof Config>, typeof EventSchemas> {
static Config = Config;
static EventSchemas = EventSchemas;
// Now this.config is typed with your schema
async init(obs: Observable) {
const greeting = this.config.greeting; // "Hello"
const max = this.config.maxNameLength; // 100
}
}
Then in your sec-config.yaml:
default:
services:
service-hello:
plugin: service-hello
enabled: true
config:
greeting: "Welcome"
maxNameLength: 50
Built-In Runtime Plugins
For local development and simple single-container services, BSB includes defaults from @bsb/base:
config-default: readssec-config.yaml.config-env: reads the full runtime config fromBSB_CONFIG_JSON.events-default: in-process event bus for one container/process.observable-default: console logging plus basic tracing/metrics hooks.
Replace these in production when you need remote config, distributed events, or external observability. Find replacement plugins in the Plugin Registry.