Migrating from Prisma

Turbine is a Postgres-native TypeScript ORM with a Prisma-inspired API. If you're on Prisma but want a smaller dependency footprint, edge/serverless without an extra adapter, and a code-first schema (no .prisma DSL), Turbine is a near drop-in. Most of your query code moves over by renaming include to with and re-pointing your imports.

This is the full mapping. Skip to Side-by-side if you want to see it working.

API mapping

PrismaTurbineNotes
prisma.user.findManydb.users.findManyTable accessor uses the snake_case table name (camelCased).
prisma.user.findUniquedb.users.findUniqueSame shape.
prisma.user.findFirstdb.users.findFirstSame.
prisma.user.findFirstOrThrowdb.users.findFirstOrThrowThrows NotFoundError (TURBINE_E001).
prisma.user.findUniqueOrThrowdb.users.findUniqueOrThrowSame.
prisma.user.createdb.users.createSame data shape.
prisma.user.createManydb.users.createManySingle INSERT ... UNNEST under the hood.
prisma.user.updatedb.users.updateSupports atomic operators: { count: { increment: 1 } }.
prisma.user.updateManydb.users.updateManyEmpty where rejected unless allowFullTableScan: true.
prisma.user.deletedb.users.deleteSame.
prisma.user.deleteManydb.users.deleteManyEmpty where rejected unless allowFullTableScan: true.
prisma.user.upsertdb.users.upsertSame where / create / update shape.
prisma.user.countdb.users.countSame.
prisma.user.aggregatedb.users.aggregate_sum / _avg / _min / _max / _count.
prisma.user.groupBydb.users.groupByby, where, orderBy supported.
prisma.$transactiondb.$transactionCallback form with nested SAVEPOINTs and isolation levels.
include: { posts: true }with: { posts: true }The only renamed key.
select: { id: true, name: true }select: { id: true, name: true }Same.
where: { name: { contains: 'a' } }where: { name: { contains: 'a' } }All operators ported.
where: { posts: { some: ... } }where: { posts: { some: ... } }Relation filters: some / every / none.
take: 10limit: 10Renamed.
skip: 20offset: 20Renamed.

Schema translation

Prisma's .prisma schema translates to Turbine's defineSchema() call.

// schema.prisma
model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  name      String
  posts     Post[]
  createdAt DateTime @default(now())
}
 
model Post {
  id        Int      @id @default(autoincrement())
  userId    Int
  user      User     @relation(fields: [userId], references: [id])
  title     String
  published Boolean  @default(false)
  viewCount Int      @default(0)
  createdAt DateTime @default(now())
}
// schema.ts
import { defineSchema } from 'turbine-orm';
 
export default defineSchema({
  users: {
    id: { type: 'serial', primaryKey: true },
    email: { type: 'text', unique: true, notNull: true },
    name: { type: 'text', notNull: true },
    createdAt: { type: 'timestamp', default: 'now()' },
  },
  posts: {
    id: { type: 'serial', primaryKey: true },
    userId: { type: 'bigint', notNull: true, references: 'users.id' },
    title: { type: 'text', notNull: true },
    published: { type: 'boolean', notNull: true, default: 'false' },
    viewCount: { type: 'integer', notNull: true, default: '0' },
    createdAt: { type: 'timestamp', default: 'now()' },
  },
});

Relations aren't declared in Turbine — they're inferred from foreign keys. posts.userId references 'users.id' automatically yields user on Post and posts on User.

Side-by-side

findMany with nested relations

// Prisma
const users = await prisma.user.findMany({
  where: { orgId: 1 },
  include: { posts: { orderBy: { createdAt: 'desc' }, take: 5 } },
  orderBy: { createdAt: 'desc' },
  take: 10,
});
// Turbine
const users = await db.users.findMany({
  where: { orgId: 1 },
  with: { posts: { orderBy: { createdAt: 'desc' }, limit: 5 } },
  orderBy: { createdAt: 'desc' },
  limit: 10,
});

Atomic update

// Prisma
await prisma.post.update({
  where: { id: 42 },
  data: { viewCount: { increment: 1 } },
});
// Turbine — identical
await db.posts.update({
  where: { id: 42 },
  data: { viewCount: { increment: 1 } },
});

Both generate view_count = view_count + $1. No extra round-trip.

Transaction

// Prisma
await prisma.$transaction(async (tx) => {
  const user = await tx.user.create({ data: { email: 'a@b.c', name: 'A' } });
  await tx.post.create({ data: { userId: user.id, title: 'Hi' } });
});
// Turbine
await db.$transaction(async (tx) => {
  const user = await tx.users.create({ data: { email: 'a@b.c', name: 'A' } });
  await tx.posts.create({ data: { userId: user.id, title: 'Hi' } });
});

Upsert

// Prisma
await prisma.user.upsert({
  where: { email: 'a@b.c' },
  create: { email: 'a@b.c', name: 'A' },
  update: { name: 'A' },
});
// Turbine — identical
await db.users.upsert({
  where: { email: 'a@b.c' },
  create: { email: 'a@b.c', name: 'A' },
  update: { name: 'A' },
});

Relation filter

// Prisma
const active = await prisma.user.findMany({
  where: { posts: { some: { published: true } } },
});
// Turbine — identical
const active = await db.users.findMany({
  where: { posts: { some: { published: true } } },
});

Notable differences

  • No schema.prisma. Code-first defineSchema() in a TypeScript module. npx turbine push (fast path) or migrate create --auto (generates SQL). No DSL, no separate parser.
  • includewith, takelimit, skipoffset. That's the full lexical diff.
  • Atomic update operators are first-class. set, increment, decrement, multiply, divide. All compile to in-place SQL.
  • Typed errors with codes. UniqueConstraintError / ForeignKeyError / NotNullViolationError / CheckConstraintError all carry code (TURBINE_E008E011) and cause. findUniqueOrThrow throws NotFoundError (TURBINE_E001) with the where attached. DeadlockError / SerializationFailureError have readonly isRetryable = true as const.
  • Driver-agnostic edge support. Pass any pg-compatible pool to turbineHttp(pool, schema) and the same API runs on Neon, Vercel, Cloudflare Hyperdrive, Supabase. No extra adapter package.
  • Single runtime dependency. pg only. No engine binary, no WASM, no @prisma/client.
  • Postgres only. Intentional — depth over breadth enables the edge and Studio stories. See the roadmap for planned dialects.

Migration checklist

  1. npm install turbine-orm && npm uninstall @prisma/client prisma
  2. Write schema.ts mirroring your .prisma models (or run npx turbine pull to introspect your live DB).
  3. npx turbine generate — writes ./generated/turbine/{types,metadata,index}.ts.
  4. Find/replace:
    • prisma.db.
    • Singular model names → plural snake-camelCase table names (prisma.userdb.users)
    • include:with:
    • take:limit:
    • skip:offset:
  5. Replace import { PrismaClient } from '@prisma/client' with import { turbine } from './generated/turbine'.
  6. Port any raw SQL from prisma.$queryRaw to db.raw\SELECT ...``.
  7. Update your error-handling to the typed classes (or keep catching by message during transition).
  8. Delete schema.prisma and the prisma/ directory once the build passes.

See also