Documents, not rows
A MongoDB document is BSON (binary JSON) with an _id. Nested objects and arrays are allowed. You model how you read, not how a SQL third-normal-form table would look.
{
"_id": "ObjectId(...)",
"title": "Pattern Craft",
"href": "https://bg.reactbd.com/",
"featured": true,
"category": "Design",
"tags": ["backgrounds", "css"],
"createdAt": "2026-01-12T00:00:00.000Z"
}Collections
A collection is a bag of documents of one kind: users, tools, shops. Indexes live on the collection. Mixing unrelated shapes in one collection makes indexes and validation harder — don’t.
- Name collections in plural, lowercase:
tools, notTool. - Put a unique index on fields you look up:
email,slug. - Validation (
$jsonSchema) is optional but useful in production.
CRUD
| Need | Method |
|---|---|
| Insert one | insertOne(doc) |
| Insert many | insertMany(docs) |
| Find one | findOne({ slug }) |
| Find many | find(filter).sort().limit() |
| Update | updateOne(filter, { $set: { ... } }) |
| Delete | deleteOne(filter) |
const { db } = await connectToDatabase();
const tools = db.collection("tools");
await tools.updateOne(
{ slug: "pattern-craft" },
{ $set: { featured: true, updatedAt: new Date() } },
{ upsert: false },
);Queries
find filters, projection picks fields, sort + skip + limit paginate. Prefer indexed fields in the filter. Do not skip into deep pages on huge collections — use a range on _id or createdAt (cursor pagination).
db.tools.find(
{ featured: true, category: "Design" },
{ title: 1, href: 1, featured: 1 }
).sort({ createdAt: -1 }).limit(12)- Operators:
$in,$gte,$regex(use sparingly — leading-wildcard regex cannot use an index well). - Projection
{ title: 1 }is inclusion.{ passwordHash: 0 }is exclusion. Don’t mix both except_id. - Always define a sort when you paginate, or the order is undefined.
Indexes
Without an index, MongoDB scans the collection. With the right index, it jumps. Unique indexes also enforce uniqueness — that is your email constraint.
db.users.createIndex({ email: 1 }, { unique: true });
db.tools.createIndex({ featured: 1, createdAt: -1 });
db.tools.createIndex({ slug: 1 }, { unique: true });Aggregation
Pipelines reshape data: $match → $group → $sort → $project. Use them for counts, category breakdowns, and reports — not for fetching a single document by slug.
db.tools.aggregate([
{ $match: { published: true } },
{ $group: { _id: "$category", count: { $sum: 1 } } },
{ $sort: { _id: 1 } }
])Put $match first so later stages see fewer documents. Index the match fields.
Schema design
| Relationship | Default |
|---|---|
| One-to-few (address on user) | Embed. |
| One-to-many (products in a shop) | Reference _ids, or embed if always read together and bounded. |
| Many-to-many (posts ↔ tags) | Array of tag slugs if small; join collection if huge. |
| Events / logs | Separate collection. Never unbounded embed. |
ReactBD stores tools, users, shops, community in Mongo. Editorial posts and courses live in Sanity (or Neon for some blog tables). Pick the store by who edits it and how it is queried.
Node driver
The official driver is enough. Mongoose adds schemas and middleware — fine if the team wants it; this repo often uses the driver plus a cached connection for serverless/Next.
import { MongoClient } from "mongodb";
const uri = process.env.MONGODB_URI!;
const globalForMongo = globalThis as unknown as { _client?: MongoClient };
export async function connectToDatabase() {
const client =
globalForMongo._client ?? new MongoClient(uri);
if (!globalForMongo._client) {
globalForMongo._client = client;
await client.connect();
}
return { client, db: client.db() };
}Local vs Atlas
- Local: Docker or
mongodfor offline work. Same driver URI shape. - Atlas: managed replica set, IP allowlist or peering, backups.
- Connection string includes user, cluster host, and database name. Treat it as a secret.
- Never log the URI. Never put it in
NEXT_PUBLIC_*.
How ReactBD uses MongoDB
- Public tools catalog:
GET /api/public/toolsreads Mongo, featured-first in the UI. - Shops, community, licenses, users: Mongo collections with indexes on lookup keys.
- Health:
lib/db-healthpings Mongo separately from Neon and Sanity. - If the document is copy editors will change weekly — consider Sanity instead.