Skip to content

Search docs

Jump between documentation pages.

Browse docs

Use TypeORM with DaloyJS

TypeORM gives you decorator-based entities and the active-record / data-mapper patterns familiar to Java and .NET teams. This guide targets TypeORM 1.x (npm latest is 1.1 as of October 2026; the old 0.3 line is on the legacy tag) and runs best on the Node.js adapter. TypeORM 1.0 requires Node.js 20 or newer.

One request through TypeORM
  1. 01clientHTTP requestGET /users/:id
  2. 02zodValidated inputparams.id is a uuid
  3. 03typeormRepository querygetRepository(User).findOneBy(...)
  4. 04responseTyped body200 UserSchema | 404
Zod validates the request, the handler gets a repository off the DataSource on state.db, runs the entity query, then the response schema checks the body on the way out.

1. Install

bash
pnpm add typeorm@^1 reflect-metadata pg
pnpm add -D @types/node

2. Decorators need a real build step

Enable legacy decorators in the tsconfig.json you build with:

tsconfig.jsonjson
{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "strictPropertyInitialization": false
  }
}

Then develop against the compiled output, for example tsc -w in one terminal and node --watch --env-file=.env dist/server.js in another. Give every column an explicit type (as below) so column types never depend on emitted design-type metadata, whichever compiler you use.

3. Define an entity

src/db/entities/User.tsts
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn } from "typeorm";

@Entity("users")
export class User {
  @PrimaryGeneratedColumn("uuid")
  id!: string;

  @Column({ type: "text", unique: true })
  email!: string;

  @Column({ type: "text", nullable: true })
  name!: string | null;

  @CreateDateColumn({ type: "timestamptz", name: "created_at" })
  createdAt!: Date;
}

4. Configure the DataSource

TypeORM 1.0 no longer loads .env files or TYPEORM_* environment variables on its own. Load configuration yourself (node --env-file=.env, or import "dotenv/config" at the entrypoint).

src/db/data-source.tsts
import "reflect-metadata";
import { DataSource } from "typeorm";
import { User } from "./entities/User.ts";

export const AppDataSource = new DataSource({
  type: "postgres",
  url: process.env.DATABASE_URL,
  entities: [User],
  // Resolved from the project root against the compiled output, so the same
  // glob works for the app and for the CLI after `pnpm build`.
  migrations: ["dist/db/migrations/*.js"],
  synchronize: false, // use migrations in production
  logging: process.env.NODE_ENV !== "production",
});

5. Create a TypeORM plugin

src/db/plugin.tsts
import type { App } from "@daloyjs/core";
import { AppDataSource } from "./data-source.ts";

// 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 typeormPlugin(app: App) {
  if (!AppDataSource.isInitialized) {
    await AppDataSource.initialize();
  }
  app.decorate("db", AppDataSource);
  app.onClose(async () => {
    if (AppDataSource.isInitialized) await AppDataSource.destroy();
  });
}

onClose() runs on the Node.js, Bun, and Deno adapters only; see Serverless and edge lifecycle for Lambda, Vercel, and Azure Functions.

6. Augment app state types

Add the declare module block to the same module that creates the plugin, 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.

src/db/plugin.tsts
// same module as the plugin above
declare module "@daloyjs/core" {
  interface AppState {
    db: typeof AppDataSource;
  }
}

7. Use repositories in routes

src/server.tsts
import "reflect-metadata";
import { z } from "zod";
import { App, NotFoundError } from "@daloyjs/core";
import { serve } from "@daloyjs/core/node";
import { typeormPlugin } from "./db/plugin.ts";
import { User } from "./db/entities/User.ts";

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

// Dates go over the wire as ISO strings: z.coerce.date() has no
// representation in the generated OpenAPI document.
const UserSchema = z.object({
  id: z.uuid(),
  email: z.email(),
  name: z.string().nullable(),
  createdAt: z.iso.datetime(),
});

const toUser = (user: User) => ({ ...user, createdAt: user.createdAt.toISOString() });

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 repo = state.db.getRepository(User);
    const user = await repo.findOneBy({ id: params.id });
    if (!user) throw new NotFoundError("User not found");
    return { status: 200, body: toUser(user) };
  },
);

app.post(
  "/users",
  {
    operationId: "createUser",
    request: { body: z.object({ email: z.email(), name: z.string().optional() }) },
    responses: { 201: { description: "Created", body: UserSchema } },
  },
  async ({ body, state }) => {
    const repo = state.db.getRepository(User);
    // Safe only because z.object() strips unknown keys (no mass assignment).
    const created = await repo.save(repo.create(body));
    return { status: 201, body: toUser(created) };
  },
);

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

Transactions

This example assumes Order and Inventory entities and a route that declares 201.

ts
import { Inventory } from "./db/entities/Inventory.ts";
import { Order } from "./db/entities/Order.ts";

handler: async ({ body, state }) => {
  const order = await state.db.transaction(async (manager) => {
    const created = await manager.save(Order, manager.create(Order, { sku: body.sku, qty: body.qty }));
    await manager.decrement(Inventory, { sku: body.sku }, "stock", body.qty);
    return created;
  });
  return { status: 201, body: { id: order.id, sku: order.sku, qty: order.qty } };
}

Migrations

Generate against the compiled DataSource, rebuild so the new migration lands in dist/, then run it as a deploy step, never at app boot (see migrations):

bash
pnpm build
pnpm typeorm migration:generate src/db/migrations/InitUser -d dist/db/data-source.js
pnpm build
pnpm typeorm migration:run -d dist/db/data-source.js

Runtime notes

  • TypeORM is built around long-lived Node.js processes and native drivers. We have not verified it on Cloudflare Workers, Bun, or Deno; use Drizzle or Supabase on the edge.
  • Always import reflect-metadata once at the entrypoint, before anything else.

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

Edit this page on GitHub

Last updated