Building a Full Stack App with Next.js and MongoDB: A Complete Guide
Next.js and MongoDB are one of the most popular combinations for building modern full stack web applications. Next.js handles the frontend and API layer, while MongoDB provides a flexible, document-based database that works naturally with JavaScript objects.
In this guide, you will build a complete full stack application from scratch — covering project setup, database connection, API routes, CRUD operations, and deployment.
Why Next.js + MongoDB?
- Full stack in one framework — Next.js handles frontend, API routes, and server-side rendering
- JavaScript/TypeScript end to end — same language for frontend, backend, and database queries
- Flexible schema — MongoDB's document model lets you iterate quickly without rigid migrations
- Scalable — both Next.js (on Vercel or similar) and MongoDB Atlas scale with your traffic
- Server Components — Next.js App Router lets you query MongoDB directly from React components
Prerequisites
- Node.js 18+ installed
- Basic knowledge of React and JavaScript/TypeScript
- A MongoDB Atlas account (free tier available at mongodb.com/atlas)
- A code editor (VS Code recommended)
Step 1: Create a Next.js Project
Open your terminal and run:
npx create-next-app@latest my-fullstack-app
cd my-fullstack-app
Choose the following options when prompted:
- TypeScript: Yes
- App Router: Yes
- Tailwind CSS: your preference
- src/ directory: Yes
Step 2: Set Up MongoDB Atlas
- Go to MongoDB Atlas and create a free cluster
- Create a database user with a username and password
- Whitelist your IP address (or use 0.0.0.0/0 for development)
- Click "Connect" and copy your connection string
Your connection string will look like:
mongodb+srv://username:password@cluster0.xxxxx.mongodb.net/myDatabase?retryWrites=true&w=majority
Step 3: Install MongoDB Driver
npm install mongodb
Step 4: Create the Database Connection Utility
Create a file at src/lib/mongodb.ts to manage your database connection:
import { MongoClient } from "mongodb";
if (!process.env.MONGODB_URI) {
throw new Error("Please add your MongoDB URI to .env.local");
}
const uri = process.env.MONGODB_URI;
const options = {};
let client: MongoClient;
let clientPromise: Promise<MongoClient>;
if (process.env.NODE_ENV === "development") {
// Use a global variable to preserve the client across hot reloads
let globalWithMongo = global as typeof globalThis & {
_mongoClientPromise?: Promise<MongoClient>;
};
if (!globalWithMongo._mongoClientPromise) {
client = new MongoClient(uri, options);
globalWithMongo._mongoClientPromise = client.connect();
}
clientPromise = globalWithMongo._mongoClientPromise;
} else {
client = new MongoClient(uri, options);
clientPromise = client.connect();
}
export default clientPromise;
Why the global variable? In development, Next.js hot-reloads your code frequently. Without caching the connection, you would open a new database connection on every reload and quickly exhaust your connection pool.
Step 5: Configure Environment Variables
Create a .env.local file in your project root:
MONGODB_URI=mongodb+srv://username:password@cluster0.xxxxx.mongodb.net/myDatabase?retryWrites=true&w=majority
Replace with your actual MongoDB Atlas connection string. Never commit this file to git.
Step 6: Build API Routes (CRUD Operations)
Next.js App Router uses Route Handlers for API endpoints. Let us build a simple items API.
GET and POST — src/app/api/items/route.ts
import { NextRequest, NextResponse } from "next/server";
import clientPromise from "@/lib/mongodb";
// GET all items
export async function GET() {
const client = await clientPromise;
const db = client.db("myDatabase");
const items = await db.collection("items").find({}).toArray();
return NextResponse.json(items);
}
// POST a new item
export async function POST(request: NextRequest) {
const client = await clientPromise;
const db = client.db("myDatabase");
const body = await request.json();
const result = await db.collection("items").insertOne({
...body,
createdAt: new Date(),
});
return NextResponse.json(
{ insertedId: result.insertedId },
{ status: 201 }
);
}
GET, PUT, DELETE by ID — src/app/api/items/[id]/route.ts
import { NextRequest, NextResponse } from "next/server";
import { ObjectId } from "mongodb";
import clientPromise from "@/lib/mongodb";
// GET single item
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const client = await clientPromise;
const db = client.db("myDatabase");
const item = await db
.collection("items")
.findOne({ _id: new ObjectId(params.id) });
if (!item) {
return NextResponse.json({ error: "Not found" }, { status: 404 });
}
return NextResponse.json(item);
}
// PUT (update) an item
export async function PUT(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const client = await clientPromise;
const db = client.db("myDatabase");
const body = await request.json();
const result = await db.collection("items").updateOne(
{ _id: new ObjectId(params.id) },
{ $set: { ...body, updatedAt: new Date() } }
);
return NextResponse.json({ modifiedCount: result.modifiedCount });
}
// DELETE an item
export async function DELETE(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const client = await clientPromise;
const db = client.db("myDatabase");
const result = await db
.collection("items")
.deleteOne({ _id: new ObjectId(params.id) });
return NextResponse.json({ deletedCount: result.deletedCount });
}
Step 7: Fetch Data in Server Components
One of the most powerful features of Next.js App Router is Server Components. You can query MongoDB directly inside your React components — no API call needed.
import clientPromise from "@/lib/mongodb";
export default async function ItemsPage() {
const client = await clientPromise;
const db = client.db("myDatabase");
const items = await db.collection("items").find({}).toArray();
return (
<div>
<h1>All Items</h1>
<ul>
{items.map((item) => (
<li key={item._id.toString()}>{item.name}</li>
))}
</ul>
</div>
);
}
Note: This code runs on the server only. The MongoDB connection string and query logic are never exposed to the browser.
Step 8: Add a Client Component Form
Create a form component to add new items:
"use client";
import { useState } from "react";
export default function AddItemForm() {
const [name, setName] = useState("");
const [loading, setLoading] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
await fetch("/api/items", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name }),
});
setName("");
setLoading(false);
window.location.reload();
};
return (
<form onSubmit={handleSubmit}>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Item name"
required
/>
<button type="submit" disabled={loading}>
{loading ? "Adding..." : "Add Item"}
</button>
</form>
);
}
Step 9: Project Structure
Your project should now look like this:
my-fullstack-app/
├── src/
│ ├── app/
│ │ ├── api/
│ │ │ └── items/
│ │ │ ├── route.ts (GET all, POST)
│ │ │ └── [id]/
│ │ │ └── route.ts (GET one, PUT, DELETE)
│ │ ├── page.tsx (Server Component)
│ │ └── layout.tsx
│ ├── components/
│ │ └── AddItemForm.tsx (Client Component)
│ └── lib/
│ └── mongodb.ts (Database connection)
├── .env.local
├── package.json
└── tsconfig.json
Step 10: Deploy to Vercel
- Push your code to GitHub
- Go to vercel.com and import your repository
- Add your
MONGODB_URIenvironment variable in the Vercel dashboard - Deploy — Vercel automatically detects Next.js and builds your app
Important: Make sure your MongoDB Atlas cluster allows connections from Vercel's IP ranges. The easiest option is to allow access from anywhere (0.0.0.0/0) in your Atlas Network Access settings.
Best Practices
- Use indexes — add MongoDB indexes on fields you query frequently for better performance
- Validate input — always validate request bodies before inserting into MongoDB
- Handle errors — wrap database operations in try/catch blocks
- Use TypeScript interfaces — define types for your documents to catch errors at compile time
- Connection pooling — the connection utility above handles this, but never create new clients per request
- Environment variables — never hardcode your connection string, always use env variables
Conclusion
Next.js and MongoDB make a powerful full stack combination. With the App Router, you can query your database directly in Server Components, build API routes for client interactions, and deploy everything to Vercel with zero server management.
Start with this foundation and expand it with authentication, file uploads, real-time updates, or whatever your project needs.