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
Examples

Build a project

Build a complete tasks API step by step, then pick Mongoose, Drizzle or Sequelize for the data layer.

This guide builds a small tasks API from an empty starter. The routes, the rules and the entry file are the same whatever database you use. Only the data layer changes, and it has its own page for each library.

  • Mongoose with MongoDB.
  • Drizzle ORM with PostgreSQL.
  • Sequelize with PostgreSQL.

After that, add JWT and cookies for login and roles, and File uploads for images. If something goes wrong, check Troubleshooting.

1. Create the project

Terminal
bun create riser-bob my-appcd my-appbun installbun run riser-bob g module tasks

The generator writes tasks.controller.ts, tasks.route.ts and tasks.module.ts into src/modules/tasks/. You will replace all three. The starter's users module can stay as a reference or be deleted.

2. Environment

Create a .env file in the project root. Bun loads it automatically. Use your own values:

.env
# MongoDB (Mongoose example)DATABASE_URL=mongodb://localhost:27017/riserbob_tasks # PostgreSQL (Drizzle and Sequelize examples)# DATABASE_URL=postgres://postgres:postgres@localhost:5432/riserbob_tasks # Authentication exampleJWT_SECRET=change-me-to-a-long-random-stringJWT_REFRESH_SECRET=change-me-to-another-long-random-string # File uploads exampleCLOUDINARY_CLOUD_NAME=your-cloud-nameCLOUDINARY_API_KEY=your-api-keyCLOUDINARY_API_SECRET=your-api-secret
These are placeholders. Generate long random secrets for the JWT variables, and never publish real keys in a repository or in a screenshot. If a key was exposed, rotate it.

3. Shared helpers

Two tiny helpers keep every list endpoint and every id check the same. They have no dependency on a database.

src/utils/pagination.ts
import type { SpongRequest } from "riser-bob"; export function getPagination(spongReq: SpongRequest) {  const page = Math.max(1, Number(spongReq.query.get("page")) || 1);  const limit = Math.min(    100,    Math.max(1, Number(spongReq.query.get("limit")) || 10),  );   return { page, limit, skip: (page - 1) * limit };} export function buildPagination(total: number, page: number, limit: number) {  return {    total,    page,    limit,    totalPages: Math.ceil(total / limit),    hasNextPage: page * limit < total,    hasPreviousPage: page > 1,  };}
src/utils/is-uuid.ts
const UUID_PATTERN =  /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; export const isUuid = (value: string): boolean => UUID_PATTERN.test(value);

4. Validation rules

src/modules/tasks/rules/create.rule.ts
import type { ValidationSchema } from "riser-bob"; export const CreateTaskRule = {  title: ["required", "string", "minLength:3", "maxLength:50"],  content: ["nullable", "string", "minLength:3"],  isCompleted: ["boolean"],} satisfies ValidationSchema;
src/modules/tasks/rules/update.rule.ts
import type { ValidationSchema } from "riser-bob"; // Nothing is required on update: the client sends only what changes.export const UpdateTaskRule = {  title: ["string", "minLength:3", "maxLength:50"],  content: ["nullable", "string", "minLength:3"],  isCompleted: ["boolean"],} satisfies ValidationSchema;

5. The router

Every controller in the three data-layer pages exposes the same five handlers, so this router works with all of them. Authentication is left out on purpose and added later in JWT and cookies.

src/modules/tasks/tasks.route.ts
import { RiserBob } from "riser-bob";import type { TasksController } from "./tasks.controller"; export class TasksRouter {  constructor(    private readonly bob: RiserBob,    private readonly controller: TasksController,  ) {}   register(): void {    this.bob.spongPost("/api/tasks", this.controller.create);    this.bob.spongGet("/api/tasks", this.controller.findAll);    this.bob.spongGet("/api/tasks/:id", this.controller.findOne);    this.bob.spongPut("/api/tasks/:id", this.controller.update);    this.bob.spongDelete("/api/tasks/:id", this.controller.delete);  }}

6. The data layer

Now write the controller, the schema and the module for your database. Pick one: Mongoose, Drizzle ORM or Sequelize. Each page ends with the changes to make in src/index.ts.

7. Try it

Terminal
bun run dev
Terminal
# createcurl -X POST http://localhost:4000/api/tasks \  -H "Content-Type: application/json" \  -d '{"title":"Learn RiserBob","content":"Read the docs"}' # list, with paginationcurl "http://localhost:4000/api/tasks?page=1&limit=10" # update one fieldcurl -X PUT http://localhost:4000/api/tasks/<id> \  -H "Content-Type: application/json" \  -d '{"isCompleted":true}' # deletecurl -X DELETE http://localhost:4000/api/tasks/<id>
Output
{  "success": true,  "data": [    { "id": "…", "title": "Learn RiserBob", "isCompleted": false }  ],  "pagination": {    "total": 1,    "page": 1,    "limit": 10,    "totalPages": 1,    "hasNextPage": false,    "hasPreviousPage": false  }}
The id field is called _id in the Mongoose version and id in the Drizzle and Sequelize versions. Everything else in the answer is the same.
PreviousCLINextMongoose

On this page

  • 1. Create the project
  • 2. Environment
  • 3. Shared helpers
  • 4. Validation rules
  • 5. The router
  • 6. The data layer
  • 7. Try it