rhythmjs

Search documentation

Search guides, the tutorial and every package.

On this page

Register a child#

A child receives the parent context as its input. Its added top-level fields stay inside the child unless an export function returns them. The parent context must satisfy the child’s input type.

TypeScript
import { Rhythm } from "@rhythmjs/rhythm";

const identity = new Rhythm<{ userId: string }, { source: string }>({ name: "identity" })
  .use<{ user: { name: string } }>(async (_ctx, next) => {
    await next({ user: { name: "Ada" } });
  });

const app = new Rhythm<{ userId: string }>()
  .register(identity, (result) => ({ user: result.user }))
  .use((ctx) => console.log(ctx.user.name));

identity.context.source = "local";

await app.run({ userId: "u1" });

Export deliberately#

Without an export function, no child-added fields are merged back into the parent. Exported fields become part of the parent’s inferred context. The boundary is shallow: nested input objects remain shared references, so registration is not a deep clone.

Run in place#

A registered module is part of the request's chain, not a separate pass. If the module's chain calls next() past its last middleware, the parent carries on with whatever it registered next, and the exports are applied at that moment. If the chain ends without calling next(), the request ends there: the parent's later middleware does not run. That is how a module that mounts a controller answers its own requests, while a catch-all 404 registered after it only sees what no module handled.

TypeScript
const productsModule = new Rhythm<RhythmHttpContext, { productsService: ProductsService }>({
  name: "products",
  type: "module",
}).use(productsController.middleware());

productsModule.context.productsService = createProductsService(); // answers /api/products/** and does not call next()

const app = new Rhythm<RhythmHttpContext>()
  .register(productsModule)
  .use(() => {
    throw new HttpError(404, "Route not found"); // runs only for requests no module answered
  });

Inherit context, wrap errors#

A registered module inherits its parent's context, including the parent's startup values, and may add its own through module.context; those never reach the parent (see Startup context). Errors thrown by a registered child during dispatch are wrapped with its type and name; the original error is available as cause. Errors thrown by the parent's later middleware are not wrapped: they are not the module's failure. Because the wrapped error no longer carries the status of an HttpError, give a module that throws HttpErrors its own filter() at the top of its chain.

Mount controllers flat#

RhythmRouter and RhythmCli are standalone controllers, not Rhythm modules: they have no startup context or register(), and they cannot be passed to register() either; it composes Rhythm instances only. Mount them into a core module with use(router.middleware()) or use(cli.middleware()), and nest controller into controller the same way. The mount is opaque, so a nested controller carries its own full prefix.

Inspect the app#

Middleware is opaque, so tooling cannot ask a function what it is. Instead, a middleware can be tagged with the object it was built from using withSource(fn, source) from @rhythmjs/rhythm/source. RhythmRouter#middleware(), RhythmCli#middleware() and Rhythm#middleware() tag themselves; anything else, such as a future extension, can do the same.

use() records the tag, and register() records the child module. module.sources then lists the tagged sources below a module in order, with each registered module replaced by its own sources, and module.parent points back up. A module reads these lazily, when it first needs them, so controllers mounted after it are still found.

TypeScript
const usersModule = new Rhythm<RhythmHttpContext>({ name: "users" }).use(usersController.middleware());

const appModule = new Rhythm<RhythmHttpContext>({ name: "app" })
  .register(usersModule)
  .use(healthController.middleware());

appModule.sources; // [usersController, healthController]
usersModule.sources; // [usersController]
usersModule.parent; // appModule

A module sees only its own scope: a module registered in apiModule inspects apiModule and below, never its siblings. This is how openapiModule documents one group of routes, or the whole app, depending on where it is registered. The OpenAPI page shows it in use.