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.
One request through Prisma
01clientHTTP requestGET /users/:id
02zodValidated inputparams.id is a uuid
03prismaTyped querystate.db.user.findUnique(...)
04responseTyped body200 UserSchema | 404
Zod validates the request before your handler runs, Prisma runs the typed query off state.db, and the result is checked against the response schema on the way out. Two validation layers guard the wire and the database.
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 initpnpm prisma generate
3. Create a Prisma plugin
Instantiate one PrismaClient per process, decorate the app, and disconnect on shutdown.
For Cloudflare Workers and Vercel, set the generated client runtime for your target and use the appropriate Prisma Driver Adapter (Neon, PlanetScale, D1, etc.).
ts
// prisma/schema.prismagenerator client { provider = "prisma-client" output = "../src/generated/prisma" runtime = "vercel" // use "workerd" for Cloudflare Workers}// src/db/prisma-edge.tsimport { 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 });
Operator injection: validate your filter shapes
Prisma always emits parameterized SQL, but the filter object you pass to where is interpreted by Prisma. If a field annotated as string arrives at runtime as an object like { "not": "" }, Prisma treats it as an operator and an attacker can bypass equality checks, the “NoSQL-injection-in-Prisma” pattern documented by Aikido. Daloy's contract-first routes neutralize this when you keep the request body typed with primitive Zod schemas (z.string(), z.number(), …) instead of z.any(), z.unknown(), or a pass-through z.record(). See Security → SQL injection for the full pattern and review-time rules.
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;}