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.
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.
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.
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 abovedeclare 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.
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):
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.