Search docs

Jump between documentation pages.

Use Prisma with DaloyJS

Prisma is a schema-first ORM with first-class migrations and a generated, fully typed client. It pairs well with DaloyJS's contract-first routes: Zod validates the wire, Prisma validates the database.

1. Install

ts
pnpm add @prisma/client @prisma/adapter-pg dotenv
pnpm add -D prisma
pnpm prisma init --datasource-provider postgresql

2. Define your schema

ts
// prisma/schema.prisma
datasource db {
  provider = "postgresql"
}

generator client {
  provider = "prisma-client"
  output   = "../src/generated/prisma"
}

model User {
  id    String @id @default(uuid())
  email String @unique
  name  String?
}
ts
// prisma.config.ts
import "dotenv/config";
import { defineConfig, env } from "prisma/config";

export default defineConfig({
  schema: "prisma/schema.prisma",
  migrations: { path: "prisma/migrations" },
  datasource: {
    url: env("DATABASE_URL"),
  },
});

Prisma's current prisma-client generator writes the client to the configured output path. Connection URLs live in prisma.config.ts, and application code imports PrismaClient from the generated path instead of @prisma/client.

ts
pnpm prisma migrate dev --name init
pnpm prisma generate

3. Create a Prisma plugin

Instantiate one PrismaClient per process, decorate the app, and disconnect on shutdown.

ts
// src/db/prisma.ts
import { PrismaPg } from "@prisma/adapter-pg";
import { PrismaClient } from "../generated/prisma/client";
import type { App } from "@daloyjs/core";

const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL! });
export const prisma = new PrismaClient({
  adapter,
  log: process.env.NODE_ENV === "production" ? ["error"] : ["query", "error"],
});

export const prismaPlugin = {
  name: "prisma",
  async register(app: App) {
    await prisma.$connect();
    app.decorate("db", prisma);
    app.onClose(async () => {
      await prisma.$disconnect();
    });
  },
};

4. Augment app state types

ts
// src/types/state.d.ts
import type { PrismaClient } from "../generated/prisma/client";

declare module "@daloyjs/core" {
  interface AppState {
    db: PrismaClient;
  }
}

5. Wire the plugin and route

ts
// src/server.ts
import { z } from "zod";
import { App, secureHeaders, requestId } from "@daloyjs/core";
import { serve } from "@daloyjs/core/node";
import { prismaPlugin } from "./db/prisma";

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

const app = new App();
app.use(requestId());
app.use(secureHeaders());
app.register(prismaPlugin);

app.route({
  method: "GET",
  path: "/users/:id",
  operationId: "getUser",
  request: { params: z.object({ id: z.string().uuid() }) },
  responses: {
    200: { description: "Found", body: UserSchema },
    404: { description: "Not found" },
  },
  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 } };
  },
});

app.route({
  method: "POST",
  path: "/users",
  operationId: "createUser",
  request: { body: z.object({ email: z.string().email(), name: z.string().optional() }) },
  responses: { 201: { description: "Created", body: UserSchema } },
  handler: async ({ body, state }) => ({
    status: 201,
    body: await state.db.user.create({ data: body }),
  }),
});

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

Transactions

Use $transaction for atomic units of work. Throwing inside the callback rolls back; a successful return commits.

ts
handler: async ({ body, state }) => {
  const order = await state.db.$transaction(async (tx) => {
    const created = await tx.order.create({ data: { sku: body.sku, qty: body.qty } });
    await tx.inventory.update({
      where: { sku: body.sku },
      data: { stock: { decrement: body.qty } },
    });
    return created;
  });
  return { status: 201, body: order };
}

Edge runtimes

For Cloudflare Workers and Vercel Edge, set the generated client runtime for your target and use the appropriate Prisma Driver Adapter (Neon, PlanetScale, D1, etc.).

ts
// prisma/schema.prisma
generator client {
  provider = "prisma-client"
  output   = "../src/generated/prisma"
  runtime  = "vercel-edge" // use "workerd" for Cloudflare Workers
}

// src/db/prisma-edge.ts
import { PrismaClient } from "../generated/prisma/client";
import { PrismaNeon } from "@prisma/adapter-neon";

const adapter = new PrismaNeon({ connectionString: env.DATABASE_URL });
export const prisma = new PrismaClient({ adapter });

Mapping errors to problem+json

ts
import { Prisma } from "../generated/prisma/client";
import { HttpError } from "@daloyjs/core";

try {
  return await state.db.user.create({ data: body });
} catch (err) {
  if (err instanceof Prisma.PrismaClientKnownRequestError && err.code === "P2002") {
    throw new HttpError(409, { title: "Email already in use" });
  }
  throw err;
}

Continue with Drizzle, TypeORM, Sequelize, or the ODM overview for document databases.

For serverless or edge deployments, see the database hosting overview — Prisma supports Neon, PlanetScale, and Cloudflare D1 through Driver Adapters.