Drizzle ORM is a lightweight, type-safe SQL ORM for TypeScript. It generates SQL that maps directly to your TypeScript schema with zero runtime overhead. Here is how to set it up with Neon PostgreSQL and Next.js.
Step 1: Create a Database
Use Neon for serverless PostgreSQL (free tier available):
- Create a Neon account
- Create a new project
- Copy the connection string
# .env.local
DATABASE_URL=postgresql://username:password@ep-xxx.us-east-2.aws.neon.tech/mydb?sslmode=require
Step 2: Install Dependencies
pnpm add drizzle-orm @neondatabase/serverless
pnpm add -D drizzle-kit
Step 3: Configure Drizzle
// drizzle.config.ts
import { defineConfig } from "drizzle-kit";
export default defineConfig({
schema: "./src/db/schema.ts",
out: "./drizzle",
dialect: "postgresql",
dbCredentials: {
url: process.env.DATABASE_URL!,
},
});
Step 4: Define Your Schema
// src/db/schema.ts
import { pgTable, text, timestamp, integer, boolean, uuid } from "drizzle-orm/pg-core";
export const users = pgTable("users", {
id: uuid("id").defaultRandom().primaryKey(),
name: text("name").notNull(),
email: text("email").notNull().unique(),
createdAt: timestamp("created_at").defaultNow().notNull(),
});
export const posts = pgTable("posts", {
id: uuid("id").defaultRandom().primaryKey(),
title: text("title").notNull(),
content: text("content").notNull(),
published: boolean("published").default(false).notNull(),
authorId: uuid("author_id")
.notNull()
.references(() => users.id),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull(),
});
export const comments = pgTable("comments", {
id: uuid("id").defaultRandom().primaryKey(),
content: text("content").notNull(),
postId: uuid("post_id")
.notNull()
.references(() => posts.id, { onDelete: "cascade" }),
authorId: uuid("author_id")
.notNull()
.references(() => users.id),
createdAt: timestamp("created_at").defaultNow().notNull(),
});
Step 5: Create the Database Client
// src/db/index.ts
import { drizzle } from "drizzle-orm/neon-http";
import { neon } from "@neondatabase/serverless";
import * as schema from "./schema";
const sql = neon(process.env.DATABASE_URL!);
export const db = drizzle(sql, { schema });
For connection pooling (recommended for serverless):
// src/db/index.ts
import { drizzle } from "drizzle-orm/neon-serverless";
import { Pool } from "@neondatabase/serverless";
import * as schema from "./schema";
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
export const db = drizzle(pool, { schema });
Step 6: Run Migrations
Generate and run migrations:
# Generate migration from schema changes
pnpm drizzle-kit generate
# Apply migrations to the database
pnpm drizzle-kit migrate
# Or push schema directly (development only)
pnpm drizzle-kit push
Add scripts to package.json:
{
"scripts": {
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:push": "drizzle-kit push",
"db:studio": "drizzle-kit studio"
}
}
Step 7: Query Data
Select
import { db } from "@/db";
import { users, posts } from "@/db/schema";
import { eq, desc } from "drizzle-orm";
// Get all users
const allUsers = await db.select().from(users);
// Get user by email
const user = await db
.select()
.from(users)
.where(eq(users.email, "user@example.com"))
.limit(1);
// Get published posts with author, sorted by newest
const publishedPosts = await db
.select({
id: posts.id,
title: posts.title,
authorName: users.name,
createdAt: posts.createdAt,
})
.from(posts)
.innerJoin(users, eq(posts.authorId, users.id))
.where(eq(posts.published, true))
.orderBy(desc(posts.createdAt));
Insert
// Insert a single row
const newUser = await db
.insert(users)
.values({
name: "Sarah Johnson",
email: "sarah@example.com",
})
.returning();
// Insert multiple rows
await db.insert(posts).values([
{ title: "First Post", content: "Hello world", authorId: newUser[0].id },
{ title: "Second Post", content: "More content", authorId: newUser[0].id },
]);
Update
await db
.update(posts)
.set({ published: true, updatedAt: new Date() })
.where(eq(posts.id, postId));
Delete
await db.delete(comments).where(eq(comments.postId, postId));
Step 8: Use in Server Components
// app/posts/page.tsx
import { db } from "@/db";
import { posts, users } from "@/db/schema";
import { eq, desc } from "drizzle-orm";
export default async function PostsPage() {
const allPosts = await db
.select({
id: posts.id,
title: posts.title,
authorName: users.name,
createdAt: posts.createdAt,
})
.from(posts)
.innerJoin(users, eq(posts.authorId, users.id))
.where(eq(posts.published, true))
.orderBy(desc(posts.createdAt));
return (
<main>
<h1>Posts</h1>
{allPosts.map((post) => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>By {post.authorName}</p>
</article>
))}
</main>
);
}
Step 9: Use in Server Actions
// app/actions.ts
"use server";
import { db } from "@/db";
import { posts } from "@/db/schema";
import { revalidatePath } from "next/cache";
import { z } from "zod";
const createPostSchema = z.object({
title: z.string().min(1).max(200),
content: z.string().min(1),
});
export async function createPost(formData: FormData) {
const data = createPostSchema.parse({
title: formData.get("title"),
content: formData.get("content"),
});
await db.insert(posts).values({
title: data.title,
content: data.content,
authorId: "user-id-from-auth", // Get from your auth provider
});
revalidatePath("/posts");
}
Step 10: Drizzle Studio
Explore your database with a visual interface:
pnpm db:studio
Opens a browser-based database explorer where you can view and edit data.
Type Safety
The biggest advantage of Drizzle: every query is fully typed. TypeScript catches errors at compile time:
// TypeScript error: 'nonExistentColumn' does not exist
await db.select().from(users).where(eq(users.nonExistentColumn, "value"));
// TypeScript error: number is not assignable to string
await db.insert(users).values({ name: 123, email: "test@test.com" });
Need Database Development?
We design and implement database architectures for web and mobile applications. Contact us for backend development services.