Use Mongoose with DaloyJS
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.
1. Install
pnpm add mongoose2. Define a schema and model
// 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
// src/db/plugin.ts
import type { App } from "@daloyjs/core";
import { connection, db } from "./mongoose";
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
// src/types/state.d.ts
import type { db } from "../db/mongoose";
declare module "@daloyjs/core" {
interface AppState {
db: typeof db;
}
}5. Use it in routes
// src/server.ts
import { z } from "zod";
import { App, HttpError } from "@daloyjs/core";
import { serve } from "@daloyjs/core/node";
import { mongoosePlugin } from "./db/plugin";
const UserSchema = z.object({
id: z.string(),
email: z.string().email(),
name: z.string().nullable(),
});
const app = new App();
app.register(mongoosePlugin);
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).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.
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.
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.
Compare with Ottoman for Couchbase, Prisma for SQL, or return to the ODM overview.