Skip to main content
Migrating from Prisma to Drizzle ORM is straightforward and can be done incrementally. This guide covers the migration process, API differences, and how to preserve your existing data.

Why Migrate to Drizzle?

Developers choose Drizzle over Prisma for several reasons:

Type Safety

Drizzle provides true TypeScript type inference without code generation

SQL-like API

Write queries that look like SQL but with full type safety

Zero Dependencies

Minimal bundle size with no runtime dependencies

Edge Compatible

Works in Cloudflare Workers, Vercel Edge, and other edge runtimes

Performance

No query engine overhead, direct database driver access

SQL Control

Full control over generated SQL with raw query support

Migration Strategies

Run Prisma and Drizzle together during migration:
1

Install Drizzle

npm install drizzle-orm
npm install -D drizzle-kit
2

Generate Drizzle schema from Prisma

Use Drizzle Kit to introspect your existing database:
npx drizzle-kit introspect
This creates a Drizzle schema matching your current database structure.
3

Migrate routes incrementally

Convert one route/feature at a time from Prisma to Drizzle:
// Before: Prisma
const users = await prisma.user.findMany();

// After: Drizzle
const users = await db.select().from(usersTable);
4

Remove Prisma when complete

Once all code is migrated, uninstall Prisma:
npm uninstall prisma @prisma/client

Strategy 2: Fresh Start

Start a new project with Drizzle and migrate data:
  1. Create new Drizzle schema from scratch
  2. Export data from Prisma database
  3. Import data into new Drizzle-managed database
  4. Update application code

Schema Conversion

Prisma Schema to Drizzle

Here’s how Prisma schema elements map to Drizzle:

Basic Model

model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  name      String?
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

Enums

enum Role {
  USER
  ADMIN
}

model User {
  id   Int  @id @default(autoincrement())
  role Role @default(USER)
}

Relations

model User {
  id    Int    @id @default(autoincrement())
  posts Post[]
}

model Post {
  id       Int    @id @default(autoincrement())
  title    String
  userId   Int
  user     User   @relation(fields: [userId], references: [id])
}

Many-to-Many Relations

model Post {
  id         Int        @id @default(autoincrement())
  categories Category[]
}

model Category {
  id    Int    @id @default(autoincrement())
  posts Post[]
}

Query API Comparison

Finding Records

// Find all
const users = await prisma.user.findMany();

// Find with where clause
const activeUsers = await prisma.user.findMany({
  where: { active: true },
});

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

// Find first
const firstUser = await prisma.user.findFirst({
  where: { active: true },
});

Creating Records

const user = await prisma.user.create({
  data: {
    email: '[email protected]',
    name: 'John',
  },
});

// Create many
await prisma.user.createMany({
  data: [
    { email: '[email protected]', name: 'John' },
    { email: '[email protected]', name: 'Jane' },
  ],
});

Updating Records

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

// Update many
await prisma.user.updateMany({
  where: { active: false },
  data: { deleted: true },
});

Deleting Records

await prisma.user.delete({
  where: { id: 1 },
});

await prisma.user.deleteMany({
  where: { active: false },
});

Filtering

// Equals
const users = await prisma.user.findMany({
  where: { role: 'ADMIN' },
});

// Not equals
const users = await prisma.user.findMany({
  where: { role: { not: 'ADMIN' } },
});

// In array
const users = await prisma.user.findMany({
  where: { role: { in: ['ADMIN', 'MODERATOR'] } },
});

// Greater than
const users = await prisma.user.findMany({
  where: { age: { gt: 18 } },
});

// Contains (string)
const users = await prisma.user.findMany({
  where: { name: { contains: 'John' } },
});

Combining Filters

// AND
const users = await prisma.user.findMany({
  where: {
    AND: [
      { active: true },
      { role: 'ADMIN' },
    ],
  },
});

// OR
const users = await prisma.user.findMany({
  where: {
    OR: [
      { role: 'ADMIN' },
      { role: 'MODERATOR' },
    ],
  },
});

Selecting Specific Fields

const users = await prisma.user.findMany({
  select: {
    id: true,
    name: true,
    email: true,
  },
});

Relations and Joins

const users = await prisma.user.findMany({
  include: {
    posts: true,
  },
});

// Nested includes
const users = await prisma.user.findMany({
  include: {
    posts: {
      include: {
        comments: true,
      },
    },
  },
});

Sorting and Pagination

const users = await prisma.user.findMany({
  orderBy: { createdAt: 'desc' },
  skip: 10,
  take: 20,
});

Aggregations

const result = await prisma.user.aggregate({
  _count: true,
  _avg: { age: true },
  _sum: { age: true },
});

Transactions

await prisma.$transaction(async (tx) => {
  await tx.user.create({ data: { name: 'John' } });
  await tx.post.create({ data: { title: 'Post' } });
});

Data Migration

Preserving Existing Data

Your data stays in the database during migration. Only your application code changes.
1

Introspect existing database

npx drizzle-kit introspect
This generates a Drizzle schema matching your current Prisma database structure.
2

Review generated schema

Check drizzle/schema.ts and adjust as needed. The introspection tool maps:
  • Table names
  • Column types
  • Constraints
  • Indexes
  • Foreign keys
3

Test queries

Write equivalent Drizzle queries and verify they return the same data:
// Test script
const prismaUsers = await prisma.user.findMany();
const drizzleUsers = await db.select().from(usersTable);

console.log('Match:', JSON.stringify(prismaUsers) === JSON.stringify(drizzleUsers));

Handling Prisma-Specific Features

@updatedAt

Prisma’s @updatedAt automatically updates timestamps. In Drizzle:
import { timestamp } from 'drizzle-orm/pg-core';

export const users = pgTable('User', {
  updatedAt: timestamp('updatedAt')
    .notNull()
    .defaultNow()
    .$onUpdate(() => new Date()),
});

@default(uuid())

import { uuid } from 'drizzle-orm/pg-core';

export const users = pgTable('User', {
  id: uuid('id').defaultRandom().primaryKey(),
});

@default(cuid())

Drizzle doesn’t have built-in CUID. Use a library:
import { text } from 'drizzle-orm/pg-core';
import { createId } from '@paralleldrive/cuid2';

export const users = pgTable('User', {
  id: text('id').$defaultFn(() => createId()).primaryKey(),
});

Drizzle Kit vs Prisma Migrate

FeaturePrisma MigrateDrizzle Kit
Auto-generate migrationsYesYes
Push schema without migrationsdb pushdrizzle-kit push
Migration history_prisma_migrations table__drizzle_migrations table
Introspectionprisma db pulldrizzle-kit introspect
Studio (GUI)Prisma StudioDrizzle Studio

Migration Commands

# Generate migration
prisma migrate dev --name add_users

# Apply migrations
prisma migrate deploy

# Reset database
prisma migrate reset

Common Pitfalls

No automatic ID generation: Unlike Prisma’s @default(autoincrement()), Drizzle requires explicit specification:
// Use serial() for auto-increment
id: serial('id').primaryKey()

// Or uuid()
id: uuid('id').defaultRandom().primaryKey()
Explicit table names: Drizzle uses the first argument as table name:
// This creates a table named "User" (matches Prisma)
export const users = pgTable('User', { ... });

// Variable name doesn't affect table name
export const usersTable = pgTable('users', { ... });
Relations are optional: Foreign keys work without defining relations. Relations are only needed for the relational query API:
// This works fine
export const posts = pgTable('posts', {
  userId: integer('userId').references(() => users.id),
});

// Relations only needed for db.query.posts.findMany({ with: ... })
export const postsRelations = relations(posts, ({ one }) => ({ ... }));

Type Inference Differences

Drizzle provides direct type inference without code generation:
// Prisma: Generated in node_modules/.prisma/client
type User = Prisma.User;

// Drizzle: Inferred from schema
import { users } from './schema';

type User = typeof users.$inferSelect;
type NewUser = typeof users.$inferInsert;

Performance Comparison

Drizzle typically performs better than Prisma because:
  1. No query engine: Direct database driver usage
  2. Smaller bundle: No Prisma engines to download
  3. Prepared statements: Built-in support reduces parsing overhead
  4. Edge runtime: Works in Cloudflare Workers, Vercel Edge
Benchmark example (Next.js API route):
// Prisma: ~150ms cold start
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();

// Drizzle: ~50ms cold start
import { drizzle } from 'drizzle-orm/neon-http';
const db = drizzle({ connection: process.env.DATABASE_URL! });

Complete Example: Before & After

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

const prisma = new PrismaClient();

export async function getUsers(role: string) {
  return await prisma.user.findMany({
    where: { role },
    include: {
      posts: {
        orderBy: { createdAt: 'desc' },
        take: 10,
      },
    },
    orderBy: { name: 'asc' },
  });
}

export async function createUser(data: {
  email: string;
  name: string;
  role: string;
}) {
  return await prisma.user.create({ data });
}

Next Steps

Schema Design

Learn Drizzle schema syntax in depth

Query API

Master Drizzle’s query builder

Best Practices

Optimize your Drizzle implementation

Relational Queries

Use the relational query API

Build docs developers (and LLMs) love