Learn Drizzle ORM in Next.js with Neon Postgres | CRUD Tutorial
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
- Visit Neon and create an account
- Create a new project
- Copy your database connection URL
- 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 keytext
: Required text field for the todo contentdone
: Boolean field with a default value of falsecreatedAt
: 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 todosaddTodo
: Creates a new tododeleteTodo
: Removes a todoeditTodo
: Updates todo texttoggleTodo
: 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
- Start your Next.js development server:
npm run dev
- Visit
http://localhost:3000
- 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.