Modules
Organise each feature as a module made of three focused files.
RiserBob is modular by design. Every feature is a folder with three files: a controller for the logic, a router for the paths, and a module that wires them together. src/index.ts stays tiny, and a feature can grow without touching the others.
The module
The module creates the controller, hands its verifyToken to AuthMiddleware, builds the router and registers it.
import { RiserBob } from "riser-bob";import { AuthMiddleware } from "riser-bob";import { UsersController } from "./users.controller";import { UsersRouter } from "./users.route"; export class UsersModule { register(bob: RiserBob): void { const controller = new UsersController(); const authMiddleware = new AuthMiddleware((token) => controller.verifyToken(token), ); const router = new UsersRouter( bob, controller, authMiddleware, ); router.register(); }}The controller
Each handler does one job. Your business logic lives here, away from paths and wiring. The generated controller is a set of stubs. Every handler must return a Response.
import type { SpongHandler } from "riser-bob"; export class UsersController { readonly create: SpongHandler = async (spongReq, spongRes) => { // TODO: Implement creation logic. }; readonly findAll: SpongHandler = async (spongReq, spongRes) => { // TODO: Implement fetching all records. }; readonly findOne: SpongHandler = async (spongReq, spongRes) => { // TODO: Implement fetching a record by ID. }; readonly update: SpongHandler = async (spongReq, spongRes) => { // TODO: Implement updating a record. }; readonly delete: SpongHandler = async (spongReq, spongRes) => { // TODO: Implement deleting a record. }; verifyToken(token: string) { // TODO: Implement token verification. return null; }}The router
The router maps paths to handlers. Reads and creation are public, while updating and deleting are protected by the auth middleware.
import { RiserBob } from "riser-bob";import { AuthMiddleware } from "riser-bob";import { UsersController } from "./users.controller"; export class UsersRouter { constructor( private readonly bob: RiserBob, private readonly controller: UsersController, private readonly authMiddleware: AuthMiddleware, ) {} register(): void { // Public routes this.bob.spongPost("/api/users", this.controller.create); this.bob.spongGet("/api/users", this.controller.findAll); this.bob.spongGet("/api/users/:id", this.controller.findOne); // Protected routes this.bob.spongPut( "/api/users/:id", this.controller.update, this.authMiddleware.middleware, ); this.bob.spongDelete( "/api/users/:id", this.controller.delete, this.authMiddleware.middleware, ); }}Register it
new UsersModule().register(bob);Passing dependencies
The module is the one place that knows how everything is built, so it is also where you pass a database model or a service to the controller. The controller never creates its own connections, which keeps it easy to test and easy to swap.
export class TasksModule { register(bob: RiserBob): void { const controller = new TasksController(TaskSchema.model); new TasksRouter(bob, controller).register(); }}The Examples show this with Mongoose, Drizzle and Sequelize, and add an admin-only middleware to a module.