/ astro-integrations / How to Use MongoDB Atlas with Astro: Complete Guide
astro-integrations 10 min read

How to Use MongoDB Atlas with Astro: Complete Guide

Step-by-step guide to integrating MongoDB Atlas with your Astro website.

How to Use MongoDB Atlas with Astro: Complete Guide

MongoDB Atlas is a fully managed cloud MongoDB database service. If your data is document-shaped (nested objects, variable schemas, arrays of items), MongoDB is often a better fit than relational databases. Atlas handles the infrastructure: clustering, backups, scaling, and monitoring. You just write queries. For Astro developers, MongoDB Atlas is a solid choice when your content does not fit neatly into rows and columns, or when you want the flexibility to evolve your data model without migrations.

The integration uses MongoDB's official Node.js driver or Mongoose ODM in your Astro server-side code. You query Atlas from API routes, SSR pages, or build scripts and render the results.

Prerequisites

  • Node.js 22.12.0 or newer. Astro 6 declares engines.node as >=22.12.0, and the MongoDB driver 7.x and Mongoose 9.x both require >=20.19.0, so Node 22.12+ satisfies all three. Checked against the published package.json metadata on the npm registry.
  • An Astro project (npm create astro@latest)
  • A MongoDB Atlas account. The free M0 tier includes 512MB of storage with shared resources. If you outgrow it, the Flex tier runs roughly $8 to $30 per month, and dedicated clusters start at about $57 per month (M10).
  • A cluster created in Atlas with a database user and network access configured

Installation

Install the MongoDB Node.js driver. Versions below were the current latest on the npm registry when this guide was checked.

npm install mongodb@7.2.0

Or if you prefer Mongoose for schema validation and modeling:

npm install mongoose@9.6.3

If you want to render pages on demand (SSR), you also need an Astro adapter. For a Node server, install the official Node adapter:

npx astro add node

This pulls in @astrojs/node (10.x at time of writing, which lists Astro ^6.3.0 as a peer dependency) and wires the adapter into your config.

Configuration

Get your connection string from the Atlas dashboard. Go to Database > Connect > Drivers and copy the connection string.

Add it to .env:

MONGODB_URI=mongodb+srv://username:password@cluster0.abc123.mongodb.net/mydb?retryWrites=true&w=majority

Create a database client helper. With the native driver:

// src/lib/mongodb.ts
import { MongoClient } from "mongodb";

const uri = import.meta.env.MONGODB_URI;
const client = new MongoClient(uri);

let connected = false;

export async function getDb(dbName = "mydb") {
  if (!connected) {
    await client.connect();
    connected = true;
  }
  return client.db(dbName);
}

With Mongoose:

// src/lib/mongoose.ts
import mongoose from "mongoose";

const uri = import.meta.env.MONGODB_URI;

let isConnected = false;

export async function connectDB() {
  if (isConnected) return;
  await mongoose.connect(uri);
  isConnected = true;
}

Define a Mongoose model for your content:

// src/models/Post.ts
import mongoose from "mongoose";

const postSchema = new mongoose.Schema({
  title: { type: String, required: true },
  slug: { type: String, required: true, unique: true },
  content: String,
  excerpt: String,
  tags: [String],
  published: { type: Boolean, default: false },
  featuredImage: String,
  createdAt: { type: Date, default: Date.now },
  updatedAt: { type: Date, default: Date.now },
});

export const Post = mongoose.models.Post || mongoose.model("Post", postSchema);

Configure on-demand rendering in your Astro config. Note that output: "hybrid" no longer exists. It was removed in Astro 5, and the current Astro 6 config reference lists only "static" (the default) and "server". The recommended pattern for a mostly static site with a few dynamic routes is to leave output on its "static" default and opt individual pages or endpoints into on-demand rendering with export const prerender = false. You only need to add an adapter:

// astro.config.mjs
import { defineConfig } from "astro/config";
import node from "@astrojs/node";

export default defineConfig({
  // output defaults to "static"; no need to set it.
  // Pages stay prerendered unless they export `prerender = false`.
  adapter: node({ mode: "standalone" }),
});

If instead you want every page rendered on the server by default, set output: "server" and opt the static pages out with export const prerender = true. The official Astro docs recommend keeping the default "static" mode until you are sure most pages need on-demand rendering, so that you do not rely on a server function to serve content that could be prerendered.

Basic Usage

Query posts from MongoDB and render them. Using the native driver:

---
// src/pages/blog/index.astro
export const prerender = false;

import { getDb } from "../../lib/mongodb";
import BaseLayout from "../../layouts/BaseLayout.astro";

const db = await getDb();
const posts = await db
  .collection("posts")
  .find({ published: true })
  .sort({ createdAt: -1 })
  .limit(50)
  .toArray();
---

<BaseLayout title="Blog">
  <h1>Blog</h1>
  {posts.map((post) => (
    <article>
      <a href={`/blog/${post.slug}`}>
        <h2>{post.title}</h2>
        <p>{post.excerpt}</p>
        <time>{new Date(post.createdAt).toLocaleDateString()}</time>
      </a>
    </article>
  ))}
</BaseLayout>

Using Mongoose:

---
// src/pages/blog/index.astro
export const prerender = false;

import { connectDB } from "../../lib/mongoose";
import { Post } from "../../models/Post";
import BaseLayout from "../../layouts/BaseLayout.astro";

await connectDB();
const posts = await Post.find({ published: true })
  .sort({ createdAt: -1 })
  .limit(50)
  .lean();
---

<BaseLayout title="Blog">
  <h1>Blog</h1>
  {posts.map((post) => (
    <article>
      <a href={`/blog/${post.slug}`}>
        <h2>{post.title}</h2>
        <p>{post.excerpt}</p>
      </a>
    </article>
  ))}
</BaseLayout>

API route for creating posts:

// src/pages/api/posts.ts
import type { APIRoute } from "astro";
import { getDb } from "../../lib/mongodb";

export const POST: APIRoute = async ({ request }) => {
  const body = await request.json();
  const db = await getDb();

  const result = await db.collection("posts").insertOne({
    title: body.title,
    slug: body.slug,
    content: body.content,
    excerpt: body.excerpt,
    tags: body.tags || [],
    published: body.published || false,
    createdAt: new Date(),
    updatedAt: new Date(),
  });

  return new Response(
    JSON.stringify({ id: result.insertedId, success: true }),
    {
      status: 201,
      headers: { "Content-Type": "application/json" },
    }
  );
};

Production Tips

  1. Use Atlas Search for full-text search. Atlas includes a Lucene-based search engine. Create a search index on your collection and run $search aggregation queries. This eliminates the need for a separate search service like Algolia or Meilisearch.

  2. Enable connection pooling. The MongoDB driver handles connection pooling automatically, but configure maxPoolSize in your connection options for production. A pool size of 10-20 is usually sufficient for SSR Astro sites.

  3. Add indexes for query performance. Create indexes on fields you query frequently (slug, published, createdAt). Without indexes, MongoDB scans every document. Use db.collection.createIndex() or define them in Atlas's UI.

  4. Use Atlas's free tier wisely, and know that the old shared tiers are gone. The M0 free tier gives you 512MB with shared resources. It is fine for development and low-traffic sites. The legacy M2 and M5 shared tiers were retired (Atlas stopped supporting them on January 22, 2026) and replaced by the Flex tier, which costs roughly $8 to $30 per month and adds capabilities the old shared tiers lacked, including Atlas Search and Change Streams. For larger workloads, dedicated clusters start at the M10 tier (about $57 per month).

  • Implement pagination. For large collections, use cursor-based pagination with _id comparisons instead of skip/limit. This stays performant regardless of how many documents you have.

  • Alternatives to Consider

    • Supabase if you prefer a relational database (Postgres) with built-in auth and real-time features.
    • Neon if you want serverless Postgres with branching and autoscaling.
    • Firebase Firestore if you need real-time data sync and are building a more interactive application.

    Common Errors and Fixes

    output: "hybrid" throws a config error. This value was removed in Astro 5 and does not exist in Astro 6. The current config reference accepts only "static" (default) and "server". Delete the output: "hybrid" line and opt specific routes into on-demand rendering with export const prerender = false instead. See the on-demand rendering guide linked below.

    A page that queries MongoDB renders empty or stale at build time. In the default "static" mode, every page is prerendered. If you query Atlas inside a page without export const prerender = false, the query runs once at build time and the result is frozen into static HTML. Add export const prerender = false to the page (and make sure an adapter is configured) so the query runs per request.

    Cannot find adapter or "an adapter is required" build error. Any route that renders on demand needs an adapter. Run npx astro add node (or your host's adapter) so Astro can output a runtime entry point. The Astro on-demand rendering guide states an adapter is required to render any page on demand.

    Connection string fails with authentication or DNS errors. Copy the mongodb+srv:// string from the Atlas UI (Database, then Connect, then Drivers), replace the <db_password> placeholder with the real database-user password, and make sure your current IP is allowed under Atlas Network Access. The MongoDB Node.js get-started guide documents this exact flow.

    MongoServerSelectionError / timeout in serverless or per-request code. Do not create a new MongoClient on every request. The driver docs recommend creating a single MongoClient and reusing it for the application lifetime; the client manages its own connection pool. The helper in this guide keeps one client at module scope, which is the correct pattern. For Mongoose, guard mongoose.connect() with a cached flag (as shown above) so hot reloads and repeated invocations do not open duplicate connections.

    The connection string leaks into the client bundle. A database URI is a secret and must stay server-side. import.meta.env.MONGODB_URI works for server code, but for stronger guarantees Astro now offers the type-safe astro:env module. Declare the variable with envField.string({ context: "server", access: "secret" }) and read it with getSecret() from astro:env/server. Note that Astro does not allow client-context secret variables, which is exactly what you want here.

    Node version mismatch on install or build. Astro 6 requires Node >=22.12.0, and the MongoDB driver 7.x and Mongoose 9.x require >=20.19.0. Install or upgrade to Node 22.12+ to satisfy all of them.

    Official Docs and Examples

    Wrapping Up

    MongoDB Atlas and Astro work well together for content-driven applications, especially when your data has nested structures or evolves frequently. The flexible document model means you can add fields without migrations, and Atlas Search gives you full-text search without an additional service. The free tier is enough to get started and test your architecture, and the official driver is mature and well-maintained. For Astro sites that outgrow file-based content but need more flexibility than a traditional CMS, MongoDB Atlas is a practical choice.

    Sources

    All versions and facts below were checked on 2026-05-29.