Search docs

Jump between documentation pages.

Use Ottoman with DaloyJS

Ottoman is an ODM for Couchbase. Use it when your application stores JSON documents in Couchbase buckets, scopes, and collections and you want model definitions, validation, query helpers, and indexes around that document layer.

1. Install

ts
pnpm add ottoman couchbase

2. Define a schema and model

ts
// src/db/ottoman.ts
import { Ottoman, Schema, model } from "ottoman";

export const ottoman = new Ottoman({
  collectionName: "users",
});

const userSchema = new Schema({
  email: { type: String, required: true },
  name: { type: String, required: false },
});

userSchema.index.findByEmail = {
  by: "email",
  type: "n1ql",
};

export const User = model("User", userSchema);
export const db = { ottoman, User };

3. Create an Ottoman plugin

Connect once during app startup, build indexes with ottoman.start(), decorate the app with the models you want handlers to use, and close the connection on shutdown.

ts
// src/db/plugin.ts
import type { App } from "@daloyjs/core";
import { db, ottoman } from "./ottoman";

export const ottomanPlugin = {
  name: "ottoman",
  async register(app: App) {
    await ottoman.connect({
      connectionString: process.env.COUCHBASE_CONNECTION_STRING!,
      bucketName: process.env.COUCHBASE_BUCKET!,
      username: process.env.COUCHBASE_USERNAME!,
      password: process.env.COUCHBASE_PASSWORD!,
    });

    await ottoman.start();
    app.decorate("db", db);

    app.onClose(async () => {
      await ottoman.close();
    });
  },
};

4. Augment app state types

ts
// src/types/state.d.ts
import type { db } from "../db/ottoman";

declare module "@daloyjs/core" {
  interface AppState {
    db: typeof db;
  }
}

5. Use it in routes

ts
// src/server.ts
import { z } from "zod";
import { App, HttpError } from "@daloyjs/core";
import { serve } from "@daloyjs/core/node";
import { ottomanPlugin } from "./db/plugin";

const UserSchema = z.object({
  id: z.string(),
  email: z.string().email(),
  name: z.string().nullable(),
});

const app = new App();
app.register(ottomanPlugin);

app.route({
  method: "GET",
  path: "/users/:id",
  operationId: "getUser",
  request: { params: z.object({ id: z.string() }) },
  responses: {
    200: { description: "Found", body: UserSchema },
    404: { description: "Not found" },
  },
  handler: async ({ params, state }) => {
    const user = await state.db.User.findById(params.id);
    if (!user) {
      throw new HttpError(404, { title: "User not found" });
    }

    return {
      status: 200,
      body: {
        id: user.id,
        email: user.email,
        name: user.name ?? null,
      },
    };
  },
});

await app.ready();
serve(app, { port: 3000 });

Indexes and queries

Define indexes next to the schema and run ottoman.start() during startup so query helpers are ready before the server accepts traffic. Keep index creation out of request handlers.

ts
app.route({
  method: "GET",
  path: "/users/by-email/:email",
  operationId: "getUserByEmail",
  request: { params: z.object({ email: z.string().email() }) },
  responses: { 200: { description: "Found", body: UserSchema } },
  handler: async ({ params, state }) => {
    const user = await state.db.User.findOne({ email: params.email });
    if (!user) {
      throw new HttpError(404, { title: "User not found" });
    }
    return { status: 200, body: { id: user.id, email: user.email, name: user.name ?? null } };
  },
});

Transactions

Ottoman is best for model-centric Couchbase document access. If a workflow requires Couchbase distributed transactions, expose the Couchbase SDK objects you need through the same plugin and keep that transaction boundary inside the handler that owns the unit of work.

Runtime constraints

Ottoman depends on the Couchbase Node.js SDK, so it is a Node.js-first ODM. It is not a fit for Cloudflare Workers or Vercel Edge. For edge-compatible data access, use the SQL ORM overview and choose a compatible client.

Compare with Mongoose for MongoDB, Prisma for SQL, or return to the ODM overview.