Build a Modern TODO App with Next.js, TypeScript, Tailwind CSS & Mongoose (Full CRUD Functionality!) πŸš€

ajit guptaajit gupta
6 min read

In this guide, you'll learn how to build a TODO app using modern technologies:

  • Next.js for the frontend and backend

  • Mongoose to interact with MongoDB

  • Tailwind CSS for styling

  • TypeScript for type safety

Prerequisites

Before we begin, ensure you have:

  • Node.js installed (v16 or later)

  • A MongoDB database (local or cloud via MongoDB Atlas)

  • Basic knowledge of Next.js, TypeScript, and Tailwind CSS


Step 1: Set Up a Next.js Project

Run the following command to create a Next.js app with TypeScript:

create-next-app@latest todo-app
cd todo-app

When you run:

npx create-next-app@latest todo-app

It will prompt you with several options to configure your Next.js project. Here are the typical questions it asks:

  1. Would you like to use TypeScript? (Yes/No)
    β†’ Select Yes if you want TypeScript support.

  2. Would you like to use ESLint? (Yes/No)
    β†’ Select Yes to enable ESLint for code linting and best practices.

  3. Would you like to use Tailwind CSS? (Yes/No)
    β†’ Select Yes if you want Tailwind CSS for styling.

  4. Would you like to use src/ directory? (Yes/No)
    β†’ Selecting Yes places your code inside a src/ folder, which helps with project organization.

  5. Would you like to use experimental app/ directory? (Yes/No)
    β†’ Selecting Yes enables the new Next.js App Router (recommended for new projects).

  6. What import alias would you like configured? (@/* by default)
    β†’ You can keep the default @/* or customize it for better import management.

Step 3: Set Up MongoDB with Mongoose

Using MongoDB Atlas (Cloud)

  1. Go to MongoDB Atlas and create a free account.

  2. Create a new cluster.

  3. Click Connect β†’ Drivers β†’ Select Node.js version 4.0+.

  4. Copy the MongoDB connection URI, which looks like this:

Create a .env.local file and add your MongoDB connection string:

MONGODB_URI=mongodb+srv://<your-username>:<your-password>@cluster0.mongodb.net/

Once inside the project, install dependencies:

npm install mongoose

πŸ“Œ Bonus: Looking for a quick and efficient online background remover? Try Utilshub Background Remover – an AI-powered tool that instantly removes backgrounds from images! 🎨✨

Now, create a database connection helper:

Create a new folder lib/ and a file lib/mongodb.ts:

import mongoose from "mongoose";

const MONGODB_URI = process.env.MONGODB_URI as string;

if (!MONGODB_URI) {
  throw new Error("Please define MONGODB_URI in .env.local");
}

let cached = (global as any).mongoose || { conn: null, promise: null };

async function dbConnect() {
  if (cached.conn) return cached.conn;

  if (!cached.promise) {
    cached.promise = mongoose
      .connect(MONGODB_URI, {
        dbName: "todoApp",
        bufferCommands: false
      })
      .then((mongoose) => mongoose);
  }

  cached.conn = await cached.promise;
  return cached.conn;
}

export default dbConnect;

Step 4: Define the TODO Model

Create a models/Todo.ts file:

import mongoose, { Schema, Document, model, models } from "mongoose";

export interface ITodo extends Document {
  _id: string; // Explicitly define _id
  title: string;
  completed: boolean;
}

const TodoSchema = new Schema<ITodo>({
  title: { type: String, required: true },
  completed: { type: Boolean, default: false }
});

export default models.Todo || model<ITodo>("Todo", TodoSchema);

Step 5: Create API Routes for CRUD Operations

Get all todos

Create /api/todos/route.ts:

import dbConnect from "@/app/lib/mongodb";
import Todo from "@/app/models/Todo";
import { NextResponse } from "next/server";

export async function GET() {
  await dbConnect();
  const todos = await Todo.find({});
  return NextResponse.json(todos);
}

export async function POST(req: Request) {
  await dbConnect();
  const { title } = await req.json();
  const newTodo = await Todo.create({ title });
  return NextResponse.json(newTodo, { status: 201 });
}

Update and delete todos

Create pages/api/todos/[id]/route.ts:

import dbConnect from "@/app/lib/mongodb";
import Todo from "@/app/models/Todo";
import { NextResponse } from "next/server";

export async function PUT(
  req: Request,
  { params }: { params: { id: string } }
) {
  await dbConnect();

  if (!params.id) {
    return NextResponse.json({ error: "Todo ID is required" }, { status: 400 });
  }

  try {
    const updatedTodo = await Todo.findByIdAndUpdate(
      params.id,
      await req.json(),
      { new: true }
    );

    if (!updatedTodo) {
      return NextResponse.json({ error: "Todo not found" }, { status: 404 });
    }

    return NextResponse.json(updatedTodo);
  } catch (error) {
    return NextResponse.json(
      { error: "Failed to update todo" },
      { status: 500 }
    );
  }
}

export async function DELETE(
  req: Request,
  { params }: { params: { id: string } }
) {
  await dbConnect();

  if (!params.id) {
    return NextResponse.json({ error: "Todo ID is required" }, { status: 400 });
  }

  try {
    const deletedTodo = await Todo.findByIdAndDelete(params.id);

    if (!deletedTodo) {
      return NextResponse.json({ error: "Todo not found" }, { status: 404 });
    }

    return NextResponse.json(
      { message: "Todo deleted successfully" },
      { status: 200 }
    );
  } catch (error) {
    return NextResponse.json(
      { error: "Failed to delete todo" },
      { status: 500 }
    );
  }
}

Step 6: Build the Frontend with React & Tailwind CSS

Create the Todo Component

Create components/TodoItem.tsx:

import { useState } from "react";

type TodoProps = {
  id: string;
  title: string;
  completed: boolean;
  refreshTodos: () => void;
};

export default function TodoItem({
  id,
  title,
  completed,
  refreshTodos
}: TodoProps) {
  const [loading, setLoading] = useState(false);

  const toggleCompletion = async () => {
    setLoading(true);
    await fetch(`/api/todos/${id}`, {
      method: "PUT",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ completed: !completed })
    });
    refreshTodos();
  };

  const deleteTodo = async () => {
    setLoading(true);
    await fetch(`/api/todos/${id}`, { method: "DELETE" });
    refreshTodos();
  };

  return (
    <div className="flex items-center justify-between p-3 border rounded-lg">
      <span className={`${completed ? "line-through text-gray-500" : ""}`}>
        {title}
      </span>
      <div className="flex gap-2">
        <button onClick={toggleCompletion} className="text-green-500">
          {completed ? "Undo" : "Done"}
        </button>
        <button onClick={deleteTodo} className="text-red-500">
          Delete
        </button>
      </div>
    </div>
  );
}

Build the Main Page Home Component

Build the Homepage (Homepage.tsx)

"use client";
import { useState, useEffect } from "react";
import TodoItem from "./TodoItem";

export default function Homepage() {
  const [todos, setTodos] = useState([]);
  const [newTodo, setNewTodo] = useState("");

  const fetchTodos = async () => {
    const res = await fetch("/api/todos");
    const data = await res.json();
    setTodos(data);
  };

  useEffect(() => {
    fetchTodos();
  }, []);

  const addTodo = async () => {
    if (!newTodo) return;
    await fetch("/api/todos", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ title: newTodo })
    });
    setNewTodo("");
    fetchTodos();
  };

  return (
    <div className="max-w-xl mx-auto mt-10 p-5 bg-white shadow-lg rounded-lg">
      <h1 className="text-2xl font-bold mb-4">TODO App</h1>
      <div className="flex gap-2 mb-4">
        <input
          type="text"
          className="border p-2 flex-1 rounded-lg"
          value={newTodo}
          onChange={(e) => setNewTodo(e.target.value)}
          placeholder="New task..."
        />
        <button
          onClick={addTodo}
          className="bg-blue-500 text-white p-2 rounded-lg"
        >
          Add
        </button>
      </div>
      <div className="space-y-2">
        {todos.map((todo: any) => (
          <TodoItem
            key={todo._id}
            id={todo._id}
            title={todo.title}
            completed={todo.completed}
            refreshTodos={fetchTodos} // βœ… Make sure this function is passed
          />
        ))}
      </div>
    </div>
  );
}

See MongoDB response here -

🎨 Bonus: Enhance Your UI Design with a Free Background Remover

Utilshub’s Background Remover – Make Your UI Assets Stand Out

When designing websites or UI components, high-quality images play a crucial role. However, many designers struggle with removing backgrounds to create clean, professional visuals.

That's where Utilshub’s AI-Powered Background Remover comes in! With just one click, you can:
βœ” Remove backgrounds from images instantly
βœ” Make UI components look sleek and professional
βœ” Create transparent assets for web design
βœ” Save time on manual editing

πŸ’‘ Perfect for UI/UX designers, developers, and content creators!

πŸš€ Conclusion

πŸŽ‰ Congratulations! You’ve built a full-stack TODO app with Next.js, TypeScript, Tailwind CSS, and MongoDB. Try deploying it on Vercel or Railway!

Let me know if you need more improvements! πŸš€

πŸ‘‰ Have questions or feedback? Drop them in the comments!

πŸ’‘ Liked this guide? Share it with others! πŸ˜ƒ

Happy coding! πŸš€πŸ’»

20
Subscribe to my newsletter

Read articles from ajit gupta directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

ajit gupta
ajit gupta

Learning!