How Do I Fix next build Errors in Next.js with MongoDB?

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:

  1. 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.
  2. 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:

  1. 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.
  2. 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.
  3. Optimistic UI
    • With SWR, I used mutate with an updater function to show the new quote immediately, then revalidate.
  4. Health checks
    • The /api/health route pings MongoDB so I can wire it into container/liveness probes.
  5. 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 and MONGODB_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. Try 127.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.

Related blog posts