Events
Event handlers in app/events, file naming, event name from filename, emitting from routes, and EventContext.
Events
Legonode's event bus lets you run logic after something happens (e.g. “user created”) without blocking the request. Handlers are registered from files in appDir/events/ and invoked when you call ctx.emit(name, payload) from a route (or elsewhere). This guide covers file naming, how event names are derived, and how to emit and handle events.
Where events live
- Directory: Only
appDir/events/is scanned for event handlers. No subfolders are scanned; all handler files sit directly inappDir/events/. - File names:
*.event.ts,*.event.js,*.event.mts,*.event.mjs
Each file defines one handler. The event name is derived from the filename (before the .event.* suffix) by converting camelCase to dot.case:
| File name | Event name |
|---|---|
userCreatedEmail.event.ts | user.created.email |
orderShipped.event.ts | order.shipped |
invoicePaid.event.ts | invoice.paid |
What to export
Each event file must export a default function that will be registered as the handler for that event name. The handler receives:
payload— The value you passed toctx.emit(name, payload)(oremit(name, payload)).ctx— AnEventContextwith:traceId— The request trace id when the event was emitted from a route; empty when emitted outside a request (e.g. from a cron).logger— Logger witheventNamein bindings so logs from the handler correlate with the event (and request, when applicable).eventName— The event name (e.g."user.created.email").
Handlers can be sync or async. They are invoked with void handler(payload, ctx) (fire-and-forget); the framework does not await them, so long-running work should be scheduled or queued if you need reliability.
Emitting events
From a route, use ctx.emit(name, payload). The framework passes the current request's trace id and logger into the event context so you can correlate logs.
import type { Context, RouteSchema } from "legonode";
export const SCHEMA: RouteSchema = {
body: { name: "string", email: "string" },
};
export default async function POST(ctx: Context) {
const body = ctx.body as { name: string; email: string };
const user = { id: "1", ...body };
ctx.emit("user.created.email", user);
ctx.res.status(201).json(user);
}ctx.emit("user.created.email", user)— Invokes every handler registered foruser.created.email(e.g. fromappDir/events/userCreatedEmail.event.ts). The handler receivesuseraspayloadand anEventContextwith the request'straceIdand a logger.
You can also get the event bus from the runtime and call emit outside a request (e.g. from a cron job); in that case traceId will be empty and the logger will still include eventName.
Full example: welcome email on user create
Route — creates a user and emits an event:
import type { Context, RouteSchema } from "legonode";
export const SCHEMA: RouteSchema = {
body: { name: "string", email: "string" },
};
export default async function POST(ctx: Context) {
const body = ctx.body as { name: string; email: string };
const user = { id: "1", ...body };
ctx.emit("user.created.email", user);
ctx.res.status(201).json(user);
}Event handler — registered for user.created.email (from filename userCreatedEmail.event.ts):
import type { EventContext } from "legonode";
export default async function userCreatedEmail(
payload: { id: string; name: string; email: string },
ctx: EventContext
) {
ctx.logger.info({ userId: payload.id, event: ctx.eventName }, "sending welcome email");
// In a real app: send email, enqueue job, etc.
}- When a client POSTs to create a user, the route responds immediately with 201. The handler runs asynchronously and can log (with
traceIdwhen emitted from the request), send an email, or enqueue work. - Multiple files can register for the same event name (e.g. two handlers for
user.created.email); all are invoked when you emit.
Event name from filename
The conversion is camelCase → dot.case: each capital letter (except the first) starts a new segment, lowercased.
userCreatedEmail→user.created.emailorderShipped→order.shippedHTTPRequest→h.t.t.p.request(edge case; preferhttpRequest→http.request)
Name your files so the resulting event name matches what you emit. For example, to use ctx.emit("user.created.email", payload), the file must be named userCreatedEmail.event.ts.
Summary
| Topic | Detail |
|---|---|
| Location | Only appDir/events/. No subfolders. |
| File names | *.event.ts (or .js, .mts, .mjs). |
| Event name | From filename: camelCase → dot.case (e.g. userCreatedEmail → user.created.email). |
| Export | Default function (payload, ctx: EventContext) => void | Promise<void>. |
| Emit | From routes: ctx.emit("event.name", payload). Context gets request traceId and logger. |
| EventContext | traceId, logger, eventName. |
For emitting from routes, see Routes. For triggering logic on a schedule, see Cron jobs.