MongoDB
Mongoose (Node) and Motor/Beanie (Python) — models and document patterns.
Use MongoDB for document-oriented data — activity feeds, flexible schemas, high-volume writes, or services that don't fit a relational model.
Install
pnpm add mongooseConnection
// src/db/mongo.ts
import mongoose from "mongoose";
export async function connectMongo() {
await mongoose.connect(process.env.MONGODB_URI!);
console.log("MongoDB connected");
}
mongoose.connection.on("error", (err) => {
console.error("MongoDB error:", err);
});Call in server.ts before starting Express:
import { connectMongo } from "./db/mongo";
await connectMongo();
app.listen(PORT, ...);Schema & model
// src/models/activity.model.ts
import { Schema, model, Document } from "mongoose";
export interface IActivity extends Document {
userId: string;
action: string;
metadata: Record<string, unknown>;
createdAt: Date;
}
const activitySchema = new Schema<IActivity>(
{
userId: { type: String, required: true, index: true },
action: { type: String, required: true },
metadata: { type: Schema.Types.Mixed, default: {} },
},
{ timestamps: { createdAt: true, updatedAt: false } },
);
activitySchema.index({ userId: 1, createdAt: -1 });
export const Activity = model<IActivity>("Activity", activitySchema);Usage in services
// src/services/activity.service.ts
import { Activity } from "../models/activity.model";
export async function logActivity(userId: string, action: string, metadata = {}) {
return Activity.create({ userId, action, metadata });
}
export async function getRecentActivity(userId: string, limit = 20) {
return Activity.find({ userId }).sort({ createdAt: -1 }).limit(limit).lean();
}Python (FastAPI) — Beanie
Python services use Beanie (async ODM built on Motor + Pydantic). Documents are declared as Pydantic models, mirroring the Mongoose schema above.
uv add beanie motor# app/models/activity.py
from datetime import datetime, timezone
import pymongo
from beanie import Document
from pydantic import Field
class Activity(Document):
user_id: str
action: str
metadata: dict = {}
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
class Settings:
name = "activities"
indexes = [
[("user_id", pymongo.ASCENDING), ("created_at", pymongo.DESCENDING)],
]# app/db/mongo.py
from beanie import init_beanie
from motor.motor_asyncio import AsyncIOMotorClient
from app.core.config import settings
from app.models.activity import Activity
async def connect_mongo() -> None:
client = AsyncIOMotorClient(settings.mongodb_uri)
await init_beanie(database=client.app, document_models=[Activity])# app/services/activity.py
from app.models.activity import Activity
async def log_activity(user_id: str, action: str, metadata: dict | None = None) -> Activity:
activity = Activity(user_id=user_id, action=action, metadata=metadata or {})
return await activity.insert()
async def get_recent_activity(user_id: str, limit: int = 20) -> list[Activity]:
return (
await Activity.find(Activity.user_id == user_id)
.sort(-Activity.created_at)
.limit(limit)
.to_list()
)Call connect_mongo() from the app lifespan startup, the same way connectMongo() runs before app.listen() on the Node side.
Environment
MONGODB_URI=mongodb://localhost:27017/app
# Production
MONGODB_URI=mongodb+srv://user:pass@cluster.mongodb.net/appLocal with Docker
# docker-compose.yml
services:
mongo:
image: mongo:7
ports: ["27017:27017"]
volumes: ["mongo_data:/data/db"]Best practices
- Always define indexes in the schema — don't rely on full collection scans
- Use
.lean()for read-only queries (returns plain objects, faster) - Avoid deep nesting — prefer flat documents with references for large arrays
- Use transactions for multi-document writes that must be atomic