Skip to content

Search docs

Jump between documentation pages.

Browse docs

Use Sequelize with DaloyJS

Sequelize is a mature ORM for SQL databases with model definitions, associations, transactions, and broad driver support. It is a strong fit when your team prefers an Active Record style API and deploys DaloyJS on Node.js.

One request through Sequelize
  1. 01clientHTTP requestGET /users/:id
  2. 02zodValidated inputparams.id is a uuid
  3. 03sequelizeModel querystate.db.User.findByPk(id)
  4. 04responseTyped bodyuser.toJSON() | 404
Zod validates the request, the handler queries a Sequelize model off state.db, then the response schema checks the body on the way out (call toJSON() so the plain object matches the schema).

1. Install

bash
pnpm add sequelize pg pg-hstore
pnpm add -D @types/validator @types/node typescript

Swap the driver package if you target MySQL, MariaDB, MSSQL, or SQLite instead of Postgres.

This guide targets Sequelize v6, the current stable line (sequelize@latest is 6.37 as of October 2026). Sequelize v7 ships under a new package name, @sequelize/core, and is still in alpha, so its APIs may change before a stable release.

2. Define a model

The scaffolded tsconfig.json sets verbatimModuleSyntax, and Node's type stripping (what daloy dev runs) needs type-only imports marked as such, so import the attribute helpers with import type:

src/db/sequelize.tsts
import { Sequelize, DataTypes, Model } from "sequelize";
import type { CreationOptional, InferAttributes, InferCreationAttributes } from "sequelize";

export const sequelize = new Sequelize(process.env.DATABASE_URL!, {
  dialect: "postgres",
  logging: process.env.NODE_ENV === "production" ? false : console.log,
});

export class User extends Model<InferAttributes<User>, InferCreationAttributes<User>> {
  declare id: CreationOptional<string>;
  declare email: string;
  declare name: string | null;
}

User.init(
  {
    id: {
      type: DataTypes.UUID,
      defaultValue: DataTypes.UUIDV4,
      primaryKey: true,
    },
    email: {
      type: DataTypes.STRING,
      allowNull: false,
      unique: true,
    },
    name: {
      type: DataTypes.STRING,
      allowNull: true,
    },
  },
  {
    sequelize,
    modelName: "User",
    tableName: "users",
    underscored: true,
  }
);

3. Create a Sequelize plugin

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

export const db = { sequelize, User };

// Call with the root app, not through app.register(): a decoration made
// inside a registered plugin is visible only to that plugin's own routes.
export async function sequelizePlugin(app: App) {
  await sequelize.authenticate();
  app.decorate("db", db);
  app.onClose(async () => {
    await sequelize.close();
  });
}

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/plugin.ts (same module as the plugin above)
declare module "@daloyjs/core" {
  interface AppState {
    db: typeof db;
  }
}

5. Use it in routes

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

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

const app = new App();
await sequelizePlugin(app);

app.get(
  "/users/:id",
  {
    operationId: "getUser",
    request: { params: z.object({ id: z.uuid() }) },
    responses: {
      200: { description: "Found", body: UserSchema },
      404: { description: "Not found" },
    },
  },
  async ({ params, state }) => {
    const user = await state.db.User.findByPk(params.id);
    if (!user) throw new NotFoundError("User not found");
    return { status: 200, body: user.toJSON() };
  },
);

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

Transactions

Use managed transactions so DaloyJS can map one handler invocation to one atomic unit of work. This example assumes Order and Inventory models defined like User above, exported on db, and a route that declares 201.

ts
handler: async ({ body, state }) => {
  const created = await state.db.sequelize.transaction(async (transaction) => {
    const order = await state.db.Order.create({ sku: body.sku, qty: body.qty }, { transaction });
    await state.db.Inventory.decrement("stock", {
      by: body.qty,
      where: { sku: body.sku },
      transaction,
    });
    return order;
  });

  return { status: 201, body: created.toJSON() };
}

Migrations

Sequelize supports migrations via the CLI, but many teams keep model definitions in TypeScript and run explicit migration files through sequelize-cli or Umzug. Run migrations as a deploy step, never at app boot (see migrations), and never call sequelize.sync() in production.

bash
pnpm add -D sequelize-cli
pnpm sequelize-cli migration:generate --name create-users
pnpm sequelize-cli db:migrate

sequelize-cli is CommonJS-first. In a scaffolded DaloyJS app ("type": "module"), keep .sequelizerc and the config file it points to in CommonJS, for example config/config.cjs, and check the CLI docs for your version before writing migrations as ES modules.

Runtime constraints

Sequelize depends on Node-oriented drivers, so it is best on the Node.js adapter. onClose() runs on the Node.js, Bun, and Deno adapters only; on Lambda, Vercel, or Azure Functions keep the pool small (pool: { max: 1 }) and see Serverless and edge lifecycle. For edge runtimes, prefer Drizzle, Prisma with driver adapters, or Supabase.

Compare with Prisma, Drizzle, TypeORM, MikroORM, Kysely, Supabase, or the ODM overview for document databases.

Edit this page on GitHub

Last updated