Development Guide

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 in appDir/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 nameEvent name
userCreatedEmail.event.tsuser.created.email
orderShipped.event.tsorder.shipped
invoicePaid.event.tsinvoice.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:

  1. payload — The value you passed to ctx.emit(name, payload) (or emit(name, payload)).
  2. ctx — An EventContext with:
    • 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 with eventName in 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.

router/api/users/post.route.ts
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 for user.created.email (e.g. from appDir/events/userCreatedEmail.event.ts). The handler receives user as payload and an EventContext with the request's traceId and 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:

router/api/users/post.route.ts
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):

appDir/events/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 traceId when 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.

  • userCreatedEmailuser.created.email
  • orderShippedorder.shipped
  • HTTPRequesth.t.t.p.request (edge case; prefer httpRequesthttp.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

TopicDetail
LocationOnly appDir/events/. No subfolders.
File names*.event.ts (or .js, .mts, .mjs).
Event nameFrom filename: camelCase → dot.case (e.g. userCreatedEmailuser.created.email).
ExportDefault function (payload, ctx: EventContext) => void | Promise<void>.
EmitFrom routes: ctx.emit("event.name", payload). Context gets request traceId and logger.
EventContexttraceId, logger, eventName.

For emitting from routes, see Routes. For triggering logic on a schedule, see Cron jobs.