# Use MikroORM with DaloyJS

[MikroORM](https://mikro-orm.io) is a TypeScript ORM built around a Data Mapper, Unit of Work, and Identity Map. It supports PostgreSQL, MySQL/MariaDB, SQLite, libSQL/Turso, PGlite, MSSQL, Oracle, and MongoDB. This SQL-focused guide targets MikroORM v7, PostgreSQL, and the [Node.js adapter](/docs/adapters).

## 1. Install

Install `@mikro-orm/core` together with the driver package for your database. The version of every `@mikro-orm/*` package must match.

```ts
# PostgreSQL (also CockroachDB)
pnpm add @mikro-orm/core @mikro-orm/postgresql
# or MySQL / MariaDB
pnpm add @mikro-orm/core @mikro-orm/mysql
# or SQLite
pnpm add @mikro-orm/core @mikro-orm/sqlite
# or libSQL / Turso
pnpm add @mikro-orm/core @mikro-orm/libsql
# or PGlite (embedded PostgreSQL in WASM)
pnpm add @mikro-orm/core @mikro-orm/pglite

# CLI + migrations (optional, dev-only)
pnpm add -D @mikro-orm/cli @mikro-orm/migrations
```

MikroORM v7 supports both ES-spec decorators and the legacy `experimentalDecorators` flag. The examples below use the `defineEntity` helper, which is decorator-free and gives you full TypeScript inference without any compiler flags.

## 2. Define an entity

`defineEntity` returns a schema object you can attach to a real class. The class gives you a named type for your handlers; the schema gives MikroORM its metadata.

```ts
// src/db/entities/User.ts
import { defineEntity, p } from "@mikro-orm/core";

const UserSchema = defineEntity({
  name: "User",
  properties: {
    id: p.uuid().primary().defaultRaw("gen_random_uuid()"),
    email: p.string().unique(),
    name: p.string().nullable(),
    createdAt: p.datetime().onCreate(() => new Date()),
  },
});

export class User extends UserSchema.class {}
UserSchema.setClass(User);
```

## 3. Configure the ORM

Import `defineConfig` from your driver package, it infers the driver and gives you IntelliSense without extra type hints.

```ts
// src/mikro-orm.config.ts
import { defineConfig } from "@mikro-orm/postgresql";
import { Migrator } from "@mikro-orm/migrations";
      import { User } from "./db/entities/User.ts";

export default defineConfig({
  entities: [User],
  clientUrl: process.env.DATABASE_URL,
  // production-friendly defaults
  debug: process.env.NODE_ENV !== "production",
  extensions: [Migrator],
  migrations: {
    path: "./dist/db/migrations",
    pathTs: "./src/db/migrations",
  },
});
```

## 4. Create a MikroORM plugin

Initialize the ORM once at startup, decorate the app with the root ORM instance, and close it on shutdown. Handlers get their own forked `EntityManager` in step 5.

```ts
// src/db/plugin.ts
import type { App } from "@daloyjs/core";
import { MikroORM } from "@mikro-orm/postgresql";
import config from "../mikro-orm.config.ts";

export const mikroOrmPlugin = {
  name: "mikro-orm",
  async register(app: App) {
    const orm = await MikroORM.init(config);
    app.decorate("orm", orm);
    app.onClose(async () => {
      await orm.close(true);
    });
  },
};
```

## 5. Fork an EntityManager per request

MikroORM relies on an **Identity Map** that is bound to an `EntityManager`. You must *fork* the root EM for every request so identity maps and unit-of-work state do not leak between concurrent handlers. Do it in middleware and expose the forked EM on `state`.

**Diagram: A forked EntityManager per request**

1. **Root ORM** (startup) - state.orm = await MikroORM.init(...)
2. **Fork per request** (beforeHandle) - state.em = state.orm.em.fork()
3. **Scoped queries** (handler) - state.em.create / findOne
4. **Persist on flush** (unit of work) - await state.em.flush()

The plugin decorates one root ORM at startup. A beforeHandle hook forks a fresh EntityManager for each request onto state.em, so the identity map and unit-of-work state stay isolated, then a single flush() (or transactional()) persists the changes.

```ts
// src/db/middleware.ts
import type { Hooks } from "@daloyjs/core";

export function requestEntityManager(): Hooks {
  return {
    beforeHandle(ctx) {
      ctx.state.em = ctx.state.orm.em.fork();
    },
  };
}
```

In Express-style middleware you will often see `RequestContext.create(orm.em, next)`. Daloy hooks pass typed request state directly, so the simplest pattern is to use the forked `state.em` inside handlers.

## 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.orm` / `state.em` degrade to `any`.

```ts
// src/db/plugin.ts (same module as the plugin above)
import type { EntityManager } from "@mikro-orm/postgresql";

declare module "@daloyjs/core" {
  interface AppState {
    orm: MikroORM;
    em: EntityManager;
  }
}
```

## 7. Use the EntityManager in routes

```ts
// src/server.ts
import { z } from "zod";
import { App } from "@daloyjs/core";
import { serve } from "@daloyjs/core/node";
import { mikroOrmPlugin } from "./db/plugin.ts";
import { requestEntityManager } from "./db/middleware.ts";
import { User } from "./db/entities/User.ts";

const app = new App();
app.register(mikroOrmPlugin);
app.use(requestEntityManager());

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

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.em.findOne(User, { id: params.id });
    return user
      ? { status: 200, body: user }
      : { status: 404, body: { type: "about:blank", title: "Not found", status: 404 } };
  },
);

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 user = state.em.create(User, { email: body.email, name: body.name ?? null });
    await state.em.flush();
    return { status: 201, body: user };
  },
);

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

MikroORM batches every change in the forked EM into a single `flush()`. You almost never need to call `persist()` manually when using `em.create()`, which auto-persists in v6+. Entities created with `new User()` still need `em.persist()`.

## Transactions

Use `em.transactional()` inside the handler that owns the unit of work. The callback receives a transactional EM that commits on success and rolls back if you throw.

```ts
handler: async ({ body, state }) => {
  const order = await state.em.transactional(async (em) => {
    const created = em.create(Order, body);
    const inventory = await em.findOneOrFail(Inventory, { sku: body.sku });
    inventory.stock -= body.qty;
    return created;
  });
  return { status: 201, body: order };
}
```

## Migrations

The CLI is installed as a dev dependency and reads `src/mikro-orm.config.ts` by default. If you move the config under `src/db`, pass `--config` or configure `mikro-orm.configPaths` in `package.json`.

```ts
# generate a migration from the current entity diff
pnpm mikro-orm migration:create

# apply all pending migrations
pnpm mikro-orm migration:up

# list pending migrations
pnpm mikro-orm migration:pending

# inspect the resolved CLI config
pnpm mikro-orm debug
```

## Errors

Translate MikroORM errors into framework errors so they serialize as [problem+json](/docs/errors):

```ts
import { HttpError } from "@daloyjs/core";
import { UniqueConstraintViolationException, NotFoundError } from "@mikro-orm/core";

try {
  const user = state.em.create(User, { email: body.email, name: body.name ?? null });
  await state.em.flush();
  return { status: 201, body: user };
} catch (err) {
  if (err instanceof UniqueConstraintViolationException) {
    throw new HttpError(409, {
      title: "User already exists",
      type: "https://daloyjs.dev/errors/duplicate",
    });
  }
  if (err instanceof NotFoundError) {
    throw new HttpError(404, { title: "Not found" });
  }
  throw err;
}
```

## Runtime notes

- MikroORM is a Node.js adapter default for Daloy apps. Edge runtimes require precompiled MikroORM functions plus a compatible driver, so most Cloudflare Workers apps should start with [Drizzle](/docs/orm/drizzle) or [Supabase](/docs/orm/supabase) instead.
- Always fork the EM per request. Sharing the root EM across requests will leak the identity map and corrupt unit-of-work state.
- Keep `@mikro-orm/core`, your driver package, the CLI, and `@mikro-orm/migrations` on the same version.

Compare with [Prisma](/docs/orm/prisma), [Drizzle](/docs/orm/drizzle), [TypeORM](/docs/orm/typeorm), [Sequelize](/docs/orm/sequelize), or the [ODM overview](/docs/odm) if you need document models.

---

Source: https://daloyjs.dev/docs/orm/mikro-orm