Mongoose is the default ODM choice for MongoDB teams who want schemas, model middleware, casting, validation, and transactions through sessions. It fits naturally into DaloyJS when you register the connection once and expose a small model surface on state.
Mongoose setup
01Installpnpm add mongoose
02Schema & modelnew Schema · model('User')
03Pluginconnect · decorate('db') · onClose
04Augment stateinterface AppState { db }
05Use in routesstate.db.User.findById()
The connection happens once inside the plugin. After you augment AppState, handlers get a fully typed state.db model surface.
// src/db/plugin.tsimport type { App } from "@daloyjs/core";import { connection, db } from "./mongoose.ts";export const mongoosePlugin = { name: "mongoose", async register(app: App) { await connection.connect(process.env.MONGODB_URI!); app.decorate("db", db); app.onClose(async () => { await connection.disconnect(); }); },};
4. Augment app state types
Add the declare module block to the same module that exports db, not to a separate .d.ts file. Declaration files are exempt from type-checking when skipLibCheck is on (the scaffolded default), so a broken import inside a .d.ts fails silently and state.db quietly degrades to any.
ts
// src/db/mongoose.ts (the module that exports db)declare module "@daloyjs/core" { interface AppState { db: typeof db; }}
Use MongoDB sessions for multi-document transactions. Start the session inside the handler and thread it through every model call in the unit of work.
Session-scoped transaction
HandlerSessionModels
01requestHandlerSessionStart a sessionstartSession()
02requestHandlerModelsRun every write inside withTransactionUser.create([...], { session })
03responseSessionHandlerCommit on success, roll back on throw
04noteHandlerSessionAlways end the session in finallyendSession()
The session is opened once, threaded through every model call, and ended in a finally block so it closes whether the transaction commits or rolls back.
Keep transport validation in Zod and let Mongoose own document-level validation. Translate duplicate key or cast failures into DaloyJS errors so they serialize as problem+json.
ts
import { HttpError } from "@daloyjs/core";try { const created = await state.db.User.create(body); return { status: 201, body: created.toObject() };} catch (err) { if (typeof err === "object" && err && "code" in err && err.code === 11000) { throw new HttpError(409, { title: "Email already in use" }); } throw err;}
Runtime constraints
Mongoose is a Node.js-first ODM because it depends on the MongoDB Node driver. For SQL databases or edge runtimes, stay in the ORM section instead.