RiserBob logoRiserBobv1.0.0
HomeDocsCLIRelease notes

Getting started

  • Introduction
  • Installation
  • Quick start
  • Project structure

Core concepts

  • Routing
  • Request
  • Responses
  • Error handling

Middleware

  • Middleware
  • Authentication
  • Logger and CORS

Data

  • Validation

Structure and tooling

  • Modules
  • CLI

Examples

  • Build a project
  • Mongoose
  • Drizzle ORM
  • Sequelize
  • JWT and cookies
  • File uploads

Packages

  • Cookies

Resources

  • Troubleshooting
  • Release notes
  • License
  • Security
  • Contributing
Structure and tooling

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.

users.module.ts
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.

users.controller.ts
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.

users.route.ts
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

src/index.ts
new UsersModule().register(bob);
You do not need to write these files by hand. bun run riser-bob g module users generates all three. See CLI.

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.

tasks.module.ts
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.

PreviousValidationNextCLI

On this page

  • The module
  • The controller
  • The router
  • Register it
  • Passing dependencies