Skip to main content
Back to Blog
Tutorials
3 min read
December 2, 2024

How to Set Up a PostgreSQL Database with Drizzle ORM in Next.js

Connect a PostgreSQL database to your Next.js app using Drizzle ORM. Type-safe queries, migrations, and schema management with zero overhead.

Ryel Banfield

Founder & Lead Developer

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):

  1. Create a Neon account
  2. Create a new project
  3. 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.

PostgreSQLDrizzle ORMdatabaseNext.jstutorial

Ready to Start Your Project?

RCB Software builds world-class websites and applications for businesses worldwide.

Get in Touch

Related Articles