Skip to content

Search docs

Jump between documentation pages.

Browse docs

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.

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.

Where the ODM connection lives
Route handlersRead models from state, never connect directly
state.db.Userstate.db.Order
Database pluginapp.decorate('db', db) plus app.onClose() teardown
databasePlugin(app)onClose
ODM or driver clientSchemas, validation, query helpers
MongooseMongoDB driverOttoman
Document databaseCollections, buckets, scopes
MongoDBCouchbase
Connections are opened once in a setup function that decorates the root app, and exposed on state. Handlers read models from state.db, so the connection lifecycle stays in one place and closes cleanly on shutdown.
src/db/plugin.tsts
import type { App } from "@daloyjs/core";
import mongoose from "mongoose";
import { db } from "./mongoose.ts"; // { connection, User, Order, Inventory }

// Call with the root app, not through app.register(): a decoration made
// inside a registered plugin is visible only to that plugin's own routes.
export async function databasePlugin(app: App) {
  await mongoose.connect(process.env.MONGODB_URI!);
  app.decorate("db", db);
  app.onClose(async () => {
    await mongoose.disconnect();
  });
}
src/server.tsts
import { App } from "@daloyjs/core";
import { databasePlugin } from "./db/plugin.ts";

const app = new App();
await databasePlugin(app);

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

ODMNode.jsBunDenoVercel (Node)AWS LambdaAzure FunctionsCloudflare Workers
Mongoose 9YesNot verifiedAlpha (per the Mongoose README)Not verifiedNot verifiedNot verifiedNo
MongoDB driver 7YesNot verifiedNot verifiedNot verifiedNot verifiedNot verifiedNot verified
Ottoman 2YesNot verifiedNot verifiedNot verifiedNot verifiedNot verifiedNo

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.

ts
// src/db/mongoose.ts (the module that exports db)
declare module "@daloyjs/core" {
  interface AppState {
    db: typeof db;
  }
}

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.

ts
handler: async ({ body, state }) => {
  const session = await state.db.connection.startSession();
  try {
    let orderId = "";
    await session.withTransaction(async () => {
      const [order] = await state.db.Order.create([{ sku: body.sku, qty: body.qty }], { session });
      orderId = String(order._id);
      await state.db.Inventory.updateOne(
        { sku: body.sku },
        { $inc: { stock: -body.qty } },
        { session },
      );
    });

    return { status: 201, body: { id: orderId, sku: body.sku, qty: body.qty } };
  } finally {
    await session.endSession();
  }
}

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).

ts
import mongoose from "mongoose";

app.readinesscheck({
  token: process.env.HEALTH_TOKEN!,
  checks: {
    db: async (signal) => {
      // connection.db is the underlying driver Db; signal support is experimental there.
      await mongoose.connection.db!.command({ ping: 1 }, { signal });
    },
  },
  checkTimeoutMs: 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.

Next steps

Edit this page on GitHub

Last updated