Introduction
In today’s fast-paced development world, building a full-stack app doesn’t have to be complicated. With the power of Next.js and MongoDB, developers can quickly create scalable applications. In this tutorial, you will learn how to build a simple, full-stack To-Do App that leverages the App Router in Next.js and MongoDB for data persistence.
Moreover, this app will be built using pure JavaScript (no TypeScript), making it perfect for beginners. By the end of this guide, you’ll understand how to set up a database, connect it using Mongoose, and build a frontend that interacts seamlessly with your backend.
Prerequisites
Before we begin, make sure you have:
- Node.js installed (v18 + or later recommended)
- npm v9+
- A MongoDB Atlas account or a local MongoDB installation
- Basic knowledge of JavaScript, React, and REST API
Project Overview
We’ll build a simple but powerful To-Do App that includes:
- Adding tasks
- Marking tasks as complete/incomplete
- Deleting tasks
- MongoDB storage
- API Routes in Next.js
- App Router with JavaScript (not TypeScript)
Choose:
- JavaScript
- App Router
- Tailwind CSS (optional but recommended)
I have a Node.js version
v22.17.0
npm Version
10.3.0
mongoose
version v7.0.11
Step 1: Setting Up the Next.js Project
First, let’s create a new Next.js project with JavaScript support:
npx create-next-app@latest todo-project
cd todo-projectWhile running this command, it will ask you some questions about the dependency so that you can choose the option “No” for TypeScript and TailwindCSS

Install additional dependencies we’ll need:
Step 2: Setting Up MongoDB Connection
Install MongoDB Community Edition locally and run the MongoDB service.
Option A: MongoDB Atlas (Cloud)
- Go to MongoDB Atlas and create a free cluster
- Create a database user and note the username/password
- Add your IP address to the IP access list
- Get your connection string (it will look like
mongodb+srv://<username>:<password>@cluster0.xxxxx.mongodb.net/?retryWrites=true&w=majority)
Option B: Local MongoDB
Install MongoDB Community Edition locally and run the MongoDB service.
Creating the Database Connection Utility
Create a new file lib/mongodb.js:
npm install mongooseI have created lib/mongob.js inside the src folder, and now paste this code
import mongoose from 'mongoose'
const MONGODB_URI = process.env.MONGODB_URI
if (!MONGODB_URI) throw new Error('MONGODB_URI not defined in .env.local')
let cached = global.mongoose || { conn: null, promise: null }
export async function connectDB() {
if (cached.conn) return cached.conn
if (!cached.promise) {
cached.promise = mongoose.connect(MONGODB_URI, {
dbName: 'todo-app',
bufferCommands: false,
}).then((mongoose) => {
return mongoose
})
}
cached.conn = await cached.promise
return cached.conn
}
Create a .env.local file in your src folder:
MONGODB_URI=mongodb://<username>:<password>@localhost:27017/nextjsdb?authSource=adminStep 3: Creating the Todo Model
Create a new directory models Inside the src and add Todo.js:
import mongoose from 'mongoose'
const TodoSchema = new mongoose.Schema({
text: { type: String, required: true },
completed: { type: Boolean, default: false }
}, { timestamps: true })
export default mongoose.models.Todo || mongoose.model('Todo', TodoSchema)Step 4: Creating API Routes
For GET and POST requests, we use a single route.js file. For DELETE and PUT requests (which require dynamic parameters like an id We create a separate folder structure. This is because Next.js follows a file-based routing system, and each endpoint must have its own file or dynamic folder to handle different HTTP methods and routes correctly.
GET & POST → /app/api/todos/route.js
import { connectDB } from '@/lib/mongodb'
import Todo from '@/models/Todo'
export async function GET() {
await connectDB()
const todos = await Todo.find().sort({ createdAt: -1 })
return Response.json(todos)
}
export async function POST(req) {
const { text } = await req.json()
await connectDB()
const newTodo = await Todo.create({ text })
return Response.json(newTodo)
}Create the Folder path below. I have added
PUT & DELETE → /app/api/todos/[id]/route.js
Note: This would be a folder name [id] Do not confuse.
import { connectDB } from '@/lib/mongodb'
import Todo from '@/models/Todo'
export async function PUT(req, { params }) {
const { id } = params
const { completed } = await req.json()
await connectDB()
const updated = await Todo.findByIdAndUpdate(id, { completed }, { new: true })
return Response.json(updated)
}
export async function DELETE(req, { params }) {
const { id } = params
await connectDB()
await Todo.findByIdAndDelete(id)
return Response.json({ message: 'Deleted' })
}Step 6: Build the UI — /app/page.js
Now that our API is ready, let’s shift our focus to the front end. To begin with, we’ll build the user interface using React (via Next.js). This will include a task input field, a task list, and buttons to complete or delete a task.
First, we define two state variables: task to hold the current input, and todos to store the fetched task list. After the component mounts, we use useEffect to fetch tasks from the API and display them on the screen.
When a user adds a task, it is sent to the backend via a POST request. Then, the new task is added to the state and shown immediately in the list. In contrast, when a task is toggled or deleted, we use PUT and DELETE requests to update the backend accordingly. As a result, the interface remains synced with the database in real time.
The file /app/page.js is the main UI page of your application — it’s where users:
- Type a task
- Add it to the list
- Mark it complete/incomplete
- Delete it
'use client'What’s Happening in /app/page.js:
This line tells Next.js that this file uses client-side rendering (since we use React hooks like useState and useEffect).
React State
const [task, setTask] = useState('')
const [todos, setTodos] = useState([])
taskstores the text you type in the input.todosstores the list of tasks fetched from MongoDB.
Add a New Task
const addTodo = async () => {
if (!task.trim()) return
const res = await fetch('/api/todos', {
method: 'POST',
body: JSON.stringify({ text: task }),
})
const newTodo = await res.json()
setTodos([newTodo, ...todos])
setTask('')
}
- Sends a
POSTrequest to/api/todos - Adds the new task to the UI after the response
- Clears the input
Full code
'use client'
import { useEffect, useState } from 'react'
export default function Home() {
const [task, setTask] = useState('')
const [todos, setTodos] = useState([])
useEffect(() => {
fetchTodos()
}, [])
const fetchTodos = async () => {
const res = await fetch('/api/todos')
const data = await res.json()
setTodos(data)
}
const addTodo = async () => {
if (!task.trim()) return
const res = await fetch('/api/todos', {
method: 'POST',
body: JSON.stringify({ text: task }),
})
const newTodo = await res.json()
setTodos([newTodo, ...todos])
setTask('')
}
const toggleComplete = async (id, completed) => {
const res = await fetch(`/api/todos/${id}`, {
method: 'PUT',
body: JSON.stringify({ completed: !completed })
})
const updated = await res.json()
setTodos(todos.map(todo => todo._id === id ? updated : todo))
}
const deleteTodo = async (id) => {
await fetch(`/api/todos/${id}`, { method: 'DELETE' })
setTodos(todos.filter(todo => todo._id !== id))
}
return (
<main className="min-h-screen p-6 bg-gray-100 flex flex-col items-center">
<h1 className="text-2xl font-bold mb-4">To-Do App</h1>
<div className="flex gap-2 mb-4">
<input
type="text"
value={task}
onChange={(e) => setTask(e.target.value)}
placeholder="Enter task..."
className="border p-2 rounded w-64"
/>
<button
onClick={addTodo}
className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
>
Add
</button>
</div>
<ul className="w-full max-w-md space-y-2">
{todos.map((todo) => (
<li key={todo._id} className="flex justify-between items-center bg-white p-3 rounded shadow">
<span
onClick={() => toggleComplete(todo._id, todo.completed)}
className={`cursor-pointer ${todo.completed ? 'line-through text-gray-500' : ''}`}
>
{todo.text}
</span>
<button
onClick={() => deleteTodo(todo._id)}
className="text-red-500 hover:text-red-700"
>
</button>
</li>
))}
</ul>
</main>
)
}
Step 8: Run the Project
Finally, you can run the project in development mode
npm run devConclusion
Building a full-stack To-Do application using Next.js and MongoDB is not only an excellent project for beginners, but also a practical demonstration of how modern web technologies work together. Through this tutorial, you’ve learned how to structure a Next.js project using the App Router, create RESTful API routes, connect to a MongoDB database using Mongoose, and build a responsive UI with React.
Although we built a simple To-Do app, the patterns you’ve implemented here—such as CRUD operations, dynamic API routing, and state management—can easily scale into more complex projects.
In conclusion, this app provides a solid foundation for any full-stack application. You can enhance it further by adding authentication, filtering, due dates, or even integrating with external APIs.
So, what’s next? Try deploying your project to Vercel, share it with others, and keep building!
Optional: Deploy to Vercel
- Push code to GitHub.
- Visit vercel.com and import your repo.
- In project settings, add
MONGODB_URIit as an environment variable. - Deploy.


Pingback: WebSockets Guide: Real-Time Web Communication Explained - pythonjournals.com