I ran into this exact headache: I hit npm run build
, the terminal yelled ELIFECYCLE
, and the only thing I could see was with-mongodb@0.1.0 build: next build → exit 1
. No stack trace, no clue. If you’re here, you probably saw the same thing.
I’ll show you how I diagnosed the failure, why it happens, and how I fixed it two different ways. I’ll also share a small, modern starter (Next.js + MongoDB) you can paste into a fresh project and a few “practice” tasks (CRUD routes, a seed script, a health check) so you can exercise the full stack. I’ll write in the “I am” voice because this is exactly what I did.
Error Show
My package.json
looked like this:
"next": "latest",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"mongodb": "^3.5.9"
and I was running Node v14.15.3.
Two big problems jumped out:
- Version skew: “latest” Next.js (13/14/15+) expects Node ≥ 18.17 and React 18. I was on Node 14 and React 16. That alone makes
next build
bail out. - Ancient MongoDB driver:
mongodb@3.5.9
is very old and doesn’t match modern connection options or examples.
The Quick Confirmation
I ran this directly to see the real error:
npx next build
Running npx
bypasses npm’s lifecycle wrapper and usually prints the actual Next error. That’s how I confirmed the version mismatch.
Two way I fix it
(what I recommend): Upgrade your toolchain
- Upgrade Node to 18.18+ (or LTS 20.x).
- Use React 18, Next 13/14/15, and mongodb@5+.
(works if you’re stuck on Node 14): Pin legacy versions
- Pin Next to 12.3.4.
- Use React 17 (or 16 if you must).
- Use mongodb@4.17.x.
The starter I ended up building
Requires Node ≥ 18.18. It uses the pages router for simplicity and adds a CRUD API, health check, and seed script.
package.json
{
"name": "with-mongodb",
"version": "0.2.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "NEXT_TELEMETRY_DISABLED=1 next build",
"start": "next start",
"seed": "node --experimental-strip-types scripts/seed.mjs"
},
"dependencies": {
"mongodb": "^5.9.0",
"next": "^14.2.14",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"swr": "^2.2.5"
},
"type": "module"
}
.env.local
MONGODB_URI="mongodb://localhost:27017"
MONGODB_DB="quote_db"
(For production I put secrets in .env.production
and use an Atlas URI.)
File Structure I Created
with-mongodb/
lib/
mongodb.ts
pages/
index.tsx
api/
health.ts
quotes/
[id].ts
index.ts
scripts/
seed.mjs
.env.local
package.json
Code I wrote
lib/mongodb.ts
connection helper
Reuses the client between hot reloads to avoid connection storms:
// lib/mongodb.ts
import { MongoClient, ServerApiVersion } from "mongodb";
const uri = process.env.MONGODB_URI!;
if (!uri) throw new Error("Missing MONGODB_URI");
const options = {
serverApi: {
version: ServerApiVersion.v1,
strict: true,
deprecationErrors: true
}
};
let client: MongoClient;
let clientPromise: Promise<MongoClient>;
// Reuse the client in dev (Next hot reload).
const g = global as unknown as { _mongoClientPromise?: Promise<MongoClient> };
if (!g._mongoClientPromise) {
client = new MongoClient(uri, options as any);
g._mongoClientPromise = client.connect();
}
const connected = g._mongoClientPromise!;
export async function getDb() {
const c = await connected;
const dbName = process.env.MONGODB_DB!;
return c.db(dbName);
}
pages/api/quotes/index.ts
list & create
// pages/api/quotes/index.ts
import type { NextApiRequest, NextApiResponse } from "next";
import { getDb } from "@/lib/mongodb";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const db = await getDb();
const col = db.collection("quotes");
if (req.method === "GET") {
const items = await col.find().sort({ _id: -1 }).limit(100).toArray();
return res.status(200).json(items);
}
if (req.method === "POST") {
const { text, author } = req.body || {};
if (!text || typeof text !== "string") {
return res.status(400).json({ error: "text is required" });
}
const doc = { text, author: author || "Unknown", createdAt: new Date() };
const { insertedId } = await col.insertOne(doc);
return res.status(201).json({ _id: insertedId, ...doc });
}
res.setHeader("Allow", "GET,POST");
return res.status(405).end();
}
pages/api/quotes/[id].ts
read/update/delete
// pages/api/quotes/[id].ts
import type { NextApiRequest, NextApiResponse } from "next";
import { getDb } from "@/lib/mongodb";
import { ObjectId } from "mongodb";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const { id } = req.query as { id: string };
if (!ObjectId.isValid(id)) return res.status(400).json({ error: "Invalid id" });
const db = await getDb();
const col = db.collection("quotes");
const _id = new ObjectId(id);
if (req.method === "GET") {
const doc = await col.findOne({ _id });
if (!doc) return res.status(404).json({ error: "Not found" });
return res.status(200).json(doc);
}
if (req.method === "PATCH") {
const { text, author } = req.body || {};
const $set: Record<string, any> = {};
if (typeof text === "string") $set.text = text;
if (typeof author === "string") $set.author = author;
if (!Object.keys($set).length) return res.status(400).json({ error: "No fields" });
const r = await col.findOneAndUpdate({ _id }, { $set }, { returnDocument: "after" });
if (!r.value) return res.status(404).json({ error: "Not found" });
return res.status(200).json(r.value);
}
if (req.method === "DELETE") {
await col.deleteOne({ _id });
return res.status(204).end();
}
res.setHeader("Allow", "GET,PATCH,DELETE");
return res.status(405).end();
}
pages/api/health.ts
— health check
// pages/api/health.ts
import type { NextApiRequest, NextApiResponse } from "next";
import { getDb } from "@/lib/mongodb";
export default async function handler(_: NextApiRequest, res: NextApiResponse) {
try {
const db = await getDb();
await db.command({ ping: 1 });
res.status(200).json({ ok: true });
} catch (e: any) {
res.status(500).json({ ok: false, error: e?.message });
}
}
pages/index.tsx
— minimal UI (SWR + fetch)
// pages/index.tsx
import { useState } from "react";
import useSWR from "swr";
const fetcher = (url: string) => fetch(url).then(r => r.json());
export default function Home() {
const { data, mutate, isLoading, error } = useSWR("/api/quotes", fetcher);
const [text, setText] = useState("");
const [author, setAuthor] = useState("");
async function addQuote(e: React.FormEvent) {
e.preventDefault();
if (!text.trim()) return;
await fetch("/api/quotes", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text, author })
});
setText(""); setAuthor("");
mutate();
}
async function delQuote(id: string) {
await fetch(`/api/quotes/${id}`, { method: "DELETE" });
mutate();
}
return (
<main style={{ maxWidth: 720, margin: "40px auto", fontFamily: "system-ui" }}>
<h1>Quotes</h1>
<form onSubmit={addQuote} style={{ marginBottom: 24 }}>
<input
placeholder="Quote text"
value={text}
onChange={e => setText(e.target.value)}
style={{ width: "100%", padding: 8, marginBottom: 8 }}
/>
<input
placeholder="Author (optional)"
value={author}
onChange={e => setAuthor(e.target.value)}
style={{ width: "100%", padding: 8, marginBottom: 8 }}
/>
<button type="submit">Add Quote</button>
</form>
{isLoading && <p>Loading…</p>}
{error && <p style={{ color: "crimson" }}>Failed to load</p>}
<ul style={{ listStyle: "none", padding: 0 }}>
{(data || []).map((q: any) => (
<li key={q._id} style={{ padding: 12, border: "1px solid #ddd", marginBottom: 10 }}>
<div style={{ fontSize: 18 }}>{q.text}</div>
<div style={{ opacity: 0.7 }}>{q.author}</div>
<button onClick={() => delQuote(q._id)} style={{ marginTop: 8 }}>
Delete
</button>
</li>
))}
</ul>
</main>
);
}
scripts/seed.mjs
// scripts/seed.mjs
import { MongoClient } from "mongodb";
import "dotenv/config";
const uri = process.env.MONGODB_URI;
const dbName = process.env.MONGODB_DB;
if (!uri || !dbName) {
console.error("Missing MONGODB_URI or MONGODB_DB");
process.exit(1);
}
const client = new MongoClient(uri);
await client.connect();
const db = client.db(dbName);
const col = db.collection("quotes");
await col.deleteMany({});
await col.insertMany([
{ text: "Talk is cheap. Show me the code.", author: "Linus Torvalds", createdAt: new Date() },
{ text: "Simplicity is prerequisite for reliability.", author: "Edsger Dijkstra", createdAt: new Date() }
]);
console.log("Seeded!");
await client.close();
How I Ran it
# Node 18+ recommended
npm i
npm run seed
npm run dev
# and later
npm run build
npm start
If npm run build
still fails, I run npx next build
directly to see the precise error.
When I Couldn’t Upgrade Node
I pinned versions compatible with Node 14:
{
"name": "with-mongodb",
"version": "0.1.1",
"private": true,
"scripts": {
"dev": "next dev",
"build": "NEXT_TELEMETRY_DISABLED=1 next build",
"start": "next start"
},
"dependencies": {
"mongodb": "^4.17.2",
"next": "12.3.4",
"react": "17.0.2",
"react-dom": "17.0.2",
"swr": "^1.3.0"
}
}
Note from my testing: mongodb@5
wants Node 14.20+ and really prefers 16+, so if your Node 14 is too old, stay on mongodb@4.17.x
.
Extra “Practice” Functionality I Added
Here’s how I stretched the project a bit further to learn more:
- Pagination & search
- In
GET /api/quotes
, I added query params?q=term&skip=0&limit=20
and used a$regex
filter and.skip().limit()
in MongoDB.
- In
- Basic validation
- I kept the code lightweight, but I sometimes swap the ad-hoc checks for a schema (e.g., Zod) to return helpful 400s.
- Optimistic UI
- With SWR, I used
mutate
with an updater function to show the new quote immediately, then revalidate.
- With SWR, I used
- Health checks
- The
/api/health
route pings MongoDB so I can wire it into container/liveness probes.
- The
- Seed & reset scripts
- Seeding gives me deterministic lists for demos and testing.
If you want a quick pagination upgrade, here’s a drop-in GET
for pages/api/quotes/index.ts
:
if (req.method === "GET") {
const { q = "", skip = "0", limit = "20" } = req.query as Record<string, string>;
const s = Math.max(0, parseInt(skip, 10) || 0);
const l = Math.min(100, Math.max(1, parseInt(limit, 10) || 20));
const filter = q
? { text: { $regex: q, $options: "i" } }
: {};
const cursor = col.find(filter).sort({ _id: -1 }).skip(s).limit(l);
const [items, total] = await Promise.all([
cursor.toArray(),
col.countDocuments(filter)
]);
return res.status(200).json({ items, total, skip: s, limit: l });
}
And on the client you can call /api/quotes?skip=0&limit=20&q=code
.
Common Pitfalls I Ran Into
- Forgetting env vars:
MONGODB_URI
andMONGODB_DB
must exist in.env.local
(dev) and.env.production
(prod). - Windows/WSL weirdness: If MongoDB is inside WSL or Docker,
localhost
might not resolve the way you expect. Try127.0.0.1
or the container host IP. - “latest” drift: I never leave
"next": "latest"
anymore. I pin versions. - Silent build exits: I use
npx next build
to see the real error.
Final Thought
I’ve learned that 80% of mysterious next build
errors are version mismatches Node, React, Next, or a library that moved too fast while my project didn’t. Once I aligned Node ≥ 18.18, React 18, a pinned Next 14, and mongodb@5
, everything clicked. The CRUD API, health check, and seed script gave me a tight feedback loop so I could build without fear.