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
npm install turbine-orm
2. Initialize
npx turbine init
This creates turbine.config.ts. Set your DATABASE_URL:
import type { TurbineCliConfig } from 'turbine-orm/cli';
const config: TurbineCliConfig = {
url: process.env.DATABASE_URL,
out: './generated/turbine',
};
export default config;
3. Generate Types
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):
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
After (Turbine):
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
// 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
// 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)
// 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
// 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
// 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
// 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
// Prisma
const user = await prisma.user.delete({
where: { id: 1 },
});
// Turbine (identical)
const user = await db.users.delete({
where: { id: 1 },
});
Transactions
// 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
// 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
// Prisma
const count = await prisma.user.count({
where: { orgId: 1 },
});
// Turbine (identical)
const count = await db.users.count({
where: { orgId: 1 },
});
groupBy
// 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
withclauses
Removing Prisma
Once you have verified Turbine works in your application:
# 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:
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.