Using ODMs with DaloyJS
DaloyJS works with document databases the same way it works with SQL databases, but the abstractions are different. Use an ODM when your persistence layer is document-shaped and you want schemas, validation, middleware, and query helpers around collections or buckets.
ORM vs ODM
- ORM maps relational tables and joins into TypeScript objects. Use it for PostgreSQL, MySQL, SQLite, MariaDB, or MSSQL.
- ODM maps JSON-like documents and collection workflows into TypeScript objects. Use it for document databases such as MongoDB or Couchbase.
The recommended pattern
Like SQL clients, ODM connections belong in a setup function you call once with the root app (not through app.register(), which would scope the decoration to that plugin's own routes). Decorate the root app with a small database surface and close the connection on shutdown.
onClose() only runs on the Node.js, Bun, and Deno adapters. On Lambda, Vercel, and Azure Functions, connect at module scope and keep the pool small (Mongoose's maxPoolSize); see Serverless and edge lifecycle for the full picture.
Pick your ODM
- Mongoose: mature schemas, middleware, validation, and session support for MongoDB.
- MongoDB Node.js driver: the official driver with no model layer; Zod schemas, typed collections, and transactions.
- Ottoman: schema and model layer for Couchbase buckets, scopes, and collections.
Runtime compatibility cheat sheet
| ODM | Node.js | Bun | Deno | Vercel (Node) | AWS Lambda | Azure Functions | Cloudflare Workers |
|---|---|---|---|---|---|---|---|
| Mongoose 9 | Yes | Not verified | Alpha (per the Mongoose README) | Not verified | Not verified | Not verified | No |
| MongoDB driver 7 | Yes | Not verified | Not verified | Not verified | Not verified | Not verified | Not verified |
| Ottoman 2 | Yes | Not verified | Not verified | Not verified | Not verified | Not verified | No |
Vercel Functions (Node.js runtime), AWS Lambda, and Azure Functions run on Node.js, so these libraries are expected to load there, but we have not verified them end to end and connection reuse is your job (see the lifecycle notes). Mongoose, the MongoDB driver, and Ottoman all depend on Node.js database drivers that open long-lived TCP connections, so they are primarily Node.js choices. If you need a portable edge-friendly database layer, stay in the SQL-oriented ORM section and choose a compatible client there.
Typing the decorated client
Declare the augmentation in the module that exports the models (a regular .ts file the compiler always checks), not in 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 degrades to any.
Sessions and transactions
MongoDB transactions require a replica set and a session. Start the session inside the handler that owns the unit of work, then pass it through each model operation. This example assumes Order and Inventory models exported on db and a route that declares 201.
Pick fields explicitly when you write. Spreading { ...body } into create() is safe only because z.object() strips unknown keys before the handler runs; a z.looseObject() or .passthrough() write schema would let a client set any field on the document (mass assignment).
Readiness that pings the database
Pass checks to app.readinesscheck() so the orchestrator pulls an instance that cannot reach MongoDB. Each check receives an AbortSignal, runs concurrently with the others, and fails the probe (503) if it throws, rejects, returns false, or exceeds checkTimeoutMs (default 2000).
The probe body stays { status: "not-ready" }; the failing check and its error are logged server-side, not returned. Keep liveness on app.healthcheck(), which never touches the database. See lifecycle and health.