You can connect Drizzle ORM and Prisma to your Next.js app and use them both, but it’s not a free lunch.

Let’s see Drizzle in action with a simple Next.js API route. Imagine we have a users table.

// db/schema.ts
import { pgTable, serial, text, timestamp } from 'drizzle-orm/pg-core';

export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  name: text('name').notNull(),
  email: text('email').notNull().unique(),
  createdAt: timestamp('created_at').defaultNow(),
});

Here’s how you’d query it in an API route:

// app/api/users/route.ts
import { sql } from '@vercel/postgres';
import { drizzle } from 'drizzle-orm/vercel-postgres';
import { users } from '@/db/schema';
import { eq } from 'drizzle-orm';

export async function GET() {
  const db = drizzle(sql);
  const allUsers = await db.select().from(users);
  return Response.json(allUsers);
}

export async function POST(request: Request) {
  const { name, email } = await request.json();
  const db = drizzle(sql);
  await db.insert(users).values({ name, email });
  return Response.json({ message: 'User created' });
}

Now, let’s layer in Prisma. Prisma’s strength is its auto-generated client and migrations. You’d define your schema like this:

// prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("POSTGRES_URL")
}

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

And then use the Prisma client in your Next.js app:

// app/users/page.tsx (example client component)
'use client';

import { useState, useEffect } from 'react';
import { PrismaClient } from '@prisma/client'; // Note: This is conceptual for client-side; usually server-side

const prisma = new PrismaClient(); // In a real Next.js app, this would be on the server

async function getUsers() {
  // In a real Next.js app, this function would be in an API route or server component
  const users = await prisma.user.findMany();
  return users;
}

export default function UsersPage() {
  const [users, setUsers] = useState([]);

  useEffect(() => {
    async function fetchUsers() {
      const fetchedUsers = await getUsers(); // This call needs to be to an API route
      setUsers(fetchedUsers);
    }
    fetchUsers();
  }, []);

  return (
    <div>
      <h1>Users</h1>
      <ul>
        {users.map((user) => (
          <li key={user.id}>{user.name} ({user.email})</li>
        ))}
      </ul>
    </div>
  );
}

The core problem Drizzle solves is providing a type-safe SQL builder that’s incredibly fast and lightweight, especially when you’re already working with a specific database driver like pg or @vercel/postgres. Prisma, on the other hand, offers a higher-level abstraction with automatic migrations, a powerful schema definition language, and a robust client that handles connections and pooling for you.

When you integrate them, you’re essentially choosing which tool to use for which job. A common pattern is to use Drizzle for your core database interactions where performance and direct SQL control are paramount, and then use Prisma for its migration capabilities and potentially for specific, simpler CRUD operations where the auto-generated client is convenient.

Here’s a more concrete example of how you might use them together. Suppose you want to manage your database schema with Prisma Migrations but use Drizzle for your API routes.

  1. Set up Prisma:

    • Run npm install prisma --save-dev
    • Run npx prisma init
    • Configure prisma/schema.prisma with your database connection string.
    • Define your User model as shown above.
    • Run npx prisma migrate dev --name init to create your tables.
  2. Set up Drizzle:

    • Run npm install drizzle-orm pg @vercel/postgres (or your chosen DB driver).
    • Define your Drizzle schema (db/schema.ts) to match your Prisma schema. This is crucial for type safety.
    • In your API routes or server components, initialize Drizzle using the same database connection that Prisma uses. For @vercel/postgres, this would look like:
    // lib/db.ts
    import { neon } from '@vercel/postgres';
    import { drizzle } from 'drizzle-orm/vercel-postgres';
    
    const queryClient = neon(process.env.POSTGRES_URL!);
    export const db = drizzle(queryClient);
    

    Then use db from lib/db.ts in your API routes.

The most surprising true thing about this topic is that you can, and often should, run both Drizzle and Prisma in the same Next.js application. Prisma handles the schema definition, migrations, and provides a client for easy access to your data. Drizzle, with its SQL-like syntax and minimal overhead, can be used for performance-critical queries or when you need more direct control over the SQL being executed, all while benefiting from the type safety provided by your shared schema definition.

The key to making this work is ensuring your Drizzle schema precisely mirrors your Prisma schema. Any discrepancy will lead to runtime errors or unexpected behavior. Use Prisma’s db push or migrate dev to manage the actual database structure, and then use that structure as the source of truth for your Drizzle schema definition.

When you are using Drizzle with @vercel/postgres and encounter a query that seems to be very slow, even though the SQL looks fine, it’s often because Drizzle is generating a query that the PostgreSQL planner isn’t optimizing well for your specific data distribution. In such cases, switching to a raw SQL query using sql from @vercel/postgres and then manually passing that to Drizzle’s db.execute() or db.query() can sometimes yield better performance, as you’re bypassing Drizzle’s query builder entirely and letting the database handle the execution plan.

The next logical step is exploring advanced Drizzle features like migrations or integrating a connection pool for better performance in serverless environments.

Want structured learning?

Take the full Nextjs course →