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
bun create riser-bob my-appcd my-appbun installbun run riser-bob g module tasksThe 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:
# 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-secret3. Shared helpers
Two tiny helpers keep every list endpoint and every id check the same. They have no dependency on a database.
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, };}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
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;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.
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
bun run dev# 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>{ "success": true, "data": [ { "id": "…", "title": "Learn RiserBob", "isCompleted": false } ], "pagination": { "total": 1, "page": 1, "limit": 10, "totalPages": 1, "hasNextPage": false, "hasPreviousPage": false }}