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.
1
Create a new project
mkdir my-bsb-service
cd my-bsb-service
npm init -y
npm install @bettercorp/service-base typescript @types/node
npx tsc --init
2
Create your service plugin
Create a plugin directory structure:
mkdir -p src/plugins/service-hello
src/plugins/service-hello/index.ts
import { BSBService, BSBServiceConstructor } from "@bettercorp/service-base";
import { z } from "zod";
import { createReturnableEvent, createFireAndForgetEvent } from "@bettercorp/service-base";
// Define your event schemas
export const EventSchemas = {
emitEvents: {},
onEvents: {},
emitReturnableEvents: {},
onReturnableEvents: {
'hello.greet': createReturnableEvent(
z.object({
name: z.string()
}),
z.object({
message: z.string(),
timestamp: z.string()
}),
'Greet a user by name'
)
},
emitBroadcast: {},
onBroadcast: {}
} as const;
export class Plugin extends BSBService<null, typeof EventSchemas> {
public initBeforePlugins?: string[];
public initAfterPlugins?: string[];
public runBeforePlugins?: string[];
public runAfterPlugins?: string[];
constructor(config: BSBServiceConstructor<null, typeof EventSchemas>) {
super({
...config,
eventSchemas: EventSchemas
});
}
public async init() {
// Register event handlers
await this.events.onReturnableEvent("hello.greet", async (trace, input) => {
this.log.info(trace, "Greeting {name}", { name: input.name });
return {
message: `Hello, ${input.name}!`,
timestamp: new Date().toISOString()
};
});
}
public async run() {
this.log.info(this.metrics.createTrace("run").trace, "Hello service is running");
}
public dispose() {
// Cleanup if needed
}
}
3
Create the configuration
sec-config.yamlplugins:
service-hello:
enabled: true
4
Run your service
# Development mode (with hot reload)
npx ts-node src/dev.ts
# Or production mode
npm run build
node lib/cli.js
Understanding the Structure
Plugin Directory
BSB discovers plugins by scanning the src/plugins/ directory. Each plugin must:
- Be in a folder named
service-*,events-*,logging-*,config-*, ormetrics-* - 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
Working with Events
Emitting Events
// Fire-and-forget (first listener receives)
await this.events.emitEvent("user.created", trace, {
userId: "123",
email: "user@example.com"
});
// Request-response (wait for result)
const result = await this.events.emitEventAndReturn(
"user.validate",
trace,
{ email: "user@example.com" },
5 // timeout in seconds
);
// Broadcast (all listeners receive)
await this.events.emitBroadcast("cache.invalidate", trace, {
keys: ["user:123"]
});
Handling Events
// Handle fire-and-forget
await this.events.onEvent("user.created", trace, async (t, input) => {
this.log.info(t, "User created: {userId}", { userId: input.userId });
});
// Handle request-response
await this.events.onReturnableEvent("user.validate", trace, async (t, input) => {
const isValid = await validateEmail(input.email);
return { valid: isValid };
});
// Handle broadcast
await this.events.onBroadcast("cache.invalidate", trace, async (t, input) => {
for (const key of input.keys) {
await cache.delete(key);
}
});
Adding Configuration
To add configuration to your plugin, create a Config class:
import { BSBPluginConfig } from "@bettercorp/service-base";
import { z } from "zod";
const configSchema = z.object({
greeting: z.string().default("Hello"),
maxNameLength: z.number().default(100)
});
export class Config extends BSBPluginConfig<typeof configSchema> {
validationSchema = configSchema;
}
export class Plugin extends BSBService<Config, typeof EventSchemas> {
// Now this.config is typed with your schema
async init() {
const greeting = this.config.greeting; // "Hello"
const max = this.config.maxNameLength; // 100
}
}
Then in your sec-config.yaml:
plugins:
service-hello:
enabled: true
greeting: "Welcome"
maxNameLength: 50