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

Hey! I'm a passionate Full-Stack Developer who loves turning ideas into scalable web apps. I specialize in NestJS, React, and MongoDB, and enjoy working on modern product experiences with clean UI and solid backend architecture. Currently exploring fintech integrations like Razorpay and PayPal. Follow me for practical coding tips, tutorials, and insights from real-world projects. Let’s build something awesome together! πŸ”₯