Migrate from Prisma

A step-by-step guide for switching from Prisma to Turbine. The APIs are intentionally similar, so migration is straightforward.


Why Switch?

| | Prisma | Turbine | |---|---|---| | Nested queries | N+1 queries (one per relation level) | 1 query (json_agg) | | Latency | 7.4ms L3 nested (Neon) | 5.3ms L3 nested (Neon) | | Throughput | 3,784 RPS (L2, c=50) | 24,041 RPS (L2, c=50) | | Memory | 233MB | 109MB | | Cold start | ~300-500ms (engine binary) | Near zero | | Bundle size | ~15MB (Rust engine) | Thin client + pg | | Dependencies | @prisma/client + engine binary | pg only |


Migration Steps

1. Install Turbine

Terminal
npm install turbine-orm

2. Initialize

Terminal
npx turbine init

This creates turbine.config.ts. Set your DATABASE_URL:

TypeScript
import type { TurbineCliConfig } from 'turbine-orm/cli';

const config: TurbineCliConfig = {
  url: process.env.DATABASE_URL,
  out: './generated/turbine',
};

export default config;

3. Generate Types

Terminal
npx turbine generate

Turbine introspects your existing database -- the same one Prisma was using. No schema file needed. Your tables, columns, relations, and types are all discovered automatically.

4. Update Your Client

Before (Prisma):

TypeScript
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

After (Turbine):

TypeScript
import { turbine } from './generated/turbine';

const db = turbine({
  connectionString: process.env.DATABASE_URL,
});

5. Update Queries

Most queries translate directly. Here's a side-by-side reference.


Query Translation Guide

findUnique

TypeScript
// Prisma
const user = await prisma.user.findUnique({
  where: { id: 1 },
});

// Turbine
const user = await db.users.findUnique({
  where: { id: 1 },
});

Note: Prisma uses singular model names (prisma.user). Turbine uses the actual table name (db.users).

findMany with Filters

TypeScript
// Prisma
const users = await prisma.user.findMany({
  where: {
    role: 'admin',
    email: { contains: 'example.com' },
    age: { gte: 18 },
  },
  orderBy: { createdAt: 'desc' },
  take: 10,
  skip: 0,
});

// Turbine
const users = await db.users.findMany({
  where: {
    role: 'admin',
    email: { contains: 'example.com' },
    age: { gte: 18 },
  },
  orderBy: { createdAt: 'desc' },
  limit: 10,  // "take" -> "limit"
  offset: 0,  // "skip" -> "offset"
});

Key difference: Prisma uses take/skip. Turbine uses limit/offset (Turbine also supports take with cursor pagination).

Nested Relations (include -> with)

TypeScript
// Prisma
const user = await prisma.user.findUnique({
  where: { id: 1 },
  include: {
    posts: {
      where: { published: true },
      orderBy: { createdAt: 'desc' },
      include: {
        comments: true,
      },
    },
  },
});

// Turbine
const user = await db.users.findUnique({
  where: { id: 1 },
  with: {             // "include" -> "with"
    posts: {
      where: { published: true },
      orderBy: { createdAt: 'desc' },
      with: {           // "include" -> "with"
        comments: true,
      },
    },
  },
});

Key difference: Prisma uses include. Turbine uses with. The nested structure is identical.

Performance difference: Prisma sends 3 separate SQL queries for this. Turbine sends 1.

create

TypeScript
// Prisma
const user = await prisma.user.create({
  data: {
    email: 'alice@example.com',
    name: 'Alice',
    orgId: 1,
  },
});

// Turbine (identical)
const user = await db.users.create({
  data: {
    email: 'alice@example.com',
    name: 'Alice',
    orgId: 1,
  },
});

createMany

TypeScript
// Prisma
const result = await prisma.user.createMany({
  data: [
    { email: 'a@b.com', name: 'A', orgId: 1 },
    { email: 'b@b.com', name: 'B', orgId: 1 },
  ],
  skipDuplicates: true,
});

// Turbine (identical API, but uses UNNEST internally)
const users = await db.users.createMany({
  data: [
    { email: 'a@b.com', name: 'A', orgId: 1 },
    { email: 'b@b.com', name: 'B', orgId: 1 },
  ],
  skipDuplicates: true,
});

Difference: Prisma returns { count: number }. Turbine returns the full array of created rows (T[]).

update

TypeScript
// Prisma
const user = await prisma.user.update({
  where: { id: 1 },
  data: { name: 'Updated' },
});

// Turbine (identical)
const user = await db.users.update({
  where: { id: 1 },
  data: { name: 'Updated' },
});

delete

TypeScript
// Prisma
const user = await prisma.user.delete({
  where: { id: 1 },
});

// Turbine (identical)
const user = await db.users.delete({
  where: { id: 1 },
});

Transactions

TypeScript
// Prisma
await prisma.$transaction(async (tx) => {
  const user = await tx.user.create({ data: { ... } });
  await tx.post.create({ data: { userId: user.id, ... } });
});

// Turbine
await db.$transaction(async (tx) => {
  const user = await tx.users.create({ data: { ... } });
  await tx.posts.create({ data: { userId: user.id, ... } });
});

Difference: Prisma uses singular model names (tx.user). Turbine uses table names (tx.users).

Raw SQL

TypeScript
// Prisma
const result = await prisma.$queryRaw`
  SELECT * FROM users WHERE org_id = ${orgId}
`;

// Turbine
const result = await db.raw<User>`
  SELECT * FROM users WHERE org_id = ${orgId}
`;

count

TypeScript
// Prisma
const count = await prisma.user.count({
  where: { orgId: 1 },
});

// Turbine (identical)
const count = await db.users.count({
  where: { orgId: 1 },
});

groupBy

TypeScript
// Prisma
const groups = await prisma.user.groupBy({
  by: ['role'],
  _count: true,
});

// Turbine (identical)
const groups = await db.users.groupBy({
  by: ['role'],
  _count: true,
});

API Differences Summary

| Concept | Prisma | Turbine | |---|---|---| | Model names | Singular (prisma.user) | Table names (db.users) | | Nested relations | include: { posts: true } | with: { posts: true } | | Pagination | take / skip | limit / offset | | Client init | new PrismaClient() | turbine({ connectionString }) | | Transaction | prisma.$transaction() | db.$transaction() | | Raw SQL | prisma.$queryRaw | db.raw | | Middleware | prisma.$use() | db.$use() | | Disconnect | prisma.$disconnect() | db.disconnect() | | Schema source | .prisma schema file | Live database introspection | | Type generation | npx prisma generate | npx turbine generate |


What Turbine Adds

Features you gain by switching:

  • Pipeline API -- batch N independent queries into 1 round-trip
  • Single-query nesting -- json_agg instead of N+1 queries
  • UNNEST batch inserts -- constant parameters regardless of batch size
  • No binary engine -- no Rust process, no WASM, no cold start penalty
  • Nested SAVEPOINTs -- nested transactions within $transaction()
  • select/omit on relations -- field selection on nested with clauses

Removing Prisma

Once you have verified Turbine works in your application:

Terminal
# Remove Prisma packages
npm uninstall @prisma/client prisma

# Remove Prisma files
rm -rf prisma/              # schema and migrations
rm -rf node_modules/.prisma # generated client

# Remove from .gitignore (if Prisma-specific entries exist)

Update any prisma generate scripts in your package.json to use turbine generate instead.


Keeping Both (Gradual Migration)

You can run Prisma and Turbine side by side during migration:

TypeScript
import { PrismaClient } from '@prisma/client';
import { turbine } from './generated/turbine';

const prisma = new PrismaClient();
const db = turbine({ connectionString: process.env.DATABASE_URL });

// Migrate one query at a time
// Old: const users = await prisma.user.findMany({ ... });
// New:
const users = await db.users.findMany({ ... });

Both clients connect to the same database. Migrate queries one at a time, verify results match, then remove the Prisma version.