🤝 Why Combine Next.js and MongoDB?
- Full-stack flexibility: Next.js lets you mix server-side (API routes, server components, server actions) with client-facing pages, while MongoDB handles document-based data.
- Serverless ready: Both Vercel and MongoDB Atlas scale effortlessly, making deployments straightforward.
- Modern tooling: Use TypeScript, ESLint, and hot reloading while working with a database in a simple, modern setup.
- React + Node synergy: Next.js keeps your React UI and Node.js backend in one codebase, making data fetching, validation, and rendering flow together without context switching.
⚡ How React + Node Power Up Your Stack
- Shared TypeScript types: Reuse interfaces between React components and Node.js API handlers to avoid drift.
- Streaming-ready UI: Server Components fetch data with Node.js, then hydrate React on the client for smooth UX.
- Unified deployment: Ship a single Next.js app; Vercel or any Node-friendly host runs both your React pages and serverless functions alongside MongoDB.
Prerequisites
Before you begin, make sure you have the following:
- Node.js 18+ and npm (or pnpm/yarn)
- A MongoDB instance (MongoDB Atlas or local installation)
- Basic knowledge of Next.js 13+ App Router
- A terminal and your favorite code editor
Tip:Â If you do not already have a MongoDB Atlas account, sign up for the free tier and create a cluster. You will need the connection string in later steps.
1. Create a New Next.js Project
npx create-next-app@latest nextjs-mongodb-demo
# or
pnpm create next-app nextjs-mongodb-demoWhen prompted:
- TypeScript: Yes (recommended)
- ESLint: Yes
- Tailwind CSS: Optional
- App Router: Yes (required for this guide)
- Experimental features: Accept defaults
Move into the project directory:
cd nextjs-mongodb-demo2. Install MongoDB Dependencies
You can use the official MongoDB Node.js driver or Mongoose. This walkthrough uses the official driver for a lightweight setup.
npm install mongodb
# or
pnpm add mongodbIf you prefer Mongoose for schema modeling, install mongoose instead and adjust the connection helper accordingly.
3. Configure Environment Variables
Create a .env.local file in the project root (Next.js automatically loads this file in development):
MONGODB_URI="mongodb+srv://<username>:<password>@cluster0.mongodb.net/?retryWrites=true&w=majority"
MONGODB_DB="nextjs_demo"- Replace the URI with your actual connection string from MongoDB Atlas (or
mongodb://localhost:27017for local). MONGODB_DBis the default database name you want to work with.
Security reminder:Â Never commitÂ
.env.local to version control. Next.js ignores it by default thanks toÂ.gitignore fromÂcreate-next-app.
4. Create a Reusable MongoDB Client Helper
Inside lib/, add a mongodb.ts helper to manage the database connection with caching to prevent exhausting connections during hot reloads or in serverless environments.
// lib/mongodb.ts
import { MongoClient, Db } from "mongodb";
declare global {
// eslint-disable-next-line no-var
var _mongoClientPromise: Promise<MongoClient> | undefined;
}
const uri = process.env.MONGODB_URI;
const dbName = process.env.MONGODB_DB;
if (!uri) {
throw new Error(
"Please define the MONGODB_URI environment variable in .env.local"
);
}
let client: MongoClient;
let clientPromise: Promise<MongoClient>;
if (process.env.NODE_ENV === "development") {
if (!global._mongoClientPromise) {
client = new MongoClient(uri);
global._mongoClientPromise = client.connect();
}
clientPromise = global._mongoClientPromise;
} else {
client = new MongoClient(uri);
clientPromise = client.connect();
}
export async function getDb(): Promise<Db> {
const client = await clientPromise;
return client.db(dbName);
}What this helper does
- Creates a single MongoDB client instance that can be reused across API routes and server components.
- Prevents multiple connections by caching the client in development using a global variable.
- Throws a descriptive error if required environment variables are missing.
5. Build a Simple API Route
Create an API route that uses the helper to read and write data. With the App Router, place this inside app/api/tasks/route.ts.
// app/api/tasks/route.ts
import { NextResponse } from "next/server";
import { getDb } from "@/lib/mongodb";
export async function GET() {
try {
const db = await getDb();
const tasks = await db
.collection("tasks")
.find({})
.sort({ createdAt: -1 })
.toArray();
return NextResponse.json(tasks);
} catch (error) {
console.error("GET /api/tasks error", error);
return NextResponse.json(
{ message: "Failed to fetch tasks" },
{ status: 500 }
);
}
}
export async function POST(request: Request) {
try {
const payload = await request.json();
if (!payload?.title) {
return NextResponse.json(
{ message: "Title is required" },
{ status: 400 }
);
}
const db = await getDb();
const newTask = {
title: payload.title,
completed: false,
createdAt: new Date(),
};
const { insertedId } = await db.collection("tasks").insertOne(newTask);
return NextResponse.json({ ...newTask, _id: insertedId }, { status: 201 });
} catch (error) {
console.error("POST /api/tasks error", error);
return NextResponse.json(
{ message: "Failed to create task" },
{ status: 500 }
);
}
}How it works
GETfetches all tasks from thetaskscollection.POSTvalidates the incoming payload before inserting a new record.- Errors are logged server-side and returned as JSON responses with appropriate status codes.
6. Display Data in a Server Component
Create or update app/page.tsx to fetch tasks during server rendering and display them in the UI.
// app/page.tsx
import { getDb } from "@/lib/mongodb";
import Link from "next/link";
async function getTasks() {
const db = await getDb();
const tasks = await db
.collection("tasks")
.find({})
.sort({ createdAt: -1 })
.toArray();
return tasks.map((task) => ({
id: task._id.toString(),
title: task.title,
completed: task.completed,
createdAt: task.createdAt,
}));
}
export default async function Home() {
const tasks = await getTasks();
return (
<main className="mx-auto max-w-2xl space-y-6 p-8">
<header className="space-y-2">
<h1 className="text-3xl font-semibold">Next.js + MongoDB Demo</h1>
<p className="text-muted-foreground">
Server-rendered data fetched directly from MongoDB.
</p>
</header>
<section className="space-y-4">
{tasks.length === 0 ? (
<p className="text-sm text-muted-foreground">
No tasks yet. Add one via POST /api/tasks.
</p>
) : (
<ul className="space-y-2">
{tasks.map((task) => (
<li key={task.id} className="rounded border p-3">
<div className="flex items-center justify-between">
<h2 className="font-medium">{task.title}</h2>
<span className="text-xs uppercase tracking-wide">
{task.completed ? "Completed" : "Pending"}
</span>
</div>
<time className="text-xs text-muted-foreground">
{new Date(task.createdAt).toLocaleString()}
</time>
</li>
))}
</ul>
)}
</section>
<footer className="text-sm text-muted-foreground">
<Link href="/api/tasks" className="underline">
View API response
</Link>
</footer>
</main>
);
}- The server component runs on the server, so it can directly call
getDb()without exposing secrets to the browser. _idvalues are converted to strings to avoid serialization issues when returning data to the client.
7. Add a Client-Side Form (Optional)
To interact with the API without Postman, add a simple form using server actions or client components. Here’s a basic example with a React client component using fetch.
// app/add-task-form.tsx
"use client";
import { useState, useTransition } from "react";
import { useRouter } from "next/navigation";
export function AddTaskForm() {
const [title, setTitle] = useState("");
const [status, setStatus] = useState<string | null>(null);
const [isPending, startTransition] = useTransition();
const router = useRouter();
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
setStatus(null);
const response = await fetch("/api/tasks", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title }),
});
if (response.ok) {
startTransition(() => {
router.refresh();
setTitle("");
setStatus("Task created!");
});
} else {
const data = await response.json();
setStatus(data.message || "Something went wrong.");
}
}
return (
<form onSubmit={handleSubmit} className="space-y-2">
<label className="block text-sm font-medium" htmlFor="title">
New Task Title
</label>
<input
id="title"
value={title}
onChange={(event) => setTitle(event.target.value)}
className="w-full rounded border p-2"
placeholder="e.g. Publish my Next.js MongoDB blog"
/>
<button
type="submit"
className="rounded bg-black px-4 py-2 text-white hover:bg-black/80 disabled:opacity-60"
disabled={isPending || title.trim() === ""}
>
{isPending ? "Adding..." : "Add Task"}
</button>
{status && <p className="text-sm text-muted-foreground">{status}</p>}
</form>
);
}Import and render this component inside app/page.tsx to allow quick testing:
import { AddTaskForm } from "./add-task-form";
// ... inside the Home component JSX
<section className="space-y-4">
<AddTaskForm />
{/* existing task list */}
</section>;8. Run the Application
Start the development server:
npm run dev
# or
pnpm devVisit http://localhost:3000 to see the server-rendered task list and the optional form. You can also test the API directly at http://localhost:3000/api/tasks with GET and POST requests.
9. 🚀 Deployment Notes
- Environment variables: Set
MONGODB_URIandMONGODB_DBin your hosting platform (Vercel, Netlify, etc.). - MongoDB Atlas IP allow list: Add your hosting provider’s IP addresses when using Atlas.
- Connection pooling: The shared
MongoClientfromlib/mongodb.tshandles pooling automatically across serverless invocations. - SSR caching: If you rely heavily on server-side rendering, explore ISR (Incremental Static Regeneration) or caching strategies.
đź§ Common Troubleshooting Tips
MongooseServerSelectionError orÂMongoServerSelectionError: Check that your URI is correct and that the username/password has appropriate cluster permissions.Missing MONGODB_URI: Ensure.env.localexists and has no syntax errors. Remember to restart the dev server after changing environment variables.- Hot reload errors in dev: Restarting
npm run devclears stale connections if needed.
đź§ Next Steps
Once the basics are in place, try exploring:
- Mongoose schemas for validation and middleware.
- Server Actions (Next.js 13+) for form submissions without API routes.
- Zod + React Hook Form for type-safe validation on both client and server.
- NextAuth.js or custom JWT authentication backed by MongoDB.
- Background jobs with queues (BullMQ, RabbitMQ) that store state in MongoDB.
📦 Full Demo Repository
Want everything wired together? Grab the complete project from GitHub: https://github.com/hardik-143/SETUP-TEMPLATES/tree/main/nextjs-mongodb
🌟 Final Thoughts
Bringing MongoDB into your Next.js workflow opens up a powerhouse combo of speed, flexibility, and developer-friendly magic. With smart connection pooling, reusable database helpers, and cleanly structured API routes, you’re not just writing code — you’re building a scalable foundation that can evolve from quick prototypes to fully production-ready systems.
This setup isn’t just functional; it’s a launchpad. Use it to experiment with advanced patterns, craft richer features, and shape your app exactly the way you imagine. The more you build on this stack, the more you’ll appreciate how effortlessly it grows with you.
Build fast. Scale smart. Ship confidently. 🚀
- If you enjoyed my content or want to support my writing journey, please take a moment to follow me on Medium. I regularly share insights on JavaScript, modern frontend development, and practical dev tricks you can use in real projects.
đź”— Read the original article on Medium:
❤️ Follow me on Medium for more:
