Todo App

This tutorial demonstrates how to build Todo app using Drizzle ORM with Neon database and Next.js.

Backend

Install Drizzle Kit and Drizzle ORM

The first step is to installΒ Drizzle KitΒ and Drizzle ORM in your project.

npm i drizzle-orm @neondatabase/serverless
npm i -D drizzle-kit

Here, we’re installing the necessary packages: drizzle-orm for database interactions and @neondatabase/serverless for working with the Neon database. The drizzle-kit package is used as a dev-dependency for handling configuration and migrations.

Connect Drizzle ORM to the Neon database

db/drizzle.ts
import { neon } from '@neondatabase/serverless';
import { drizzle } from 'drizzle-orm/neon-http';

const sql = neon(process.env.NEON_DATABASE_URL!);

const db = drizzle(sql);

export default db;

In this snippet, we import functionality from @neondatabase/serverless and drizzle-orm, establish a connection to the Neon database, and initialize Drizzle.

Setup Drizzle config file

Then, set up the Drizzle configuration. Drizzle config is a configuration file used by Drizzle Kit. It contains all the information about your database connection, migration folder, and schema files.

drizzle.config.ts
import "dotenv/config";
import type {Config} from "drizzle-kit";

export default {
  schema: "./db/schema.ts",
  out: "./drizzle",
  driver: "pg",
  dbCredentials: {
    connectionString: process.env.NEON_DATABASE_URL!,
  },
} satisfies Config;

This configuration file specifies the database schema location, path for generating migrations, and database connection through the connection string.

Declare todos schema

db/schema.ts
import { integer, text, boolean, pgTable } from "drizzle-orm/pg-core";

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

Here we define the todo table with fields id, text, and done, using data types from Drizzle ORM.

Push your schema changes directly to the database without generating any migrations files using drizzle-kit push:pg command:

npx drizzle-kit push:pg

Now, we’re ready to write queries using Drizzle ORM.

Functions

  1. Add todo: The addTodo function inserts a new record into the todo table.
  2. Get todos: Here, we fetch all todos, sorted by their identifier.
  3. Edit todo: The editTodo function updates the text of a todo by its identifier.
  4. Toggle todo: This method toggles the status of a todo to its opposite state.
  5. Delete todo: The deleteTodo function removes a todo by its identifier.
Add todo
Get todos
Edit todo
Toggle todo
Delete todo
actions/todoActions.ts
import db from "@/db/drizzle";
import { todo } from "@/db/schema";

export const addTodo = async (id: number, text: string) => {
  await db.insert(todo).values({
    id: id,
    text: text,
  });
};
actions/todoActions.ts
import db from "@/db/drizzle";
import { todo } from "@/db/schema";
import { asc } from 'drizzle-orm';

export const getData = async () => {
  const data = await db.select().from(todo).orderBy(asc(todo.id));

  return data;
};
actions/todoActions.ts
import db from "@/db/drizzle";
import { todo } from "@/db/schema";
import { eq } from 'drizzle-orm';

export const editTodo = async (id: number, text: string) => {
  await db
    .update(todo)
    .set({
      text: text,
    })
    .where(eq(todo.id, id));
};
actions/todoActions.ts
import db from "@/db/drizzle";
import { todo } from "@/db/schema";
import { eq, not } from 'drizzle-orm';

export const toggleTodo = async (id: number) => {
  await db
    .update(todo)
    .set({
      done: not(todo.done),
    })
    .where(eq(todo.id, id));
};
actions/todoActions.ts
import db from "@/db/drizzle";
import { todo } from "@/db/schema";
import { eq } from 'drizzle-orm';

export const deleteTodo = async (id: number) => {
  await db.delete(todo).where(eq(todo.id, id));
};

Frontend

Initiate Next.js app with Tailwind CSS

Initiate a new Next.js application with Tailwind CSS support using the npx create-next-app@latest command. Name your project when prompted, navigate to the project directory, and start the development server with npm run dev. Your Next.js app is now set up for customization and styling with Tailwind CSS.

npx create-next-app@latest

Define a TypeScript type

Define a TypeScript type for a todo item with three properties: id of type number, text of type string, and done of type boolean. This type, named todoType, represents the structure of a typical todo item within your application.

export type todoType = {
  id: number;
  text: string;
  done: boolean;
};

Create a home page for a to-do application

import {getData} from "@/actions/todoActions";
import AddTodo from "@/components/AddTodo";
import Todo from "@/components/Todo";

export default async function Home() {
  const data = await getData();
  return <Todos todos={data} />;
}
  1. Todos.tsx: Create Todos components that represents the main interface of a Todo app. It manages the state of todo items, provides functions for creating, editing, toggling, and deleting todos, and renders the individual todo items using the Todo component.
  2. Todo.tsx: Create a Todo component that represents a single todo item. It includes features for displaying and editing the todo text, marking it as done with a checkbox, and providing actions for editing, saving, canceling, and deleting the todo.
  3. AddTodo: The AddTodo component provides a simple form for adding new todo items to the Todo app. It includes an input field for entering the todo text and a button for triggering the addition of the new todo.
Todos.tsx
Todo.tsx
AddTodo.tsx
"use client";
import { FC, useState } from "react";
import { todoType } from "@/types/todoType";
import Todo from "./Todo";
import AddTodo from "./AddTodo";
import { addTodo, deleteTodo, editTodo, toggleTodo } from "@/actions/todoActions";

interface Props {
  todos: todoType[];
}

const Todos: FC<Props> = ({ todos }) => {
  // State to manage the list of todo items
  const [todoItems, setTodoItems] = useState<todoType[]>(todos);

  // Function to create a new todo item
  const createTodo = (text: string) => {
    const id = (todoItems.at(-1)?.id || 0) + 1;
    addTodo(id, text);
    setTodoItems((prev) => [...prev, { id: id, text, done: false }]);
  };

  // Function to change the text of a todo item
  const changeTodoText = (id: number, text: string) => {
    setTodoItems((prev) =>
      prev.map((todo) => (todo.id === id ? { ...todo, text } : todo))
    );
    editTodo(id, text);
  };

  // Function to toggle the "done" status of a todo item
  const toggleIsTodoDone = (id: number) => {
    setTodoItems((prev) =>
      prev.map((todo) => (todo.id === id ? { ...todo, done: !todo.done } : todo))
    );
    toggleTodo(id);
  };

  // Function to delete a todo item
  const deleteTodoItem = (id: number) => {
    setTodoItems((prev) => prev.filter((todo) => todo.id !== id));
    deleteTodo(id);
  };

  // Rendering the Todo List component
  return (
    <main className="flex mx-auto max-w-xl w-full min-h-screen flex-col items-center p-16">
      <div className="text-5xl font-medium">To-do app</div>
      <div className="w-full flex flex-col mt-8 gap-2">
        {/* Mapping through todoItems and rendering Todo component for each */}
        {todoItems.map((todo) => (
          <Todo
            key={todo.id}
            todo={todo}
            changeTodoText={changeTodoText}
            toggleIsTodoDone={toggleIsTodoDone}
            deleteTodoItem={deleteTodoItem}
          />
        ))}
      </div>
      {/* Adding Todo component for creating new todos */}
      <AddTodo createTodo={createTodo} />
    </main>
  );
};

export default Todos;
Expand
"use client";
import { ChangeEvent, FC, useState } from "react";
import { todoType } from "@/types/todoType";

interface Props {
  todo: todoType;
  changeTodoText: (id: number, text: string) => void;
  toggleIsTodoDone: (id: number, done: boolean) => void;
  deleteTodoItem: (id: number) => void;
}

const Todo: FC<Props> = ({
  todo,
  changeTodoText,
  toggleIsTodoDone,
  deleteTodoItem,
}) => {
  // State for handling editing mode
  const [editing, setEditing] = useState(false);

  // State for handling text input
  const [text, setText] = useState(todo.text);

  // State for handling "done" status
  const [isDone, setIsDone] = useState(todo.done);

  // Event handler for text input change
  const handleTextChange = (e: ChangeEvent<HTMLInputElement>) => {
    setText(e.target.value);
  };

  // Event handler for toggling "done" status
  const handleIsDone = async () => {
    toggleIsTodoDone(todo.id, !isDone);
    setIsDone((prev) => !prev);
  };

  // Event handler for initiating the edit mode
  const handleEdit = () => {
    setEditing(true);
  };

  // Event handler for saving the edited text
  const handleSave = async () => {
    changeTodoText(todo.id, text);
    setEditing(false);
  };

  // Event handler for canceling the edit mode
  const handleCancel = () => {
    setEditing(false);
    setText(todo.text);
  };

  // Event handler for deleting a todo item
  const handleDelete = () => {
    if (confirm("Are you sure you want to delete this todo?")) {
      deleteTodoItem(todo.id);
    }
  };

  // Rendering the Todo component
  return (
    <div className="flex items-center gap-2 p-4 border-gray-200 border-solid border rounded-lg">
      {/* Checkbox for marking the todo as done */}
      <input
        type="checkbox"
        className="text-blue-200 rounded-sm h-4 w-4"
        checked={isDone}
        onChange={handleIsDone}
      />
      {/* Input field for todo text */}
      <input
        type="text"
        value={text}
        onChange={handleTextChange}
        readOnly={!editing}
        className={`${
          todo.done ? "line-through" : ""
        } outline-none read-only:border-transparent focus:border border-gray-200 rounded px-2 py-1 w-full`}
      />
      {/* Action buttons for editing, saving, canceling, and deleting */}
      <div className="flex gap-1 ml-auto">
        {editing ? (
          <button
            onClick={handleSave}
            className="bg-green-600 text-green-50 rounded px-2 w-14 py-1"
          >
            Save
          </button>
        ) : (
          <button
            onClick={handleEdit}
            className="bg-blue-400 text-blue-50 rounded w-14 px-2 py-1"
          >
            Edit
          </button>
        )}
        {editing ? (
          <button
            onClick={handleCancel}
            className="bg-red-400 w-16 text-red-50 rounded px-2 py-1"
          >
            Close
          </button>
        ) : (
          <button
            onClick={handleDelete}
            className="bg-red-400 w-16 text-red-50 rounded px-2 py-1"
          >
            Delete
          </button>
        )}
      </div>
    </div>
  );
};

export default Todo;
Expand
"use client";
import { ChangeEvent, FC, useState } from "react";

interface Props {
  createTodo: (value: string) => void;
}

const AddTodo: FC<Props> = ({ createTodo }) => {
  // State for handling input value
  const [input, setInput] = useState("");

  // Event handler for input change
  const handleInput = (e: ChangeEvent<HTMLInputElement>) => {
    setInput(e.target.value);
  };

  // Event handler for adding a new todo
  const handleAdd = async () => {
    createTodo(input);
    setInput("");
  };

  // Rendering the AddTodo component
  return (
    <div className="w-full flex gap-1 mt-2">
      {/* Input field for entering new todo text */}
      <input
        type="text"
        className="w-full px-2 py-1 border border-gray-200 rounded outline-none"
        onChange={handleInput}
        value={input}
      />
      {/* Button for adding a new todo */}
      <button
        className="flex items-center justify-center bg-green-600 text-green-50 rounded px-2 h-9 w-14 py-1"
        onClick={handleAdd}
      >
        Add
      </button>
    </div>
  );
};

export default AddTodo;
Expand

Establish server-side functions

In this step, we establish server-side functions in the todoActions file to handle crucial operations on todo items:

  1. getData:
    • Fetches all existing todo items from the database.
  2. addTodo:
    • Adds a new todo item to the database with the provided text.
    • Initiates revalidation of the home page using revalidatePath("/").
  3. deleteTodo:
    • Removes a todo item from the database based on its unique ID.
    • Triggers a revalidation of the home page.
  4. toggleTodo:
    • Toggles the completion status of a todo item, updating the database accordingly.
    • Revalidates the home page after the operation.
  5. editTodo:
    • Modifies the text of a todo item identified by its ID in the database.
    • Initiates a revalidation of the home page.
"use server";
import {eq} from "drizzle-orm";
import {revalidatePath} from "next/cache";

import db from "@/db/drizzle";
import {todo} from "@/db/schema";

export const getData = async () => {
  const data = await db.select().from(todo);
  return data;
};

export const addTodo = async (text: string) => {
  await db.insert(todo).values({
    text: text,
  });
  revalidatePath("/");
};

export const deleteTodo = async (id: number) => {
  await db.delete(todo).where(eq(todo.id, id));

  revalidatePath("/");
};

export const toggleTodo = async (id: number, done: boolean) => {
  await db
    .update(todo)
    .set({
      done: done,
    })
    .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("/");
};
Expand

Project file structure

This guide uses the following file structure:

Frontend
Backend
FullStack
πŸ“¦ <project root>
 β”œ πŸ“‚ actions
 β”‚  β”” πŸ“œ todoActions.ts
 β”œ πŸ“‚ app
 β”‚  β”œ πŸ“œ globals.css
 β”‚  β”œ πŸ“œ layout.tsx
 β”‚  β”” πŸ“œ page.tsx
 β”œ πŸ“‚ components
 β”‚  β”œ πŸ“œ AddTodo.tsx
 β”‚  β”œ πŸ“œ Todo.tsx
 β”‚  β”” πŸ“œ Todos.tsx
 β”œ πŸ“‚ types
 β”‚  β”” πŸ“œ todoType.ts
 β”œ πŸ“œ next.config.ts
 β”œ πŸ“œ package.json
 β”œ πŸ“œ postcss.config.ts
 β”œ πŸ“œ tailwind.config.ts
 β”” πŸ“œ tsconfig.json
πŸ“¦ <project root>
 β”œ πŸ“‚ drizzle
 β”‚  β”œ πŸ“‚ meta
 β”‚  β”‚  β”œ πŸ“œ _journal.json  
 β”‚  β”‚  β””  πŸ“œ 0000_snapshot.json
 β”‚  β”” πŸ“œ 0000_watery_spencer_smythe.sql
 β”œ πŸ“‚ actions
 β”‚  β”” πŸ“œ todoActions.ts
 β”œ πŸ“‚ db
 β”‚  β”œ πŸ“œ drizzle.ts
 β”‚  β”” πŸ“œ schema.ts
 β”œ πŸ“œ drizzle.config.ts
 β”œ πŸ“œ package.json
 β”” πŸ“œ tsconfig.json
πŸ“¦ <+project root>
 β”œ πŸ“‚ actions
 β”‚  β”” πŸ“œ todoActions.ts
 β”œ πŸ“‚ drizzle
 β”‚  β”œ πŸ“‚ meta
 β”‚  β”‚  β”œ πŸ“œ _journal.json  
 β”‚  β”‚  β””  πŸ“œ 0000_snapshot.json
 β”‚  β”” πŸ“œ 0000_watery_spencer_smythe.sql
 β”œ πŸ“‚ app
 β”‚  β”œ πŸ“œ globals.css
 β”‚  β”œ πŸ“œ layout.tsx
 β”‚  β”” πŸ“œ page.tsx
 β”œ πŸ“‚ components
 β”‚  β”œ πŸ“œ AddTodo.tsx
 β”‚  β”œ πŸ“œ Todo.tsx
 β”‚  β”” πŸ“œ Todos.tsx
 β”œ πŸ“‚ db
 β”‚  β”œ πŸ“œ drizzle.ts
 β”‚  β”” πŸ“œ schema.ts
 β”œ πŸ“‚ types
 β”‚  β”” πŸ“œ todoType.ts
 β”œ πŸ“œ next.config.ts
 β”œ πŸ“œ package.json
 β”œ πŸ“œ drizzle.config.ts
 β”œ πŸ“œ postcss.config.ts
 β”œ πŸ“œ tailwind.config.ts
 β”” πŸ“œ tsconfig.json