Learn Drizzle ORM in Next.js with Neon Postgres | CRUD Tutorial

Candra Kriswinarto
4 min readDec 19, 2024

--

Photo by Inna Safa on Unsplash

In this tutorial, we’ll build a full-stack Todo application using Next.js, Drizzle ORM, and Neon Postgres. We’ll implement complete CRUD (Create, Read, Update, Delete) operations and learn how to set up and use these powerful tools together.

Prerequisites

Before we begin, make sure you have Node.js and npm installed on your system. We’ll be using Next.js with the App Router.

Step 1: Setting Up the Project

1.1 Create a New Next.js Project

First, create a new Next.js project with TypeScript support:

npx create-next-app@latest

1.2 Install Required Dependencies

Install Drizzle ORM and Drizzle Kit:

npm i drizzle-orm
npm i -D drizzle-kit

Install the Neon serverless driver:

npm i @neondatabase/serverless

Install dotenv for environment variables:

npm i dotenv

Step 2: Setting Up Neon Database

  1. Visit Neon and create an account
  2. Create a new project
  3. Copy your database connection URL
  4. Create a .env file in your project root and add:
DATABASE_URL=your-database-url

Step 3: Configuring Drizzle

3.1 Database Connection Setup

Create src/db/drizzle.ts:

import { config } from "dotenv";
import { drizzle } from "drizzle-orm/neon-http";

config({ path: ".env" });
export const db = drizzle(process.env.DATABASE_URL!);

This file establishes the connection to your Neon database using Drizzle ORM. The drizzle function creates a database instance that we'll use for all our database operations.

3.2 Schema Definition

Create src/db/schema.ts:

import {
integer,
text,
boolean,
pgTable,
timestamp,
} from "drizzle-orm/pg-core";

export const todo = pgTable("todo", {
id: integer("id").primaryKey(),
text: text("text").notNull(),
done: boolean("done").default(false).notNull(),
createdAt: timestamp("created_at").notNull().defaultNow(),
});

This schema defines our Todo table structure:

  • id: Integer primary key
  • text: Required text field for the todo content
  • done: Boolean field with a default value of false
  • createdAt: Timestamp field that automatically sets to current time

3.3 Drizzle Configuration

Create drizzle.config.ts in your project root:

import { config } from "dotenv";
import { defineConfig } from "drizzle-kit";

config({ path: ".env" });
export default defineConfig({
schema: "./src/db/schema.ts",
out: "./migrations",
dialect: "postgresql",
dbCredentials: {
url: process.env.DATABASE_URL!,
},
});

This configuration file tells Drizzle:

  • Where to find your schema (./src/db/schema.ts)
  • Where to output migrations (./migrations)
  • Which database dialect to use (postgresql)
  • Database credentials from your environment variables

Step 4: Database Migrations

Generate and apply your database migrations:

# Generate migrations
npx drizzle-kit generate

# Apply migrations (or use push for development)
npx drizzle-kit push

Step 5: Implementing Server Actions

Create src/actions/todo-actions.ts:

'use server'

import { db } from "@/db/drizzle";
import { todo } from "@/db/schema";
import { eq, not } from "drizzle-orm";
import { revalidatePath } from "next/cache";
export const getTodos = async () => {
const data = await db.select().from(todo).orderBy(todo.createdAt);
return data;
}
export const addTodo = async (id: number, text: string) => {
await db.insert(todo).values({
id: id,
text: text,
});
revalidatePath('/')
}
export const deleteTodo = async (id: number) => {
await db.delete(todo).where(eq(todo.id, id));
revalidatePath("/");
};
export const editTodo = async (id: number, text: string) => {
await db
.update(todo)
.set({
text: text,
})
.where(eq(todo.id, id));
revalidatePath("/");
};
export const toggleTodo = async (id: number) => {
await db
.update(todo)
.set({
done: not(todo.done),
})
.where(eq(todo.id, id));
revalidatePath("/");
};

These server actions provide our CRUD operations:

  • getTodos: Retrieves all todos
  • addTodo: Creates a new todo
  • deleteTodo: Removes a todo
  • editTodo: Updates todo text
  • toggleTodo: Toggles todo completion status

Step 6: Creating the UI Components

6.1 Main Page Component

Update src/app/page.tsx:

import { getTodos } from '@/actions/todo-actions';
import Todos from '@/components/todos';

export default async function Home() {
const todos = await getTodos();
return (
<div className='p-4'>
<Todos todos={todos} />
</div>
);
}

6.2 Todos Component

Create src/components/todos.tsx:

'use client';

import { todoType } from '@/types/todo-type';
import AddTodo from './add-todo';
import {
addTodo,
deleteTodo,
editTodo,
toggleTodo,
} from '@/actions/todo-actions';
interface TodosProps {
todos: todoType[];
}
export default function Todos({ todos }: TodosProps) {
const handleAddTodo = async () => {
const id = Math.floor(1000 + Math.random() * 9000);
const text = `Todo ${id}`;
await addTodo(id, text);
};
const handleDeleteTodo = async (id: number) => {
await deleteTodo(id);
};
const handleEditTodo = async (id: number) => {
await editTodo(id, 'todo edited');
};
const handleToggleTodo = async (id: number) => {
await toggleTodo(id);
};
return (
<div>
<AddTodo handleAddTodo={handleAddTodo} />
<ul className='space-y-2'>
{todos.map((todo) => (
<li
key={todo.id}
className={`flex justify-between items-center p-2 border rounded ${
todo.done ? 'bg-green-100' : 'bg-red-100'
}`}
>
<span>{todo.text}</span>
<div className='flex space-x-2'>
<button
onClick={() => handleEditTodo(todo.id)}
className='bg-yellow-500 text-white px-2 py-1 rounded hover:bg-yellow-600'
>
Edit
</button>
<button
onClick={() => handleToggleTodo(todo.id)}
className='bg-green-500 text-white px-2 py-1 rounded hover:bg-green-600'
>
{todo.done ? 'Undo' : 'Done'}
</button>
<button
onClick={() => handleDeleteTodo(todo.id)}
className='bg-red-500 text-white px-2 py-1 rounded hover:bg-red-600'
>
Delete
</button>
</div>
</li>
))}
</ul>
</div>
);
}

6.3 Add Todo Component

Create src/components/add-todo.tsx:

'use client';

interface AddTodoProps {
handleAddTodo: () => void;
}
export default function AddTodo({ handleAddTodo }: AddTodoProps) {
return (
<div className='mb-4'>
<button
onClick={handleAddTodo}
className='bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600'
>
Add Todo
</button>
</div>
);
}

Testing the Application

  1. Start your Next.js development server:
npm run dev
  1. Visit http://localhost:3000
  2. Test the CRUD operations:
  • Create new todos using the “Add Todo” button
  • Toggle todo completion status
  • Edit todo text
  • Delete todos

3. You can verify the data changes in your Neon database console by visiting your project dashboard and checking the todo table.

prefer video content? here.

--

--

Candra Kriswinarto
Candra Kriswinarto

Written by Candra Kriswinarto

👋 Front-End Developer & YouTuber. I write about web dev, share coding tips, and create tutorials to help others learn. https://youtube.com/@CandDev

No responses yet