Search docs

Jump between documentation pages.

Browse docs

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's state.
  • app.onClose(async () => client.disconnect()) ties cleanup to graceful shutdown.
One app, your choice of data layer
your appDaloyJS appapp.decorate("db", client)
schema-firstPrisma@prisma/client
ts-firstDrizzle ORMdrizzle-orm
decoratorsTypeORMDataSource
unit of workMikroORMEntityManager
active recordSequelizeSequelize models
platformSupabase@supabase/supabase-js
DaloyJS is database-agnostic. The same decorate plus onClose pattern wires any SQL client (or the Supabase platform client) onto every handler's state, so the choice of data layer stays a swappable detail.

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.

Where the client lives
Route handlerreads the client off state, type-safe
state.db.user.findUnique(...)
App stateattached once via app.decorate("db", client)
state.db
ORM / query clientone shared instance per process
PrismaDrizzleTypeORM...
DatabasePostgres, MySQL, SQLite, ...
A plugin decorates one shared client onto app state at startup. Every route handler reaches the same client through state.db, and onClose ties teardown to graceful shutdown.
ts
// 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.
  • MikroORM: Data Mapper, Unit of Work, and Identity Map with first-class TypeScript.
  • 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 layerNode.jsBunDenoCloudflare WorkersVercel
PrismaYesYesYesYes, with Driver AdaptersYes, with Driver Adapters
Drizzle ORMYesYesYesYesYes
TypeORMYesPartialPartialNoNo
MikroORMYesYesPartialNoNo
SequelizeYesPartialNoNoNo
Supabase JSYesYesYesYesYes

For edge runtimes (Cloudflare Workers, Vercel), prefer Drizzle or Supabase, or use Prisma with Driver Adapters. TypeORM, MikroORM, and Sequelize all lean on Node-centric runtime assumptions 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:

ts
// 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.

ts
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:

ts
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;
}

Next steps