Using SQL ORMs with DaloyJS
DaloyJS is database-agnostic. Any SQL client that runs on your target runtime works, so pick the ORM or query layer that fits your team. The framework gives you two primitives that make integration boring (in a good way):
app.decorate("db", client)attaches a shared client to every handler'sstate.app.onClose(async () => client.disconnect())ties cleanup to graceful shutdown.
The recommended pattern
Wrap the database client in a plugin and register it once at the root of your app. Handlers read it from state with full type-safety.
// src/db/plugin.ts
import type { App } from "@daloyjs/core";
export function databasePlugin(client: DbClient) {
return {
name: "database",
async register(app: App) {
app.decorate("db", client);
app.onClose(async () => {
await client.$disconnect?.();
});
},
};
}
// src/server.ts
const app = new App();
app.register(databasePlugin(await createClient()));
app.route({
method: "GET",
path: "/users/:id",
operationId: "getUser",
request: { params: z.object({ id: z.string().uuid() }) },
responses: { 200: { description: "ok", body: UserSchema } },
handler: async ({ params, state }) => {
const user = await state.db.user.findUnique({ where: { id: params.id } });
return user
? { status: 200, body: user }
: { status: 404, body: { type: "about:blank", title: "Not found", status: 404 } };
},
});Pick your ORM
- Prisma — schema-first, mature migrations, great DX.
- Drizzle ORM — TypeScript-first, edge-friendly, SQL-like API.
- TypeORM — decorator-based entities for object-oriented teams.
- Sequelize — mature Active Record style models with broad SQL dialect support.
Need a platform client instead?
Supabase is not an ORM. It is a hosted Postgres platform with a fetch-based JavaScript client, auth, storage, realtime, and edge-friendly APIs. If that is the shape you need, use Supabase with DaloyJS.
- Supabase — platform client for hosted Postgres + auth via
@supabase/supabase-js.
Keep ORM and ODM separate
This section is intentionally SQL-focused. If you are using MongoDB or Couchbase, jump to the ODM overview and use Mongooseor Ottoman instead of forcing document models into an ORM-shaped abstraction.
Runtime compatibility cheat sheet
| Data layer | Node.js | Bun | Deno | Cloudflare Workers | Vercel Edge |
|---|---|---|---|---|---|
| Prisma | Yes | Yes | Yes | Yes, with Driver Adapters | Yes, with Driver Adapters |
| Drizzle ORM | Yes | Yes | Yes | Yes | Yes |
| TypeORM | Yes | Partial | Partial | No | No |
| Sequelize | Yes | Partial | No | No | No |
| Supabase JS | Yes | Yes | Yes | Yes | Yes |
For edge runtimes (Cloudflare Workers, Vercel Edge), prefer Drizzle or Supabase, or use Prisma with Driver Adapters. TypeORM and Sequelize both rely on Node-centric drivers and are best on the Node.js adapter.
Typing the decorated client
Use the exported AppState augmentation point to make decorated clients available on state in every handler:
// src/types/state.d.ts
import type { PrismaClient } from "@prisma/client";
declare module "@daloyjs/core" {
interface AppState {
db: PrismaClient;
}
}Transactions
Don't open transactions in middleware. Open them inside the handler that owns the unit of work, so your contract response (success or error) maps cleanly onto commit / rollback.
handler: async ({ body, state }) => {
return state.db.$transaction(async (tx) => {
const order = await tx.order.create({ data: body });
await tx.inventory.update({
where: { sku: body.sku },
data: { stock: { decrement: body.qty } },
});
return { status: 201, body: order };
});
}Errors
Translate database errors into framework errors so they serialize as problem+json automatically:
import { HttpError } from "@daloyjs/core";
try {
return await state.db.user.create({ data: body });
} catch (err) {
if (isUniqueViolation(err)) {
throw new HttpError(409, {
title: "User already exists",
type: "https://daloyjs.dev/errors/duplicate",
});
}
throw err;
}