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

Mongoose

The tasks API on MongoDB with Mongoose: connection, schema, controller and module.

This page finishes the Build a project guide with Mongoose and MongoDB. It keeps ids as UUID strings, so a task id never has to be converted to an ObjectId.

Install

Terminal
bun add mongoose

Set DATABASE_URL=mongodb://localhost:27017/riserbob_tasks in .env, or use a MongoDB Atlas connection string.

Connect to the database

A small class connects once and fails loudly when the URL is missing, so a misconfigured app never starts half-working.

src/config/database/connect-db.ts
import mongoose from "mongoose"; export class ConnectDB {  private static isConnected = false;   static async connect(): Promise<void> {    if (this.isConnected) return;     const databaseUrl = process.env.DATABASE_URL;     if (!databaseUrl) {      throw new Error("DATABASE_URL is missing from environment variables");    }     await mongoose.connect(databaseUrl);    this.isConnected = true;     console.log("🍍 Connected to the database");  }   static async disconnect(): Promise<void> {    if (!this.isConnected) return;     await mongoose.disconnect();    this.isConnected = false;  }}

A plugin for UUID ids

src/utils/plugins/uuid.plugin.ts
import { randomUUID } from "node:crypto";import type { Schema } from "mongoose"; export function uuidPlugin(schema: Schema) {  schema.add({    _id: {      type: String,      default: randomUUID,    },  });}

The schema

src/modules/tasks/schemas/task.schema.ts
import mongoose, { Schema } from "mongoose";import { uuidPlugin } from "../../../utils/plugins/uuid.plugin"; export interface ITask {  _id: string;  title: string;  content?: string | null;  isCompleted: boolean;} export class TaskSchema {  public static readonly schema = new Schema<ITask>(    {      title: {        type: String,        required: true,        minLength: 3,        maxLength: 50,      },      content: {        type: String,        default: null,      },      isCompleted: {        type: Boolean,        default: false,      },    },    { timestamps: true },  );   static {    this.schema.plugin(uuidPlugin);  }   public static readonly model = mongoose.model<ITask>(    "Task",    TaskSchema.schema,  );}

The controller

The controller receives the Mongoose model through its constructor. Each handler validates, talks to the model, and always returns a Response.

src/modules/tasks/tasks.controller.ts
import type { Model } from "mongoose";import { Validator } from "riser-bob";import type { SpongHandler } from "riser-bob";import { buildPagination, getPagination } from "../../utils/pagination";import { CreateTaskRule } from "./rules/create.rule";import { UpdateTaskRule } from "./rules/update.rule";import type { ITask } from "./schemas/task.schema"; type TaskInput = {  title?: string;  content?: string | null;  isCompleted?: boolean;}; export class TasksController {  constructor(private readonly taskModel: Model<ITask>) {}   readonly create: SpongHandler = async (spongReq, spongRes) => {    const body = await spongReq.json<Record<string, unknown>>();    const result = Validator.validate<TaskInput>(body, CreateTaskRule);     if (!result.valid) {      return spongRes.SpongUnprocessableContent({        success: false,        message: "Validation failed",        errors: result.errors,      });    }     const task = await this.taskModel.create({      title: result.data.title,      content: result.data.content ?? null,      isCompleted: result.data.isCompleted ?? false,    });     return spongRes.SpongCreated({ success: true, data: task });  };   readonly findAll: SpongHandler = async (spongReq, spongRes) => {    const { page, limit, skip } = getPagination(spongReq);     const [tasks, total] = await Promise.all([      this.taskModel.find().sort({ createdAt: -1 }).skip(skip).limit(limit),      this.taskModel.countDocuments(),    ]);     return spongRes.SpongOK({      success: true,      data: tasks,      pagination: buildPagination(total, page, limit),    });  };   readonly findOne: SpongHandler = async (spongReq, spongRes) => {    const id = spongReq.params.id;     if (!id) {      return spongRes.SpongBadRequest({        success: false,        message: "Task ID is required",      });    }     const task = await this.taskModel.findById(id);     if (!task) {      return spongRes.SpongNotFound({        success: false,        message: "Task not found",      });    }     return spongRes.SpongOK({ success: true, data: task });  };   readonly update: SpongHandler = async (spongReq, spongRes) => {    const id = spongReq.params.id;     if (!id) {      return spongRes.SpongBadRequest({        success: false,        message: "Task ID is required",      });    }     const body = await spongReq.json<Record<string, unknown>>();    const result = Validator.validate<TaskInput>(body, UpdateTaskRule);     if (!result.valid) {      return spongRes.SpongUnprocessableContent({        success: false,        message: "Validation failed",        errors: result.errors,      });    }     // Pick the fields explicitly. Never pass the whole body to the database.    const changes: TaskInput = {};    if (result.data.title !== undefined) changes.title = result.data.title;    if (result.data.content !== undefined) changes.content = result.data.content;    if (result.data.isCompleted !== undefined) {      changes.isCompleted = result.data.isCompleted;    }     if (Object.keys(changes).length === 0) {      return spongRes.SpongBadRequest({        success: false,        message: "No valid fields provided for update",      });    }     const task = await this.taskModel.findByIdAndUpdate(      id,      { $set: changes },      { new: true, runValidators: true },    );     if (!task) {      return spongRes.SpongNotFound({        success: false,        message: "Task not found",      });    }     return spongRes.SpongOK({ success: true, data: task });  };   readonly delete: SpongHandler = async (spongReq, spongRes) => {    const id = spongReq.params.id;     if (!id) {      return spongRes.SpongBadRequest({        success: false,        message: "Task ID is required",      });    }     const task = await this.taskModel.findByIdAndDelete(id);     if (!task) {      return spongRes.SpongNotFound({        success: false,        message: "Task not found",      });    }     return spongRes.SpongOK({ success: true, message: "Task deleted" });  };}

The module

src/modules/tasks/tasks.module.ts
import { RiserBob } from "riser-bob";import { TaskSchema } from "./schemas/task.schema";import { TasksController } from "./tasks.controller";import { TasksRouter } from "./tasks.route"; export class TasksModule {  register(bob: RiserBob): void {    const controller = new TasksController(TaskSchema.model);     new TasksRouter(bob, controller).register();  }}

The entry file

Connect before you register modules and start listening. If the database is unreachable the app exits with a clear error instead of serving requests that cannot work.

src/index.ts
import { RiserBob, corsMiddleware, loggerMiddleware } from "riser-bob";import { ConnectDB } from "./config/database/connect-db";import { TasksModule } from "./modules/tasks/tasks.module"; async function bootstrap() {  await ConnectDB.connect();   const bob = new RiserBob();   bob.spongUse(    corsMiddleware({      origin: "http://localhost:5173",      credentials: true,    }),  );  bob.spongUse(loggerMiddleware);   bob.spongGet("/", (spongReq, spongRes) => {    return spongRes.send("Welcome to RiserBob APIs");  });   new TasksModule().register(bob);   bob.spongListen(4000, () => {    console.log("🧽 RiserBob is running at http://localhost:4000 🍍");  });} bootstrap().catch((error: unknown) => {  console.error("[RiserBob] Failed to start application:", error);  process.exit(1);});
Mongoose throws a cast error for an id it cannot convert. Because these ids are plain strings, any value is accepted and a missing task simply answers 404. If you switch to ObjectId ids, check the format first and answer 400.
PreviousBuild a projectNextDrizzle ORM

On this page

  • Install
  • Connect to the database
  • A plugin for UUID ids
  • The schema
  • The controller
  • The module
  • The entry file