# Use Mongoose with DaloyJS

[Mongoose](https://mongoosejs.com) 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`.

**Diagram: Mongoose setup**

1. **Install** - pnpm add mongoose
2. **Schema & model** - new Schema · model('User')
3. **Plugin** - connect · decorate('db') · onClose
4. **Augment state** - interface AppState { db }
5. **Use in routes** - state.db.User.findById()

The connection happens once inside the plugin. After you augment AppState, handlers get a fully typed state.db model surface.

## 1. Install

```ts
pnpm add mongoose
```

## 2. Define a schema and model

```ts
// src/db/mongoose.ts
import mongoose, { InferSchemaType, model, Schema } from "mongoose";

const userSchema = new Schema(
  {
    email: { type: String, required: true, unique: true },
    name: { type: String, default: null },
  },
  {
    timestamps: true,
    versionKey: false,
  }
);

export type UserDocument = InferSchemaType<typeof userSchema> & { _id: string };
export const User = model("User", userSchema);

export const connection = mongoose;
export const db = { connection, User };
```

## 3. Create a Mongoose plugin

```ts
// src/db/plugin.ts
import 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` degrades to `any`.

```ts
// src/db/mongoose.ts (the module that exports db)
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 { mongoosePlugin } from "./db/plugin.ts";

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

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

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

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

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

## Sessions and transactions

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.

**Diagram: Session-scoped transaction**

Participants: Handler, Session, Models

1. **Handler -> Session** (request) - Start a session - startSession()
2. **Handler -> Models** (request) - Run every write inside withTransaction - User.create([...], { session })
3. **Session -> Handler** (response) - Commit on success, roll back on throw
4. **Handler -> Session** (note) - Always end the session in finally - endSession()

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.

```ts
handler: async ({ body, state }) => {
  const session = await state.db.connection.startSession();

  try {
    let createdUser: unknown;
    await session.withTransaction(async () => {
      const [user] = await state.db.User.create([{ email: body.email, name: body.name }], { session });
      createdUser = user.toObject();
      await state.db.AuditLog.create([{ action: "user.created", userId: user.id }], { session });
    });

    return { status: 201, body: createdUser };
  } finally {
    await session.endSession();
  }
}
```

## Validation and errors

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](/docs/orm) instead.

Compare with [Ottoman](/docs/odm/ottoman) for Couchbase, [Prisma](/docs/orm/prisma) for SQL, or return to the [ODM overview](/docs/odm).

---

Source: https://daloyjs.dev/docs/odm/mongoose