Building Services with Node.js
Create your first BSB service plugin. This guide covers setup, event handling, configuration, and deployment.
Requirements
- Node.js 23.0.0 or higher
- npm 11.0.0 or higher
- Docker (for container 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 typescript @types/node
npx tsc --init
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
}
}
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. BSB strips unknown config keys centrally during startup, so plugin schemas do not need { unknownKeys: "strip" }.
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