Why Drizzle?
Drizzle has no query engine binary, making it ideal for edge runtimes. Its schema definition is pure TypeScript and the query API mirrors SQL closely, making it easy to reason about what's happening in your database.
Drizzle ORM — A Lighter Alternative
While traditional ORMs offer robust abstractions, they often ship with heavy runtime engines that increase serverless cold start times and lack compatibility with Edge environments. Drizzle ORM is a lightweight, TypeScript-first database driver wrapper that operates with near-zero runtime overhead. Drizzle maps schemas using pure TypeScript objects, supports SQL-like querying syntax, and runs natively inside Next.js Edge Middleware and Server Actions. In this lesson, we will explore how to declare TypeScript schemas, run migrations using Drizzle Kit, execute queries, and compare its performance to Prisma.
1. Installing Drizzle ORM and Drizzle Kit
To set up Drizzle ORM with a serverless Postgres backend (like Neon), install the ORM package and the development CLI:
npm install drizzle-orm @neondatabase/serverless
npm install -D drizzle-kit dotenv
Here:
drizzle-ormis the runtime query builder.@neondatabase/serverlessis the serverless PostgreSQL connection driver.drizzle-kitis the CLI for schema synchronization and migrations generation.
2. Declaring Schemas in Pure TypeScript
With Drizzle, there is no custom schema language. You write schemas using pure TypeScript. This allows you to export types directly from your table objects:
// db/schema.ts
import { pgTable, serial, text, timestamp, integer } from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';
// Define the users table schema
export const users = pgTable('users', {
id: serial('id').primaryKey(),
email: text('email').notNull().unique(),
name: text('name'),
createdAt: timestamp('created_at').defaultNow().notNull(),
});
// Define the posts table schema
export const posts = pgTable('posts', {
id: serial('id').primaryKey(),
title: text('title').notNull(),
content: text('content'),
authorId: integer('author_id').references(() => users.id, { onDelete: 'cascade' }),
createdAt: timestamp('created_at').defaultNow().notNull(),
});
// Declare relational linkages explicitly
export const usersRelations = relations(users, ({ many }) => ({
posts: many(posts),
}));
export const postsRelations = relations(posts, ({ one }) => ({
author: one(users, {
fields: [posts.authorId],
references: [users.id],
}),
}));
3. Configuring Drizzle Kit and Running Migrations
Create a Drizzle Kit configuration file in your project root to handle schema folders and migration logs:
// drizzle.config.ts
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
schema: './db/schema.ts',
out: './drizzle',
dialect: 'postgresql',
dbCredentials: {
url: process.env.DATABASE_URL || '',
},
});
Generate SQL migration scripts by running:
npx drizzle-kit generate
To push schema changes directly to your database (ideal for quick prototyping without SQL migration files), run:
npx drizzle-kit push
4. Database Client Instantiation (Edge Compatible)
Because Drizzle does not rely on a compiled Rust engine binary, you can instantiate the connection client natively on the Edge runtime using serverless drivers:
// db/index.ts
import { neon } from '@neondatabase/serverless';
import { drizzle } from 'drizzle-orm/neon-http';
import * as schema from './schema';
const sql = neon(process.env.DATABASE_URL || '');
export const db = drizzle(sql, { schema });
5. Fetching Data in Server Components
Drizzle provides two querying styles: the traditional SQL query builder, and the simplified Relational Query API:
// app/posts/page.tsx
import { db } from '@/db';
import { posts } from '@/db/schema';
import { desc, eq } from 'drizzle-orm';
export default async function PostsPage() {
// Option A: SQL-like Query Builder
const sqlPosts = await db
.select({
id: posts.id,
title: posts.title,
})
.from(posts)
.where(eq(posts.authorId, 1))
.orderBy(desc(posts.createdAt));
// Option B: Relational Query API (Autocompleted and Nested)
const relationalPosts = await db.query.posts.findMany({
with: {
author: true,
},
});
return (
<main className="p-6">
<h1 className="text-2xl font-bold mb-4">Course Lessons</h1>
<ul>
{relationalPosts.map((post) => (
<li key={post.id} className="mb-2">
{post.title} - By {post.author?.name || 'Unknown'}
</li>
))}
</ul>
</main>
);
}
6. Data Mutations inside Server Actions
Drizzle handles mutations with clean helper methods. TypeScript will automatically validate parameters against your schema models:
// app/actions/auth-actions.ts
'use server';
import { db } from '@/db';
import { users } from '@/db/schema';
import { revalidatePath } from 'next/cache';
export async function registerUser(email: string, name: string) {
if (!email) throw new Error('Email is required.');
await db.insert(users).values({
email,
name,
});
revalidatePath('/admin/users');
}
7. Prisma vs. Drizzle: Performance Trade-offs
| Feature | Prisma ORM | Drizzle ORM |
|---|---|---|
| Schema Definition | Custom DSL (schema.prisma) |
Pure TypeScript (schema.ts) |
| Edge Runtime | Requires proxy server extensions | Native Edge compatible |
| Cold Starts | Higher (due to Rust engine startup) | Minimal / Negligible |
| Query Style | JSON-based queries | SQL-like / Relational queries |
8. Mitigating SQL Injection Risks
Because Drizzle closely mirrors raw SQL syntax, developers might feel tempted to concatenate query strings manually. Never concatenate variables directly into SQL queries:
// BAD: Vulnerable to SQL Injection
import { sql } from 'drizzle-orm';
db.execute(sql`SELECT * FROM users WHERE name = '${userInput}'`);
// GOOD: Safe parameterized query
import { sql } from 'drizzle-orm';
db.execute(sql`SELECT * FROM users WHERE name = ${userInput}`);
9. Common Gotchas
-
Handling Schema Relational Linkages: In Prisma, relations are automatically created in code when you model them in tables. In Drizzle, you must define relations using the
relations()helper helper function separately, otherwise the Relational Query API will throw compilation errors. - Lack of Automatic Type Generation inside JS Files: Since Drizzle uses TypeScript interfaces directly, make sure your tsconfig or build engines resolve types properly, as compiling without TypeScript can skip validations.
-
Raw SQL Type Mismatches: When using the raw query builder (
db.execute()), the returned values are not typed by default. You must assert the types manually.
Key Takeaways
- Drizzle ORM maps databases using pure TypeScript, eliminating custom markup languages.
- Migrations are managed using Drizzle Kit to output standard, readable SQL scripts.
- Drizzle does not use binary engines, allowing queries to execute natively inside Next.js Edge Middleware.
- Use the Relational Query API (
db.query) for autocompleted nested fetches. - Always use parameterized inputs (SQL templating helpers) to protect queries from SQL injection.
Mastering database integration completes the server-side persistence layer of your Next.js application. But a dynamic application must expose data to other systems and validate webhook events. In the next module, we explore **API Routes & Route Handlers** with a lesson on **Route Handlers (app/api)**, learning how to configure custom endpoints inside the App Router.