35% OFF
Ends14d 00:00:00
Shop
Back to Blog
nextjsmongodbtypescriptwebdev

How to Fetch Data from MongoDB in Next.js 15 (With Caching Explained)

Pixel Anas··5 min read

A complete guide to fetching MongoDB data in Next.js 15 Server Components, including how fetch caching actually works and where it does not apply.

Searching for how to fetch data from MongoDB in Next.js usually leads to two separate problems mixed together. One is how to actually query MongoDB from inside a Server Component. The other is how Next.js fetch caching works, and whether it even applies to a database call at all.

This covers both, since they come up together in almost every real project.

Fetching MongoDB Data Directly in a Server Component

This is the part that trips people up first. fetch caching in Next.js only applies to actual fetch() calls, HTTP requests. A direct MongoDB query through Mongoose is not a fetch call, so none of the cache or next.revalidate options apply to it at all.

// lib/db.ts
import mongoose from 'mongoose';

const MONGODB_URI = process.env.MONGODB_URI as string;

let cached = (global as any).mongoose || { conn: null, promise: null };

export async function connectDB() {
  if (cached.conn) return cached.conn;
  if (!cached.promise) {
    cached.promise = mongoose.connect(MONGODB_URI);
  }
  cached.conn = await cached.promise;
  (global as any).mongoose = cached;
  return cached.conn;
}
// app/blog/page.tsx
import { connectDB } from '@/lib/db';
import Post from '@/models/Post';

export default async function BlogPage() {
  await connectDB();
  const posts = await Post.find().sort({ createdAt: -1 }).lean();

  return (
    <div>
      {posts.map((post: any) => (
        <article key={post._id}>{post.title}</article>
      ))}
    </div>
  );
}

This runs fresh on every request by default. No caching layer sits between this query and MongoDB unless you add one yourself, which is the part most fetch caching tutorials skip when they cover MongoDB specifically.

Where Next.js Fetch Caching Actually Applies

If your Next.js app calls an external API with fetch, that is where the built-in caching system applies:

// This IS a fetch call, caching options apply here
const res = await fetch('https://api.example.com/posts', {
  next: { revalidate: 3600 },
});
// This is a direct database query, not a fetch call
// cache and next options do not exist on this
const posts = await Post.find().lean();

Mixing these up is the most common source of confusion in the nextjs fetch cache conversation. The cache: 'force-cache' and next: { revalidate } options only exist on the native fetch function. Mongoose queries need a different caching approach entirely.

Caching a MongoDB Query in Next.js 15

Since fetch caching does not apply, caching a MongoDB query means using Next.js's unstable_cache function, or Next.js's newer use cache directive depending on your version, wrapped around the query itself:

// lib/queries/posts.ts
import { unstable_cache } from 'next/cache';
import { connectDB } from '@/lib/db';
import Post from '@/models/Post';

export const getPosts = unstable_cache(
  async () => {
    await connectDB();
    const posts = await Post.find().sort({ createdAt: -1 }).lean();
    return JSON.parse(JSON.stringify(posts));
  },
  ['posts-list'],
  { revalidate: 3600, tags: ['posts'] }
);
// app/blog/page.tsx
import { getPosts } from '@/lib/queries/posts';

export default async function BlogPage() {
  const posts = await getPosts();
  return (
    <div>
      {posts.map((post: any) => (
        <article key={post._id}>{post.title}</article>
      ))}
    </div>
  );
}

unstable_cache gives Mongoose queries the same revalidate and tag-based invalidation behavior that fetch gets natively. The JSON.parse(JSON.stringify(posts)) step matters here too, Mongoose documents are not plain serializable objects, and unstable_cache needs a plain object to store.

Invalidating a Cached MongoDB Query After a Mutation

Same pattern as tag-based fetch revalidation, just pointed at a Mongoose write instead of an API call:

// actions/posts.ts
'use server';
import { revalidateTag } from 'next/cache';
import { connectDB } from '@/lib/db';
import Post from '@/models/Post';

export async function createPost(formData: FormData) {
  await connectDB();
  await Post.create({
    title: formData.get('title'),
    content: formData.get('content'),
  });

  revalidateTag('posts');
}

Since the cached query was tagged posts, calling revalidateTag('posts') after the write clears exactly that cached result, so the blog page shows the new post on the next request instead of a stale cached list.

Should You Cache MongoDB Queries At All

Not always. For a personal blog with a handful of visitors, an uncached direct query on every request is fine and simpler to reason about. Caching earns its complexity once a query is expensive (heavy aggregation, large collection scans) or once traffic is high enough that repeating the same query on every request adds real load to the database.

A reasonable default is to leave simple queries uncached and add unstable_cache specifically to the ones that show up as slow once you actually look at real usage, rather than caching everything upfront.

Frequently Asked Questions

Does Next.js fetch caching work automatically with MongoDB? No. Fetch caching only applies to actual fetch() calls to HTTP endpoints. A direct Mongoose or MongoDB driver query needs unstable_cache or a manual caching layer, since it never goes through the fetch function at all.

Can I use Sanity CMS instead of MongoDB with Next.js? Yes, and the same caching distinction applies. Sanity's client typically fetches over HTTP, so standard Next.js fetch caching options can apply depending on how the client is configured, unlike a direct MongoDB connection which never touches fetch at all.

Why is my MongoDB data not updating after a change in Next.js? This is almost always one of two things: a cached query that has not been revalidated (fixed with revalidateTag after the mutation), or a page still using stale build-time data because it was statically generated instead of dynamically rendered.

What is the difference between fetch caching and unstable_cache in Next.js 15? fetch caching is built directly into the native fetch function and only works on real HTTP requests. unstable_cache is a general-purpose caching wrapper that works around any async function, including a Mongoose query, and needs to be applied manually rather than working automatically.


I use this exact connection caching and query caching setup, direct Mongoose queries wrapped in unstable_cache where it actually matters, across every MongoDB-backed Next.js project I build.

See it in a real codebase: https://neurodash-dashbord.vercel.app/

Get the templates: https://pixelanas.gumroad.com


Anas, full-stack Next.js developer building SaaS products and premium templates. X: @ASheikh69751