A Prisma generator that creates a fully-typed, Effect-based service wrapper for your Prisma Client.
- 🚀 Effect Integration: All Prisma operations are wrapped in
Effectfor robust error handling and composability. - 🛡️ Type Safety: Full TypeScript support with generated types matching your Prisma schema.
- 🧩 Dependency Injection: Integrates seamlessly with Effect's
LayerandContextsystem. - 🔍 Error Handling: Automatically catches and wraps Prisma errors into specific typed Effect errors.
Install the generator as a development dependency:
npm install -D effect-prisma-generator
# or
pnpm add -D effect-prisma-generator
# or
yarn add -D effect-prisma-generatoreffect and @prisma/client are peer dependencies — install them in your project alongside Prisma.
The generator supports both Effect v3 and Effect v4 (beta) from a single package. It detects the effect version installed in your project and emits matching code, so you don't need a special install:
- Effect v3 → uses
Context.Tagand the auto-generatedPrismaService.Defaultlayer. - Effect v4 → uses
Context.Serviceand exposesPrismaService.layer.
Effect v4 is still in beta and its APIs may shift between releases, so v4 output is considered experimental.
If detection ever picks the wrong major (or effect can't be resolved — in which case it defaults to v3), set it explicitly in the generator block:
generator effect {
provider = "effect-prisma-generator"
output = "./generated/effect.ts"
clientImportPath = "./client"
effectVersion = "4" // "3" or "4"; omit to auto-detect
}Add the generator to your schema.prisma file:
// prisma/schema.prisma
generator client {
provider = "prisma-client"
output = "./generated"
}
generator effect {
provider = "effect-prisma-generator"
output = "./generated/effect.ts" // relative to the schema.prisma file
clientImportPath = "./client" // relative to the output path ^
}Then run prisma generate to generate the client and the Effect service.
Add the following to your tsconfig.json:
{
"compilerOptions": {
"paths": {
"~prisma/*": ["./prisma/generated/*"]
}
}
}Then you can import the generated PrismaService (and PrismaClient) like this:
import { PrismaClient } from "~prisma/client";
import { PrismaService } from "~prisma/effect";Otherwise, you can import the generated types like this (adjust the path accordingly):
import { PrismaClient } from "../../prisma/generated/client";
import { PrismaService } from "../../prisma/generated/effect";Initialize the PrismaClient and provide it to the generated PrismaService layer as a PrismaClientService. The layer accessor depends on your Effect version: PrismaService.Default on v3, PrismaService.layer on v4.
import { Effect, Layer } from "effect";
import { PrismaService, PrismaClientService } from "~prisma/effect";
// ... in your program
const prisma = new PrismaClient({ adapter });
const PrismaLayer = Layer.provide(
PrismaService.Default, // on Effect v4, use PrismaService.layer
Layer.succeed(PrismaClientService, prisma),
);A client created with client.$extends(...) — for example with
@prisma/extension-accelerate — has a static type that is not assignable to
PrismaClient, so Layer.succeed(PrismaClientService, extended) does not
typecheck. Use the generated layerFromPrismaClient instead, which accepts
plain and extended clients:
import { PrismaService, layerFromPrismaClient } from "~prisma/effect";
import { withAccelerate } from "@prisma/extension-accelerate";
const prisma = new PrismaClient().$extends(withAccelerate());
const PrismaLayer = Layer.provide(
PrismaService.Default, // on Effect v4, use PrismaService.layer
layerFromPrismaClient(prisma),
);At runtime all operations go through the extended client and behave per the extension. Note that an extension's type-level changes are not reflected in the service's types:
- Result extensions' computed fields don't appear on the service's return types.
- Extension-specific query arguments are rejected by the service's typed
methods — for Accelerate that means per-query caching config such as
cacheStrategycannot be passed through the service without a cast; use the extended client directly (keep your own reference to it) for those queries.
PrismaClientService stays typed as PrismaClient, so lifecycle methods
remain available through the service — $connect(), $disconnect(), and
batch $transaction all exist on extended clients too. The one exception is
$on: Prisma strips event listening from extended clients, so call $on on
the base client before $extends (per Prisma's own guidance) rather than
through the service when it was provided an extended client.
Access the PrismaService in your Effect programs.
import { PrismaService } from "./generated/effect";
import { Effect } from "effect";
const program = Effect.gen(function* () {
const prisma = yield* PrismaService;
// All standard Prisma operations are available
const users = yield* prisma.user.findMany({
where: { active: true },
select: {
id: true,
accounts: {
select: {
id: true,
},
},
},
});
// users: { id: string, accounts: { id: string }[] }[]
return users;
});The generated PrismaService mirrors your Prisma Client API but returns Effect<SpecificPrismaResultType, PrismaError, never> instead of Promises, where PrismaError is a specific union type based on the operation (e.g., PrismaCreateError, PrismaUpdateError, PrismaFindError).
All operations return an Effect that can fail with specific Prisma errors. The generator maps Prisma's error codes to typed Effect errors.
Each operation type (create, update, delete, find, etc.) returns a specific union of possible errors.
PrismaUniqueConstraintErrorPrismaForeignKeyConstraintErrorPrismaRecordNotFoundErrorPrismaRelationViolationErrorPrismaRelatedRecordNotFoundErrorPrismaTransactionConflictErrorPrismaValueTooLongErrorPrismaValueOutOfRangeErrorPrismaDbConstraintErrorPrismaConnectionErrorPrismaMissingRequiredValueErrorPrismaInputValidationError
All errors carry the following context:
{
cause: Prisma.PrismaClientKnownRequestError;
operation: string; // e.g. "create", "findUnique"
model: string; // e.g. "User", "Post"
}import { PrismaService, PrismaUniqueConstraintError } from "./generated/effect";
import { Effect } from "effect";
const program = Effect.gen(function* () {
const prisma = yield* PrismaService;
yield* prisma.user
.create({
data: { email: "test@example.com", name: "Test" },
})
.pipe(
Effect.catchTag("PrismaUniqueConstraintError", (error) =>
Effect.logError(`User with email already exists: ${error.model}`),
),
);
});The generated service includes a $transaction method that allows you to run multiple operations within a database transaction.
const program = Effect.gen(function* () {
const prisma = yield* PrismaService;
yield* prisma.$transaction(
Effect.gen(function* () {
const user = yield* prisma.user.create({ data: { name: "Alice" } });
yield* prisma.post.create({
data: { title: "Hello", authorId: user.id },
});
}),
);
});The $transaction method supports nesting. If you call $transaction within an existing transaction, it will reuse the parent transaction context. If any operation fails, the entire transaction (including the parent) is rolled back.
const program = Effect.gen(function* () {
const prisma = yield* PrismaService;
yield* prisma.$transaction(
Effect.gen(function* () {
// Operation 1
yield* prisma.user.create({ data: { name: "Parent" } });
// Nested transaction
yield* prisma.$transaction(
Effect.gen(function* () {
// Operation 2
yield* prisma.user.create({ data: { name: "Child" } });
}),
);
}),
);
});npm install
npm testThe integration tests under tests/ are an isolated package with their own
effect version, so the generator can be exercised against both Effect v3 and
v4. npm test installs the tests/ fixtures on first run (defaulting to the
Effect v4 line); CI runs the suite against both majors via a matrix that swaps
the tests/ effect / @effect/vitest versions.