
Using Document Databases
- 47 installs
- 426 repo stars
- Updated December 11, 2025
- ancoleman/ai-design-components
using-document-databases is a skill that guides selecting and implementing document databases like MongoDB, DynamoDB, and Firestore for flexible-schema applications.
About
A skill that guides selecting and implementing NoSQL document databases for flexible-schema applications. It compares MongoDB, DynamoDB, and Firestore, and covers embed-versus-reference schema design, indexing strategies, and MongoDB aggregation pipelines. A developer uses it when building content management, user profiles, catalogs, or event logging with JSON-like nested data.
- Selects and implements MongoDB, DynamoDB, or Firestore for flexible-schema apps
- Covers embed-vs-reference schema design, MongoDB indexing, and aggregation pipelines
- Ships FastAPI, Next.js, Lambda, and React example implementations
Using Document Databases by the numbers
- 47 all-time installs (skills.sh)
- Ranked #424 of 911 Databases skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
using-document-databases capabilities & compatibility
- Capabilities
- database · schema design · api development
- Works with
- mongodb · aws
- Use cases
- database · api development
What using-document-databases says it does
Document database implementation for flexible schema applications. Use when building content management, user profiles, catalogs, or event logging.
Covers MongoDB (primary), DynamoDB, Firestore, schema design patterns, indexing strategies, and aggregation pipelines.
npx skills add https://github.com/ancoleman/ai-design-components --skill using-document-databasesAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 47 |
|---|---|
| repo stars | ★ 426 |
| Last updated | December 11, 2025 |
| Repository | ancoleman/ai-design-components ↗ |
What it does
Pick and implement a document database (MongoDB, DynamoDB, or Firestore) for a flexible-schema application.
Who is it for?
Building content management, user profiles, catalogs, or event logging on a flexible schema
Skip if: Fixed relational schemas or time-series data at scale
When should I use this skill?
You are choosing or implementing a document database for JSON-like nested data
By the numbers
- 3 document databases compared (MongoDB, DynamoDB, Firestore)
- 5 MongoDB index types documented
- 4 multi-language example implementations
Files
Document Database Implementation
Guide NoSQL document database selection and implementation for flexible schema applications across Python, TypeScript, Rust, and Go.
When to Use This Skill
Use document databases when applications need:
- Flexible schemas - Data models evolve rapidly without migrations
- Nested structures - JSON-like hierarchical data
- Horizontal scaling - Built-in sharding and replication
- Developer velocity - Object-to-database mapping without ORM complexity
Database Selection
Quick Decision Framework
DEPLOYMENT ENVIRONMENT?
├── AWS-Native Application → DynamoDB
│ ✓ Serverless, auto-scaling, single-digit ms latency
│ ✗ Limited query flexibility
│
├── Firebase/GCP Ecosystem → Firestore
│ ✓ Real-time sync, offline support, mobile-first
│ ✗ More expensive for heavy reads
│
└── General-Purpose/Complex Queries → MongoDB
✓ Rich aggregation, full-text search, vector search
✓ ACID transactions, self-hosted or managedDatabase Comparison
| Database | Best For | Latency | Max Item | Query Language |
|---|---|---|---|---|
| MongoDB | General-purpose, complex queries | 1-5ms | 16MB | MQL (rich) |
| DynamoDB | AWS serverless, predictable performance | <10ms | 400KB | PartiQL (limited) |
| Firestore | Real-time apps, mobile-first | 50-200ms | 1MB | Firebase queries |
See references/mongodb.md for MongoDB details See references/dynamodb.md for DynamoDB single-table design See references/firestore.md for Firestore real-time patterns
Schema Design Patterns
Embedding vs Referencing
Use the decision matrix in `references/schema-design-patterns.md`
Quick guide:
| Relationship | Pattern | Example |
|---|---|---|
| One-to-Few | Embed | User addresses (2-3 max) |
| One-to-Many | Hybrid | Blog posts → comments |
| One-to-Millions | Reference | User → events (logging) |
| Many-to-Many | Reference | Products ↔ Categories |
Embedding Example (MongoDB)
// User with embedded addresses
{
_id: ObjectId("..."),
email: "user@example.com",
name: "Jane Doe",
addresses: [
{
type: "home",
street: "123 Main St",
city: "Boston",
default: true
}
],
preferences: {
theme: "dark",
notifications: { email: true, sms: false }
}
}Referencing Example (E-commerce)
// Orders reference products
{
_id: ObjectId("..."),
userId: ObjectId("..."),
items: [
{
productId: ObjectId("..."), // Reference
priceAtPurchase: 49.99, // Denormalize (historical)
quantity: 2
}
],
totalAmount: 99.98
}When to denormalize:
- Frequently read together
- Historical snapshots (prices, names)
- Read-heavy workloads
Indexing Strategies
MongoDB Index Types
// 1. Single field (unique email)
db.users.createIndex({ email: 1 }, { unique: true })
// 2. Compound index (ORDER MATTERS!)
db.orders.createIndex({ status: 1, createdAt: -1 })
// 3. Partial index (index subset)
db.orders.createIndex(
{ userId: 1 },
{ partialFilterExpression: { status: { $eq: "pending" }}}
)
// 4. TTL index (auto-delete after 30 days)
db.sessions.createIndex(
{ createdAt: 1 },
{ expireAfterSeconds: 2592000 }
)
// 5. Text index (full-text search)
db.articles.createIndex({
title: "text",
content: "text"
})Index Best Practices:
- Add indexes for all query filters
- Compound index order: Equality → Range → Sort
- Use covering indexes (query + projection in index)
- Use
explain()to verify index usage - Monitor with Performance Advisor (Atlas)
Validate indexes with the script:
python scripts/validate_indexes.pySee references/indexing-strategies.md for complete guide.
MongoDB Aggregation Pipelines
Key Operators: $match (filter), $group (aggregate), $lookup (join), $unwind (arrays), $project (reshape)
For complete pipeline patterns and examples, see: references/aggregation-patterns.md
DynamoDB Single-Table Design
Design for access patterns using PK/SK patterns. Store multiple entity types in one table with composite keys.
For complete single-table design patterns and GSI strategies, see: references/dynamodb.md
Firestore Real-Time Patterns
Use onSnapshot() for real-time listeners and Firestore security rules for access control.
For complete real-time patterns and security rules, see: references/firestore.md
Multi-Language Examples
Complete implementations available in `examples/` directory:
examples/mongodb-fastapi/- Python FastAPI + MongoDBexamples/mongodb-nextjs/- TypeScript Next.js + MongoDBexamples/dynamodb-serverless/- Python Lambda + DynamoDBexamples/firestore-react/- React + Firestore real-time
Frontend Skill Integration
- Media Skill - Use MongoDB GridFS for large file storage with metadata
- AI Chat Skill - MongoDB Atlas Vector Search for semantic conversation retrieval
- Feedback Skill - DynamoDB for high-throughput event logging with TTL
For integration examples, see: references/skill-integrations.md
Performance Optimization
Key practices:
- Always use indexes for query filters (verify with
.explain()) - Use connection pooling (reuse clients across requests)
- Avoid collection scans in production
For complete optimization guide, see: references/performance.md
Common Patterns
Pagination: Use cursor-based pagination for large datasets (recommended over offset) Soft Deletes: Mark as deleted with timestamp instead of removing Audit Logs: Store version history within documents
For implementation details, see: references/common-patterns.md
Validation and Scripts
Validate Index Coverage
# Run validation script
python scripts/validate_indexes.py --db myapp --collection orders
# Output:
# ✓ Query { status: "pending" } covered by index status_1
# ✗ Query { userId: "..." } missing index - add: { userId: 1 }Schema Analysis
# Analyze schema patterns
python scripts/analyze_schema.py --db myapp
# Output:
# Collection: users
# - Average document size: 2.4 KB
# - Embedding ratio: 87% (addresses, preferences)
# - Reference ratio: 13% (orderIds)
# Recommendation: Good balanceAnti-Patterns to Avoid
Unbounded Arrays: Limit embedded arrays (use references for large collections) Over-Indexing: Only index queried fields (indexes slow writes) DynamoDB Scans: Always use Query with partition key (avoid Scan)
For detailed anti-patterns, see: references/anti-patterns.md
Dependencies
Python
# MongoDB
pip install motor pymongo
# DynamoDB
pip install boto3
# Firestore
pip install firebase-adminTypeScript
# MongoDB
npm install mongodb
# DynamoDB
npm install @aws-sdk/client-dynamodb @aws-sdk/util-dynamodb
# Firestore
npm install firebase firebase-adminRust
# MongoDB
mongodb = "2.8"
# DynamoDB
aws-sdk-dynamodb = "1.0"Go
# MongoDB
go get go.mongodb.org/mongo-driver
# DynamoDB
go get github.com/aws/aws-sdk-go-v2/service/dynamodbAdditional Resources
Database-Specific Guides:
references/mongodb.md- Complete MongoDB documentationreferences/dynamodb.md- DynamoDB single-table patternsreferences/firestore.md- Firestore real-time guide
Pattern Guides:
references/schema-design-patterns.md- Embedding vs referencing decisionsreferences/indexing-strategies.md- Index optimizationreferences/aggregation-patterns.md- MongoDB pipeline cookbookreferences/common-patterns.md- Pagination, soft deletes, audit logsreferences/anti-patterns.md- Mistakes to avoidreferences/performance.md- Query optimizationreferences/skill-integrations.md- Frontend skill integration
Examples: examples/mongodb-fastapi/, examples/mongodb-nextjs/, examples/dynamodb-serverless/, examples/firestore-react/
"""
DynamoDB + AWS Lambda Serverless Example
Production-ready serverless API with single-table design.
"""
import json
import os
import boto3
from boto3.dynamodb.conditions import Key
from datetime import datetime
from decimal import Decimal
from typing import Any, Dict
# DynamoDB client
dynamodb = boto3.resource('dynamodb', region_name=os.getenv('AWS_REGION', 'us-east-1'))
table = dynamodb.Table(os.getenv('TABLE_NAME', 'AppData'))
class DecimalEncoder(json.JSONEncoder):
"""Custom JSON encoder for Decimal types"""
def default(self, obj):
if isinstance(obj, Decimal):
return float(obj)
return super(DecimalEncoder, self).default(obj)
def response(status_code: int, body: Dict[str, Any]) -> Dict[str, Any]:
"""Generate API Gateway response"""
return {
'statusCode': status_code,
'headers': {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
'body': json.dumps(body, cls=DecimalEncoder)
}
def create_user(event, context):
"""
Create a new user
POST /users
Body: { "email": "user@example.com", "name": "Jane Doe" }
"""
try:
body = json.loads(event['body'])
email = body['email']
name = body['name']
user_id = f"USER#{email.replace('@', '_at_')}"
# Put user metadata
table.put_item(
Item={
'PK': user_id,
'SK': 'METADATA',
'email': email,
'name': name,
'createdAt': datetime.utcnow().isoformat(),
'updatedAt': datetime.utcnow().isoformat()
},
ConditionExpression='attribute_not_exists(PK)' # Prevent duplicates
)
return response(201, {
'message': 'User created',
'userId': user_id,
'email': email,
'name': name
})
except table.meta.client.exceptions.ConditionalCheckFailedException:
return response(409, {'error': 'User already exists'})
except Exception as e:
return response(500, {'error': str(e)})
def get_user(event, context):
"""
Get user by email
GET /users/{email}
"""
try:
email = event['pathParameters']['email']
user_id = f"USER#{email.replace('@', '_at_')}"
result = table.get_item(
Key={'PK': user_id, 'SK': 'METADATA'}
)
if 'Item' not in result:
return response(404, {'error': 'User not found'})
return response(200, result['Item'])
except Exception as e:
return response(500, {'error': str(e)})
def create_order(event, context):
"""
Create a new order
POST /orders
Body: {
"userId": "USER#user_at_example_com",
"items": [...],
"totalAmount": 249.97
}
"""
try:
body = json.loads(event['body'])
user_id = body['userId']
items = body['items']
total_amount = Decimal(str(body['totalAmount']))
# Verify user exists
user = table.get_item(Key={'PK': user_id, 'SK': 'METADATA'})
if 'Item' not in user:
return response(404, {'error': 'User not found'})
# Generate order ID
timestamp = datetime.utcnow().isoformat()
order_id = f"ORDER#{timestamp.replace(':', '-')}"
order_number = f"ORD-{timestamp[:10]}-{timestamp[11:13]}{timestamp[14:16]}"
# Single-table design: multiple items
with table.batch_writer() as batch:
# 1. Order metadata (PK: ORDER#..., SK: METADATA)
batch.put_item(Item={
'PK': order_id,
'SK': 'METADATA',
'userId': user_id,
'orderNumber': order_number,
'totalAmount': total_amount,
'status': 'pending',
'createdAt': timestamp
})
# 2. User's order reference (PK: USER#..., SK: ORDER#...)
batch.put_item(Item={
'PK': user_id,
'SK': order_id,
'orderNumber': order_number,
'totalAmount': total_amount,
'status': 'pending',
'createdAt': timestamp
})
# 3. Order items (PK: ORDER#..., SK: ITEM#...)
for idx, item in enumerate(items):
batch.put_item(Item={
'PK': order_id,
'SK': f"ITEM#{idx:03d}",
'productId': item['productId'],
'quantity': item['quantity'],
'price': Decimal(str(item['price'])),
'name': item['name']
})
return response(201, {
'message': 'Order created',
'orderId': order_id,
'orderNumber': order_number
})
except Exception as e:
return response(500, {'error': str(e)})
def get_order(event, context):
"""
Get order by ID (including all items)
GET /orders/{orderId}
"""
try:
order_id = event['pathParameters']['orderId']
# Query all items for this order
result = table.query(
KeyConditionExpression=Key('PK').eq(order_id)
)
if not result['Items']:
return response(404, {'error': 'Order not found'})
# Separate metadata and items
order_data = None
items = []
for item in result['Items']:
if item['SK'] == 'METADATA':
order_data = item
elif item['SK'].startswith('ITEM#'):
items.append(item)
if not order_data:
return response(404, {'error': 'Order not found'})
order_data['items'] = items
return response(200, order_data)
except Exception as e:
return response(500, {'error': str(e)})
def list_user_orders(event, context):
"""
List all orders for a user
GET /users/{userId}/orders
"""
try:
user_id = event['pathParameters']['userId']
# Query all orders for user (PK = USER#..., SK begins_with ORDER#)
result = table.query(
KeyConditionExpression=Key('PK').eq(user_id) & Key('SK').begins_with('ORDER#'),
ScanIndexForward=False # Descending order (most recent first)
)
return response(200, {
'userId': user_id,
'orderCount': result['Count'],
'orders': result['Items']
})
except Exception as e:
return response(500, {'error': str(e)})
def update_order_status(event, context):
"""
Update order status
PUT /orders/{orderId}/status
Body: { "status": "shipped" }
"""
try:
order_id = event['pathParameters']['orderId']
body = json.loads(event['body'])
new_status = body['status']
# Update order metadata
result = table.update_item(
Key={'PK': order_id, 'SK': 'METADATA'},
UpdateExpression='SET #status = :status, updatedAt = :updatedAt',
ExpressionAttributeNames={'#status': 'status'},
ExpressionAttributeValues={
':status': new_status,
':updatedAt': datetime.utcnow().isoformat()
},
ConditionExpression='attribute_exists(PK)',
ReturnValues='ALL_NEW'
)
# Also update user's order reference
user_id = result['Attributes']['userId']
table.update_item(
Key={'PK': user_id, 'SK': order_id},
UpdateExpression='SET #status = :status',
ExpressionAttributeNames={'#status': 'status'},
ExpressionAttributeValues={':status': new_status}
)
return response(200, {
'message': 'Order status updated',
'order': result['Attributes']
})
except table.meta.client.exceptions.ConditionalCheckFailedException:
return response(404, {'error': 'Order not found'})
except Exception as e:
return response(500, {'error': str(e)})
def list_orders_by_status(event, context):
"""
List orders by status using GSI
GET /orders/status/{status}
Requires GSI: StatusIndex (PK: status, SK: createdAt)
"""
try:
status_value = event['pathParameters']['status']
# Query GSI
result = table.query(
IndexName='StatusIndex',
KeyConditionExpression=Key('status').eq(status_value),
ScanIndexForward=False # Most recent first
)
return response(200, {
'status': status_value,
'count': result['Count'],
'orders': result['Items']
})
except Exception as e:
return response(500, {'error': str(e)})
def health_check(event, context):
"""Health check endpoint"""
try:
# Verify table exists
table.table_status
return response(200, {
'status': 'healthy',
'service': 'DynamoDB Lambda API',
'tableName': table.table_name
})
except Exception as e:
return response(500, {
'status': 'unhealthy',
'error': str(e)
})
service: dynamodb-api
provider:
name: aws
runtime: python3.11
region: us-east-1
environment:
TABLE_NAME: ${self:service}-${sls:stage}
AWS_REGION: ${self:provider.region}
iam:
role:
statements:
- Effect: Allow
Action:
- dynamodb:Query
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:UpdateItem
- dynamodb:DeleteItem
- dynamodb:BatchWriteItem
Resource:
- !GetAtt AppDataTable.Arn
- !Sub "${AppDataTable.Arn}/index/*"
functions:
# Health check
health:
handler: handler.health_check
events:
- http:
path: /health
method: get
cors: true
# User endpoints
createUser:
handler: handler.create_user
events:
- http:
path: /users
method: post
cors: true
getUser:
handler: handler.get_user
events:
- http:
path: /users/{email}
method: get
cors: true
# Order endpoints
createOrder:
handler: handler.create_order
events:
- http:
path: /orders
method: post
cors: true
getOrder:
handler: handler.get_order
events:
- http:
path: /orders/{orderId}
method: get
cors: true
listUserOrders:
handler: handler.list_user_orders
events:
- http:
path: /users/{userId}/orders
method: get
cors: true
updateOrderStatus:
handler: handler.update_order_status
events:
- http:
path: /orders/{orderId}/status
method: put
cors: true
listOrdersByStatus:
handler: handler.list_orders_by_status
events:
- http:
path: /orders/status/{status}
method: get
cors: true
resources:
Resources:
AppDataTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: ${self:service}-${sls:stage}
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: PK
AttributeType: S
- AttributeName: SK
AttributeType: S
- AttributeName: status
AttributeType: S
- AttributeName: createdAt
AttributeType: S
KeySchema:
- AttributeName: PK
KeyType: HASH
- AttributeName: SK
KeyType: RANGE
GlobalSecondaryIndexes:
- IndexName: StatusIndex
KeySchema:
- AttributeName: status
KeyType: HASH
- AttributeName: createdAt
KeyType: RANGE
Projection:
ProjectionType: ALL
StreamSpecification:
StreamViewType: NEW_AND_OLD_IMAGES
Tags:
- Key: Environment
Value: ${sls:stage}
- Key: Service
Value: ${self:service}
plugins:
- serverless-python-requirements
custom:
pythonRequirements:
dockerizePip: true
layer: true
Firestore + React Real-Time Example
React application with Firestore real-time listeners, offline support, and security rules.
Stack
- React 18
- Firebase/Firestore
- Real-time listeners (onSnapshot)
- Offline persistence
- Firestore Security Rules
- TypeScript
Features
- Real-time data synchronization
- Offline support with local cache
- Optimistic updates
- Security rules enforcement
- Subcollection queries
Project Structure
firestore-react/
├── src/
│ ├── firebase.ts # Firebase configuration
│ ├── hooks/
│ │ ├── useCollection.ts # Real-time collection hook
│ │ └── useDocument.ts # Real-time document hook
│ ├── components/
│ │ ├── PostList.tsx
│ │ └── CreatePost.tsx
│ └── App.tsx
├── firestore.rules # Security rules
└── package.jsonQuick Start
# Install
npm install firebase
# Configure Firebase (create project at console.firebase.google.com)
# Add config to .env
# Run
npm run devFirebase Setup
// src/firebase.ts
import { initializeApp } from 'firebase/app';
import { getFirestore, enableIndexedDbPersistence } from 'firebase/firestore';
const firebaseConfig = {
apiKey: process.env.REACT_APP_FIREBASE_API_KEY,
authDomain: "myapp.firebaseapp.com",
projectId: "myapp",
storageBucket: "myapp.appspot.com",
};
const app = initializeApp(firebaseConfig);
export const db = getFirestore(app);
// Enable offline persistence
enableIndexedDbPersistence(db).catch((err) => {
if (err.code === 'failed-precondition') {
console.warn('Multiple tabs open, persistence can only be enabled in one tab');
}
});Real-Time Hooks
// hooks/useCollection.ts
import { useEffect, useState } from 'react';
import { collection, query, onSnapshot, QueryConstraint } from 'firebase/firestore';
import { db } from '../firebase';
export function useCollection<T>(
collectionName: string,
...queryConstraints: QueryConstraint[]
) {
const [data, setData] = useState<T[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
const q = query(collection(db, collectionName), ...queryConstraints);
const unsubscribe = onSnapshot(
q,
(snapshot) => {
const items = snapshot.docs.map((doc) => ({
id: doc.id,
...doc.data(),
})) as T[];
setData(items);
setLoading(false);
},
(err) => {
setError(err);
setLoading(false);
}
);
return () => unsubscribe();
}, [collectionName]);
return { data, loading, error };
}Real-Time Component
// components/PostList.tsx
import { useCollection } from '../hooks/useCollection';
import { orderBy, limit } from 'firebase/firestore';
interface Post {
id: string;
title: string;
content: string;
createdAt: Date;
}
export function PostList() {
const { data: posts, loading } = useCollection<Post>(
'posts',
orderBy('createdAt', 'desc'),
limit(20)
);
if (loading) return <div>Loading...</div>;
return (
<div>
{posts.map((post) => (
<div key={post.id}>
<h2>{post.title}</h2>
<p>{post.content}</p>
</div>
))}
</div>
);
}CRUD Operations
import {
collection,
addDoc,
updateDoc,
deleteDoc,
doc,
serverTimestamp,
} from 'firebase/firestore';
// Create
const createPost = async (title: string, content: string) => {
await addDoc(collection(db, 'posts'), {
title,
content,
createdAt: serverTimestamp(),
userId: currentUser.uid,
});
};
// Update
const updatePost = async (postId: string, updates: Partial<Post>) => {
await updateDoc(doc(db, 'posts', postId), {
...updates,
updatedAt: serverTimestamp(),
});
};
// Delete
const deletePost = async (postId: string) => {
await deleteDoc(doc(db, 'posts', postId));
};Security Rules
// firestore.rules
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Posts: Read public, write authenticated
match /posts/{postId} {
allow read: if true;
allow create: if request.auth != null
&& request.resource.data.userId == request.auth.uid;
allow update, delete: if request.auth != null
&& resource.data.userId == request.auth.uid;
}
// Comments: Nested under posts
match /posts/{postId}/comments/{commentId} {
allow read: if true;
allow create: if request.auth != null;
allow update, delete: if request.auth.uid == resource.data.userId;
}
// User profiles: Read public, write owner only
match /users/{userId} {
allow read: if true;
allow write: if request.auth.uid == userId;
}
}
}Optimistic Updates
import { doc, updateDoc, writeBatch } from 'firebase/firestore';
function PostActions({ postId }: { postId: string }) {
const [likes, setLikes] = useState(0);
const handleLike = async () => {
// Optimistic update
setLikes((prev) => prev + 1);
try {
await updateDoc(doc(db, 'posts', postId), {
likes: increment(1),
});
} catch (error) {
// Rollback on error
setLikes((prev) => prev - 1);
}
};
return <button onClick={handleLike}>❤️ {likes}</button>;
}Batch Operations
import { writeBatch, doc } from 'firebase/firestore';
const batch = writeBatch(db);
// Add operations to batch
batch.set(doc(db, 'users', 'user1'), { name: 'Alice' });
batch.update(doc(db, 'posts', 'post1'), { views: increment(1) });
batch.delete(doc(db, 'temp', 'temp1'));
// Commit atomically
await batch.commit();Real-Time Presence
import { onDisconnect, ref, set } from 'firebase/database';
import { getDatabase } from 'firebase/database';
const rtdb = getDatabase();
const userStatusRef = ref(rtdb, `/status/${currentUser.uid}`);
// Set online
await set(userStatusRef, {
state: 'online',
lastSeen: serverTimestamp(),
});
// Set offline on disconnect
onDisconnect(userStatusRef).set({
state: 'offline',
lastSeen: serverTimestamp(),
});Integration Summary
- Real-time updates - onSnapshot for live data
- Offline support - IndexedDB persistence
- Security - Firestore rules enforce access control
- Optimistic UI - Update UI before server confirms
- Batch writes - Atomic multi-document operations
Best Practices
1. Enable offline persistence - Better UX on poor connections 2. Security rules - Never trust client, validate server-side 3. Optimize queries - Create indexes for filtered/sorted fields 4. Limit listeners - Unsubscribe when component unmounts 5. Handle errors - Network failures, permission denied 6. Batch writes - Atomic multi-operation updates 7. Denormalize - Duplicate data for read performance
Resources
- Firestore Docs: https://firebase.google.com/docs/firestore
- React Fire: https://github.com/FirebaseExtended/reactfire
- Security Rules: https://firebase.google.com/docs/firestore/security/get-started
"""
MongoDB + FastAPI Example
Production-ready REST API with async MongoDB integration.
"""
from fastapi import FastAPI, HTTPException, status
from motor.motor_asyncio import AsyncIOMotorClient
from pydantic import BaseModel, EmailStr, Field
from typing import List, Optional
from datetime import datetime
from bson import ObjectId
import os
# Pydantic models
class PyObjectId(ObjectId):
"""Custom ObjectId type for Pydantic"""
@classmethod
def __get_validators__(cls):
yield cls.validate
@classmethod
def validate(cls, v):
if not ObjectId.is_valid(v):
raise ValueError("Invalid ObjectId")
return ObjectId(v)
@classmethod
def __modify_schema__(cls, field_schema):
field_schema.update(type="string")
class UserCreate(BaseModel):
"""User creation request"""
email: EmailStr
name: str
age: Optional[int] = None
class UserResponse(BaseModel):
"""User response model"""
id: str = Field(alias="_id")
email: str
name: str
age: Optional[int] = None
createdAt: datetime
updatedAt: datetime
class Config:
populate_by_name = True
json_encoders = {ObjectId: str}
class OrderCreate(BaseModel):
"""Order creation request"""
userId: str
items: List[dict]
totalAmount: float
class OrderResponse(BaseModel):
"""Order response model"""
id: str = Field(alias="_id")
userId: str
orderNumber: str
items: List[dict]
totalAmount: float
status: str
createdAt: datetime
class Config:
populate_by_name = True
json_encoders = {ObjectId: str}
# FastAPI application
app = FastAPI(title="MongoDB FastAPI Example")
# MongoDB client (initialized on startup)
mongodb_client: Optional[AsyncIOMotorClient] = None
db = None
@app.on_event("startup")
async def startup_db_client():
"""Initialize MongoDB connection on startup"""
global mongodb_client, db
mongodb_uri = os.getenv("MONGODB_URI", "mongodb://localhost:27017/")
mongodb_client = AsyncIOMotorClient(
mongodb_uri,
maxPoolSize=50,
minPoolSize=10,
serverSelectionTimeoutMS=5000
)
db = mongodb_client.myapp
# Create indexes
await db.users.create_index("email", unique=True)
await db.orders.create_index([("userId", 1), ("createdAt", -1)])
print("✓ Connected to MongoDB")
@app.on_event("shutdown")
async def shutdown_db_client():
"""Close MongoDB connection on shutdown"""
global mongodb_client
if mongodb_client:
mongodb_client.close()
print("✓ Disconnected from MongoDB")
# User endpoints
@app.post("/users", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
async def create_user(user: UserCreate):
"""Create a new user"""
user_dict = user.dict()
user_dict["createdAt"] = datetime.utcnow()
user_dict["updatedAt"] = datetime.utcnow()
try:
result = await db.users.insert_one(user_dict)
created_user = await db.users.find_one({"_id": result.inserted_id})
return created_user
except Exception as e:
if "duplicate key error" in str(e):
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="User with this email already exists"
)
raise HTTPException(status_code=500, detail=str(e))
@app.get("/users/{email}", response_model=UserResponse)
async def get_user(email: str):
"""Get user by email"""
user = await db.users.find_one({"email": email})
if not user:
raise HTTPException(status_code=404, detail="User not found")
return user
@app.get("/users", response_model=List[UserResponse])
async def list_users(skip: int = 0, limit: int = 20):
"""List all users with pagination"""
users = await db.users.find().skip(skip).limit(limit).to_list(length=limit)
return users
@app.put("/users/{email}", response_model=UserResponse)
async def update_user(email: str, user_update: dict):
"""Update user by email"""
user_update["updatedAt"] = datetime.utcnow()
result = await db.users.update_one(
{"email": email},
{"$set": user_update}
)
if result.matched_count == 0:
raise HTTPException(status_code=404, detail="User not found")
updated_user = await db.users.find_one({"email": email})
return updated_user
@app.delete("/users/{email}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_user(email: str):
"""Soft delete user (mark as deleted)"""
result = await db.users.update_one(
{"email": email},
{"$set": {"deleted": True, "deletedAt": datetime.utcnow()}}
)
if result.matched_count == 0:
raise HTTPException(status_code=404, detail="User not found")
# Order endpoints
@app.post("/orders", response_model=OrderResponse, status_code=status.HTTP_201_CREATED)
async def create_order(order: OrderCreate):
"""Create a new order"""
# Verify user exists
user = await db.users.find_one({"_id": ObjectId(order.userId)})
if not user:
raise HTTPException(status_code=404, detail="User not found")
# Generate order number
order_count = await db.orders.count_documents({})
order_number = f"ORD-{datetime.utcnow().year}-{order_count + 1:06d}"
order_dict = order.dict()
order_dict["userId"] = ObjectId(order_dict["userId"])
order_dict["orderNumber"] = order_number
order_dict["status"] = "pending"
order_dict["createdAt"] = datetime.utcnow()
result = await db.orders.insert_one(order_dict)
created_order = await db.orders.find_one({"_id": result.inserted_id})
created_order["userId"] = str(created_order["userId"])
return created_order
@app.get("/orders/{order_number}", response_model=OrderResponse)
async def get_order(order_number: str):
"""Get order by order number"""
order = await db.orders.find_one({"orderNumber": order_number})
if not order:
raise HTTPException(status_code=404, detail="Order not found")
order["userId"] = str(order["userId"])
return order
@app.get("/users/{user_id}/orders", response_model=List[OrderResponse])
async def list_user_orders(user_id: str):
"""List all orders for a user"""
orders = await db.orders.find(
{"userId": ObjectId(user_id)}
).sort("createdAt", -1).to_list(length=100)
for order in orders:
order["userId"] = str(order["userId"])
return orders
# Analytics endpoint (aggregation example)
@app.get("/analytics/revenue-by-user")
async def revenue_by_user():
"""Get total revenue by user (aggregation pipeline)"""
pipeline = [
# Group by userId
{
"$group": {
"_id": "$userId",
"totalRevenue": {"$sum": "$totalAmount"},
"orderCount": {"$sum": 1},
"avgOrderValue": {"$avg": "$totalAmount"}
}
},
# Lookup user details
{
"$lookup": {
"from": "users",
"localField": "_id",
"foreignField": "_id",
"as": "user"
}
},
# Unwind user array
{"$unwind": "$user"},
# Project final structure
{
"$project": {
"_id": 0,
"userId": {"$toString": "$_id"},
"userName": "$user.name",
"userEmail": "$user.email",
"totalRevenue": {"$round": ["$totalRevenue", 2]},
"orderCount": 1,
"avgOrderValue": {"$round": ["$avgOrderValue", 2]}
}
},
# Sort by revenue
{"$sort": {"totalRevenue": -1}}
]
results = await db.orders.aggregate(pipeline).to_list(length=100)
return results
@app.get("/health")
async def health_check():
"""Health check endpoint"""
try:
# Ping MongoDB
await mongodb_client.admin.command('ping')
return {"status": "healthy", "database": "connected"}
except Exception as e:
return {"status": "unhealthy", "error": str(e)}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
fastapi==0.104.1
motor==3.3.2
pymongo==4.6.0
pydantic[email]==2.5.0
uvicorn[standard]==0.24.0
python-dotenv==1.0.0
MongoDB + Next.js Example
Full-stack Next.js 14 application with MongoDB, demonstrating CRUD operations, server components, and real-time updates.
Stack
- Next.js 14 (App Router)
- MongoDB (with Motor/PyMongo or native Node driver)
- Server Components + API Routes
- TypeScript
- Tailwind CSS
Features
- CRUD operations (Create, Read, Update, Delete)
- Server-side data fetching
- API routes for database operations
- Optimistic updates
- Form validation
- Error handling
Project Structure
mongodb-nextjs/
├── app/
│ ├── page.tsx # Home page (Server Component)
│ ├── posts/
│ │ ├── page.tsx # Posts list
│ │ ├── [id]/page.tsx # Post detail
│ │ └── new/page.tsx # Create post
│ └── api/
│ └── posts/
│ ├── route.ts # GET /api/posts, POST /api/posts
│ └── [id]/
│ └── route.ts # GET/PUT/DELETE /api/posts/:id
├── lib/
│ └── mongodb.ts # MongoDB client singleton
├── models/
│ └── post.ts # TypeScript types
└── package.jsonQuick Start
# Install
npm install mongodb
# Configure
cp .env.example .env
# Set MONGODB_URI=mongodb://localhost:27017/myapp
# Run
npm run devMongoDB Connection
// lib/mongodb.ts
import { MongoClient } from 'mongodb';
if (!process.env.MONGODB_URI) {
throw new Error('Please add MONGODB_URI to .env');
}
const uri = process.env.MONGODB_URI;
const options = {};
let client: MongoClient;
let clientPromise: Promise<MongoClient>;
if (process.env.NODE_ENV === 'development') {
// Preserve client across hot reloads
if (!(global as any)._mongoClientPromise) {
client = new MongoClient(uri, options);
(global as any)._mongoClientPromise = client.connect();
}
clientPromise = (global as any)._mongoClientPromise;
} else {
client = new MongoClient(uri, options);
clientPromise = client.connect();
}
export default clientPromise;Server Component (Read)
// app/posts/page.tsx
import clientPromise from '@/lib/mongodb';
export default async function PostsPage() {
const client = await clientPromise;
const db = client.db('myapp');
const posts = await db.collection('posts')
.find({})
.sort({ createdAt: -1 })
.limit(20)
.toArray();
return (
<div>
<h1>Posts</h1>
{posts.map((post) => (
<PostCard key={post._id.toString()} post={post} />
))}
</div>
);
}API Route (Create)
// app/api/posts/route.ts
import { NextRequest, NextResponse } from 'next/server';
import clientPromise from '@/lib/mongodb';
export async function POST(request: NextRequest) {
const { title, content } = await request.json();
const client = await clientPromise;
const db = client.db('myapp');
const result = await db.collection('posts').insertOne({
title,
content,
createdAt: new Date(),
updatedAt: new Date(),
});
return NextResponse.json({
id: result.insertedId.toString(),
title,
content,
}, { status: 201 });
}
export async function GET() {
const client = await clientPromise;
const db = client.db('myapp');
const posts = await db.collection('posts')
.find({})
.sort({ createdAt: -1 })
.toArray();
return NextResponse.json({ posts });
}Client Component (Optimistic Update)
'use client';
import { useMutation, useQueryClient } from '@tanstack/react-query';
function CreatePostForm() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (newPost) => fetch('/api/posts', {
method: 'POST',
body: JSON.stringify(newPost),
}).then(r => r.json()),
onMutate: async (newPost) => {
// Optimistic update
await queryClient.cancelQueries({ queryKey: ['posts'] });
const previous = queryClient.getQueryData(['posts']);
queryClient.setQueryData(['posts'], (old: any) => [
...old,
{ ...newPost, _id: 'temp-id' },
]);
return { previous };
},
onError: (err, newPost, context) => {
// Rollback on error
queryClient.setQueryData(['posts'], context?.previous);
},
onSuccess: () => {
// Refetch to get server data
queryClient.invalidateQueries({ queryKey: ['posts'] });
},
});
return <form onSubmit={(e) => {
e.preventDefault();
mutation.mutate({ title: '...', content: '...' });
}} />;
}Integration Summary
| Frontend Skill | MongoDB Pattern | Backend Pattern |
|---|---|---|
| Forms | insertOne, updateOne | POST/PUT API routes with validation |
| Tables | find() with cursor pagination | GET with cursor parameter |
| Search | $text index + aggregation | Text search + filters API |
| Media | GridFS | Upload/download with streams |
| Dashboards | Aggregation pipeline | GET with date ranges, grouping |
| AI Chat | Vector search (Atlas) | Semantic search API |
Best Practices
1. Singleton connection - Reuse MongoDB client 2. Index query fields - All find() filters should have indexes 3. Cursor pagination - Not offset for large collections 4. Optimistic updates - Better UX for mutations 5. Error boundaries - Graceful error handling 6. TypeScript types - Type safety for documents 7. Validation - Zod/Pydantic on API layer 8. Connection pooling - Default is usually sufficient
Resources
- Next.js + MongoDB: https://github.com/vercel/next.js/tree/canary/examples/with-mongodb
- MongoDB Node Driver: https://www.mongodb.com/docs/drivers/node/
skill: "using-document-databases"
version: "1.0"
domain: "backend"
# Base outputs required for all document database projects
base_outputs:
- path: "db/"
must_contain: []
reason: "Database configuration, schemas, and migration scripts"
- path: "src/models/"
must_contain: []
reason: "Data models and schema definitions"
- path: "config/"
must_contain: []
reason: "Database connection configuration and environment settings"
- path: "tests/"
must_contain: []
reason: "Database integration tests and query validation"
# Conditional outputs based on configuration
conditional_outputs:
maturity:
starter:
- path: "src/models/*.{js,ts,py,rs,go}"
must_contain: ["Schema", "model"]
reason: "Basic schema definitions with simple data models"
- path: "db/indexes.{js,ts,py,sql}"
must_contain: ["createIndex", "index"]
reason: "Basic single-field indexes for common queries"
- path: "config/database.{js,ts,py,yml}"
must_contain: ["connection", "uri"]
reason: "Database connection configuration with connection string"
- path: "src/repositories/*.{js,ts,py,rs,go}"
must_contain: ["find", "insert", "update"]
reason: "Basic CRUD repository patterns"
intermediate:
- path: "src/models/"
must_contain: ["validation", "schema"]
reason: "Schema definitions with validation rules and constraints"
- path: "db/indexes.{js,ts,py,sql}"
must_contain: ["compound", "createIndex"]
reason: "Compound indexes optimized for query patterns"
- path: "db/migrations/"
must_contain: ["*.{js,ts,py}"]
reason: "Database migration scripts for schema evolution"
- path: "src/repositories/"
must_contain: ["pagination", "filter"]
reason: "Advanced repository patterns with pagination and filtering"
- path: "tests/integration/"
must_contain: ["*.test.{js,ts,py}"]
reason: "Integration tests for database operations"
- path: "config/connection-pool.{js,ts,py,yml}"
must_contain: ["pool", "max", "min"]
reason: "Connection pooling configuration for performance"
advanced:
- path: "src/models/"
must_contain: ["validation", "schema", "hooks"]
reason: "Advanced schema with validation, hooks, and business logic"
- path: "db/indexes/"
must_contain: ["compound", "partial", "ttl"]
reason: "Advanced indexing strategies (compound, partial, TTL, text)"
- path: "db/aggregations/"
must_contain: ["pipeline", "$match", "$group"]
reason: "Aggregation pipeline definitions for complex queries"
- path: "db/migrations/"
must_contain: ["up", "down", "rollback"]
reason: "Bi-directional migrations with rollback capability"
- path: "src/repositories/"
must_contain: ["transaction", "session"]
reason: "Transaction support for multi-document operations"
- path: "tests/performance/"
must_contain: ["benchmark", "explain"]
reason: "Performance tests and query plan analysis"
- path: "monitoring/"
must_contain: ["metrics", "slow-query"]
reason: "Database monitoring and slow query logging"
- path: "scripts/validate_indexes.py"
must_contain: ["explain", "analyze"]
reason: "Index validation and query optimization scripts"
database:
mongodb:
- path: "src/models/*.{js,ts,py,rs,go}"
must_contain: ["Schema|model|struct"]
reason: "MongoDB schema definitions (Mongoose, Beanie, mongo-go-driver)"
- path: "db/indexes.{js,ts,py}"
must_contain: ["createIndex", "ensureIndex"]
reason: "MongoDB index creation scripts"
- path: "config/mongodb.{js,ts,py,yml}"
must_contain: ["mongodb://|mongodb+srv://", "options"]
reason: "MongoDB connection string and client options"
- path: "db/aggregations/"
must_contain: ["$match", "$group", "$lookup"]
reason: "MongoDB aggregation pipeline definitions"
- path: "package.json|requirements.txt|Cargo.toml|go.mod"
must_contain: ["mongodb|mongoose|motor|beanie"]
reason: "MongoDB driver dependencies"
dynamodb:
- path: "db/table-definitions/"
must_contain: ["TableName", "KeySchema", "AttributeDefinitions"]
reason: "DynamoDB table definitions with partition/sort keys"
- path: "src/models/"
must_contain: ["PK", "SK", "GSI"]
reason: "Single-table design models with PK/SK/GSI patterns"
- path: "config/dynamodb.{js,ts,py,yml}"
must_contain: ["region", "endpoint"]
reason: "DynamoDB client configuration with region and endpoint"
- path: "db/gsi-definitions.{js,ts,py,yml}"
must_contain: ["GlobalSecondaryIndexes"]
reason: "Global Secondary Index (GSI) definitions for query patterns"
- path: "serverless.yml|sam.yml|terraform/"
must_contain: ["DynamoDB|AWS::DynamoDB"]
reason: "Infrastructure-as-code for DynamoDB provisioning"
- path: "package.json|requirements.txt|Cargo.toml|go.mod"
must_contain: ["aws-sdk|boto3|rusoto"]
reason: "AWS SDK dependencies for DynamoDB"
firestore:
- path: "src/models/"
must_contain: ["collection", "document"]
reason: "Firestore collection and document structure definitions"
- path: "db/security-rules.rules"
must_contain: ["rules_version", "allow read", "allow write"]
reason: "Firestore security rules for access control"
- path: "db/indexes.json"
must_contain: ["collectionGroup", "fields"]
reason: "Firestore composite index definitions"
- path: "config/firebase.{js,ts,json}"
must_contain: ["apiKey", "projectId", "appId"]
reason: "Firebase configuration with project credentials"
- path: "src/listeners/"
must_contain: ["onSnapshot", "real-time"]
reason: "Real-time listener implementations for live data sync"
- path: "package.json|requirements.txt"
must_contain: ["firebase|firebase-admin"]
reason: "Firebase SDK dependencies"
language:
typescript:
- path: "src/models/*.ts"
must_contain: ["interface", "type", "Schema"]
reason: "TypeScript type-safe model definitions"
- path: "src/repositories/*.ts"
must_contain: ["async", "Promise"]
reason: "Async TypeScript repository implementations"
- path: "package.json"
must_contain: ["mongodb|@aws-sdk|firebase"]
reason: "TypeScript database driver dependencies"
python:
- path: "src/models/*.py"
must_contain: ["class", "model|schema"]
reason: "Python model definitions (Pydantic, Beanie, or dataclasses)"
- path: "src/repositories/*.py"
must_contain: ["async def", "await"]
reason: "Async Python repository implementations"
- path: "requirements.txt"
must_contain: ["motor|pymongo|boto3|firebase-admin"]
reason: "Python database driver dependencies"
rust:
- path: "src/models/*.rs"
must_contain: ["struct", "derive", "Serialize"]
reason: "Rust struct definitions with serde serialization"
- path: "src/repositories/*.rs"
must_contain: ["async fn", "Result"]
reason: "Async Rust repository implementations with error handling"
- path: "Cargo.toml"
must_contain: ["mongodb|aws-sdk-dynamodb"]
reason: "Rust database driver dependencies"
go:
- path: "models/*.go"
must_contain: ["type", "struct", "bson"]
reason: "Go struct definitions with BSON tags"
- path: "repositories/*.go"
must_contain: ["context.Context", "error"]
reason: "Go repository implementations with context and error handling"
- path: "go.mod"
must_contain: ["mongo-driver|aws-sdk-go"]
reason: "Go database driver dependencies"
use_case:
cms:
- path: "src/models/content.{js,ts,py,rs,go}"
must_contain: ["title", "body", "author", "published"]
reason: "Content model with metadata and publishing workflow"
- path: "src/models/media.{js,ts,py,rs,go}"
must_contain: ["url", "type", "metadata"]
reason: "Media asset model for images, videos, files"
- path: "db/indexes.{js,ts,py}"
must_contain: ["text", "published", "author"]
reason: "Full-text search and content filtering indexes"
user_profiles:
- path: "src/models/user.{js,ts,py,rs,go}"
must_contain: ["email", "profile", "preferences"]
reason: "User model with embedded profile and preferences"
- path: "db/indexes.{js,ts,py}"
must_contain: ["email", "unique"]
reason: "Unique index on email for authentication"
- path: "src/repositories/user.{js,ts,py,rs,go}"
must_contain: ["findByEmail", "update"]
reason: "User repository with email lookup and profile updates"
catalog:
- path: "src/models/product.{js,ts,py,rs,go}"
must_contain: ["name", "price", "categories", "attributes"]
reason: "Product model with flexible attributes and categories"
- path: "src/models/category.{js,ts,py,rs,go}"
must_contain: ["name", "parent", "products"]
reason: "Category model with hierarchical structure"
- path: "db/indexes.{js,ts,py}"
must_contain: ["categories", "price", "text"]
reason: "Indexes for category filtering, price sorting, and search"
event_logging:
- path: "src/models/event.{js,ts,py,rs,go}"
must_contain: ["timestamp", "type", "userId", "data"]
reason: "Event model with timestamp, type, and flexible data payload"
- path: "db/indexes.{js,ts,py}"
must_contain: ["timestamp", "TTL|expireAfterSeconds"]
reason: "TTL index for automatic event expiration"
- path: "db/aggregations/event-analytics.{js,ts,py}"
must_contain: ["$match", "$group", "count"]
reason: "Event aggregation pipelines for analytics"
# Scaffolding files that should be created as starting points
scaffolding:
- path: "config/database.{js,ts,py,yml}"
reason: "Database connection configuration template"
- path: "src/models/README.md"
reason: "Documentation for schema design patterns and conventions"
- path: "db/indexes/README.md"
reason: "Index strategy documentation with query patterns"
- path: "src/repositories/base.{js,ts,py,rs,go}"
reason: "Base repository class with common CRUD operations"
- path: "tests/fixtures/"
reason: "Test data fixtures for integration tests"
- path: ".env.example"
reason: "Environment variable template for database credentials"
- path: "docker-compose.yml"
reason: "Local development database setup (MongoDB/DynamoDB Local)"
- path: "scripts/seed-data.{js,ts,py}"
reason: "Database seeding script for development"
- path: ".gitignore"
reason: "Ignore environment files, database dumps, and cache"
# Metadata
metadata:
primary_blueprints: ["api-first"]
contributes_to:
- "Document storage"
- "NoSQL data persistence"
- "Flexible schema applications"
- "Content management systems"
- "User profile storage"
- "Product catalogs"
- "Event logging"
- "Real-time applications"
- "Mobile backend services"
common_patterns:
- "Embedding vs referencing decision framework"
- "Single-table design for DynamoDB"
- "Aggregation pipelines for complex queries"
- "Compound indexes for query optimization"
- "Partial indexes for subset indexing"
- "TTL indexes for auto-expiration"
- "Connection pooling for performance"
- "Cursor-based pagination"
- "Soft deletes with timestamps"
- "Audit logs with version history"
integration_points:
api: "Provides data persistence layer for REST/GraphQL APIs"
auth: "Stores user profiles, sessions, and authentication data"
media: "Stores media metadata with GridFS for large files"
search: "Full-text search with MongoDB Atlas Search or Elasticsearch integration"
cache: "Primary database with Redis/Memcached for query caching"
messaging: "Event sourcing with change streams for real-time updates"
typical_directory_structure: |
project/
├── src/
│ ├── models/ # Schema definitions
│ │ ├── user.ts
│ │ ├── product.ts
│ │ └── order.ts
│ └── repositories/ # Data access layer
│ ├── base.ts
│ ├── user.ts
│ └── product.ts
├── db/
│ ├── indexes/ # Index definitions
│ │ ├── user.js
│ │ └── product.js
│ ├── aggregations/ # Aggregation pipelines
│ │ └── sales-analytics.js
│ └── migrations/ # Schema migrations
│ ├── 001-add-user-indexes.js
│ └── 002-update-product-schema.js
├── config/
│ ├── database.ts
│ └── connection-pool.ts
├── tests/
│ ├── integration/
│ │ └── user.test.ts
│ └── performance/
│ └── query-benchmarks.test.ts
├── scripts/
│ ├── seed-data.ts
│ └── validate_indexes.py
└── docker-compose.yml
Document Database Implementation Skill
Production-ready Claude Skill for NoSQL document database selection and implementation.
Overview
This skill guides document database selection and implementation for flexible schema applications across Python, TypeScript, Rust, and Go.
Primary databases covered:
- MongoDB (general-purpose, rich queries, vector search)
- DynamoDB (AWS serverless, single-table design)
- Firestore (real-time sync, mobile-first)
Skill Structure
using-document-databases/
├── SKILL.md # Main skill file (<500 lines)
├── references/
│ ├── mongodb.md # MongoDB collections, indexes, aggregation
│ ├── dynamodb.md # DynamoDB single-table, GSI patterns
│ ├── firestore.md # Firestore real-time, security rules
│ └── schema-design-patterns.md # Embedding vs referencing framework
├── examples/
│ ├── mongodb-fastapi/ # Python FastAPI + MongoDB (Motor)
│ │ ├── main.py
│ │ └── requirements.txt
│ └── dynamodb-serverless/ # Python Lambda + DynamoDB
│ ├── handler.py
│ └── serverless.yml
└── scripts/
└── validate_indexes.py # MongoDB index validation toolQuick Start
Using the Skill
The skill automatically triggers when building applications with:
- Content management systems
- User profiles with flexible attributes
- Product catalogs
- Event logging systems
- Mobile apps requiring offline sync
Database Selection
Use MongoDB when:
- Complex aggregation queries needed
- Full-text or vector search required
- ACID multi-document transactions needed
- Self-hosted or multi-cloud deployment
Use DynamoDB when:
- AWS-native serverless architecture
- Predictable single-digit ms latency required
- Auto-scaling without capacity planning
- Event-driven workflows (Streams + Lambda)
Use Firestore when:
- Real-time sync across clients required
- Mobile-first with offline support
- Firebase ecosystem (Auth, Hosting, Analytics)
- Rapid prototyping with generous free tier
Key Features
Schema Design Patterns
Decision matrix for embedding vs referencing:
- One-to-Few (<10) → Embed
- One-to-Many (10-1000) → Hybrid
- One-to-Millions → Reference
- Many-to-Many → Reference
See references/schema-design-patterns.md
Indexing Strategies
MongoDB index types:
- Single field, compound, multikey
- Text (full-text search)
- Geospatial (2dsphere)
- TTL (auto-expiring documents)
- Wildcard (dynamic schemas)
Validate indexes:
python scripts/validate_indexes.py --db myapp --collection ordersAggregation Pipelines
MongoDB's killer feature for complex transformations:
$match,$project,$group,$lookup$unwind,$sort,$limit,$facet
See references/mongodb.md for aggregation cookbook.
DynamoDB Single-Table Design
Access pattern-driven modeling:
PK: USER#12345, SK: METADATA # User data
PK: USER#12345, SK: ORDER#001 # User's orders
PK: ORDER#001, SK: METADATA # Order details
PK: ORDER#001, SK: ITEM#001 # Order itemsSee references/dynamodb.md for complete patterns.
Examples
MongoDB + FastAPI (Python)
Production-ready async API with Motor:
cd examples/mongodb-fastapi
pip install -r requirements.txt
export MONGODB_URI="mongodb://localhost:27017/"
python main.pyFeatures:
- Async MongoDB with connection pooling
- CRUD operations with validation
- Aggregation pipeline analytics
- Soft deletes
- Health checks
DynamoDB + Lambda (Serverless)
AWS serverless API with single-table design:
cd examples/dynamodb-serverless
npm install -g serverless
serverless deployFeatures:
- Single-table design pattern
- Batch writes for efficiency
- GSI for status queries
- Lambda + API Gateway
- Auto-scaling with pay-per-request
Multi-Language Support
Python:
pymongo(sync)motor(async with AsyncIO/FastAPI)boto3(DynamoDB)
TypeScript:
mongodb(native driver)@aws-sdk/client-dynamodb(DynamoDB SDK v3)firebase/firestore(Firestore)
Rust:
mongodbcrateaws-sdk-dynamodb
Go:
mongo-go-driveraws-sdk-go-v2
Integration with Other Skills
media/ - File metadata storage (MongoDB GridFS) ai-chat/ - Conversation history + vector search (Atlas Vector Search) feedback/ - Event logging (DynamoDB high-throughput writes) forms/ - Dynamic form submissions (Firestore real-time validation) search-filter/ - Product catalogs (MongoDB Atlas Search)
Performance Best Practices
MongoDB
- Use indexes for all query filters
- Covering indexes (query + projection in index)
- Connection pooling (reuse client)
- Projection (fetch only needed fields)
DynamoDB
- Design for even partition distribution
- Batch operations (up to 100 items)
- GSI projections (KEYS_ONLY or INCLUDE)
- TTL for auto-expiring data
Firestore
- Denormalize frequently accessed data
- Use subcollections for large arrays
- Offline persistence for mobile
- Security rules for access control
Common Patterns
Pagination (MongoDB):
// Cursor-based (recommended)
db.products.find({ _id: { $gt: lastId }}).limit(20)Soft Deletes:
// Mark as deleted instead of removing
{ deleted: true, deletedAt: ISODate("...") }Audit Logs:
// Versioned documents
{ documentId: "doc123", version: 3, history: [...] }Dependencies
Python:
pip install motor pymongo boto3 firebase-adminTypeScript:
npm install mongodb @aws-sdk/client-dynamodb firebaseAnti-Patterns to Avoid
❌ Unbounded arrays - Use references instead ❌ Deep nesting - Flatten with references ❌ Over-indexing - Index only queried fields ❌ DynamoDB scans - Always use query with partition key ❌ Missing indexes - Validate with explain()
Testing
Run index validation:
python scripts/validate_indexes.py --db myapp --allExpected output:
- ✓ Covered queries
- ✗ Missing indexes with suggestions
- 📈 Index usage statistics
- ⚠️ Unused indexes
Additional Resources
- MongoDB documentation:
references/mongodb.md - DynamoDB patterns:
references/dynamodb.md - Firestore guide:
references/firestore.md - Schema design:
references/schema-design-patterns.md
Version
v0.1.0 - Initial release (December 2025)
---
Skill Author: Claude Code Skill Type: Database Implementation Languages: Python, TypeScript, Rust, Go Databases: MongoDB, DynamoDB, Firestore
MongoDB Aggregation Pipeline Patterns
Complete guide to MongoDB aggregation framework for complex queries, analytics, and data transformations.
Table of Contents
- Aggregation Pipeline Basics
- Core Stages
- $match (Filter)
- $group (Aggregate)
- $project (Reshape)
- $lookup (Join)
- $unwind (Flatten Arrays)
- Common Patterns
- Top N per Category
- Time-Based Aggregation
- Moving Average
- Pagination
- Advanced Patterns
- Faceted Search
- Full-Text Search + Aggregation
- Conditional Aggregation
- Performance Optimization
- Index Usage
- $match and $project Early
- Limit Result Size
- Pipeline Validation
- Best Practices
- Resources
Aggregation Pipeline Basics
Pipeline stages process documents sequentially:
db.collection.aggregate([
{ $match: { status: "active" } }, // Filter
{ $group: { _id: "$category" } }, // Group
{ $sort: { count: -1 } }, // Sort
{ $limit: 10 }, // Limit
])Core Stages
$match (Filter)
// Filter before expensive operations
db.orders.aggregate([
{ $match: {
status: "completed",
createdAt: { $gte: ISODate("2025-01-01") }
}},
// ... other stages
])Best practice: Use $match early to reduce document count.
$group (Aggregate)
// Revenue by category
db.orders.aggregate([
{ $group: {
_id: "$category",
totalRevenue: { $sum: "$amount" },
avgOrder: { $avg: "$amount" },
count: { $sum: 1 },
maxOrder: { $max: "$amount" },
minOrder: { $min: "$amount" },
}},
])Accumulators: $sum, $avg, $max, $min, $first, $last, $push, $addToSet
$project (Reshape)
// Select and transform fields
db.users.aggregate([
{ $project: {
fullName: { $concat: ["$firstName", " ", "$lastName"] },
email: 1, // Include field
_id: 0, // Exclude field
ageGroup: {
$cond: {
if: { $gte: ["$age", 18] },
then: "adult",
else: "minor"
}
}
}},
])$lookup (Join)
// Join orders with users
db.orders.aggregate([
{ $lookup: {
from: "users",
localField: "userId",
foreignField: "_id",
as: "user"
}},
{ $unwind: "$user" }, // Flatten array
])$unwind (Flatten Arrays)
// Expand array elements into separate documents
db.posts.aggregate([
{ $unwind: "$tags" }, // Create one doc per tag
{ $group: {
_id: "$tags",
count: { $sum: 1 }
}},
])Common Patterns
Top N per Category
// Top 3 products per category
db.products.aggregate([
{ $sort: { category: 1, sales: -1 } },
{ $group: {
_id: "$category",
products: { $push: "$$ROOT" },
}},
{ $project: {
category: "$_id",
topProducts: { $slice: ["$products", 3] }
}},
])Time-Based Aggregation
// Daily revenue
db.orders.aggregate([
{ $match: {
createdAt: { $gte: ISODate("2025-11-01") }
}},
{ $group: {
_id: {
$dateToString: { format: "%Y-%m-%d", date: "$createdAt" }
},
revenue: { $sum: "$amount" },
orders: { $sum: 1 },
}},
{ $sort: { _id: 1 } },
])Moving Average
// 7-day moving average
db.metrics.aggregate([
{ $setWindowFields: {
sortBy: { date: 1 },
output: {
movingAvg: {
$avg: "$value",
window: { documents: [-6, 0] } // Current + 6 previous
}
}
}},
])Pagination
// Efficient pagination
db.products.aggregate([
{ $match: { category: "electronics" } },
{ $sort: { createdAt: -1 } },
{ $skip: 20 }, // Page 2 (skip 20)
{ $limit: 10 }, // 10 per page
])
// Total count for pagination
db.products.aggregate([
{ $match: { category: "electronics" } },
{ $facet: {
items: [
{ $skip: 20 },
{ $limit: 10 },
],
totalCount: [
{ $count: "count" },
],
}},
])Advanced Patterns
Faceted Search
// Multiple aggregations in one query
db.products.aggregate([
{ $match: { price: { $lte: 1000 } } },
{ $facet: {
byCategory: [
{ $group: { _id: "$category", count: { $sum: 1 } } },
{ $sort: { count: -1 } },
],
byBrand: [
{ $group: { _id: "$brand", count: { $sum: 1 } } },
{ $sort: { count: -1 } },
],
priceRanges: [
{ $bucket: {
groupBy: "$price",
boundaries: [0, 100, 500, 1000],
default: "Other",
output: { count: { $sum: 1 } }
}},
],
}},
])Full-Text Search + Aggregation
db.articles.aggregate([
{ $match: { $text: { $search: "mongodb aggregation" } } },
{ $addFields: {
score: { $meta: "textScore" }
}},
{ $sort: { score: -1 } },
{ $limit: 10 },
])Conditional Aggregation
// Revenue by payment method
db.orders.aggregate([
{ $group: {
_id: null,
creditCardRevenue: {
$sum: { $cond: [
{ $eq: ["$paymentMethod", "credit_card"] },
"$amount",
0
]}
},
paypalRevenue: {
$sum: { $cond: [
{ $eq: ["$paymentMethod", "paypal"] },
"$amount",
0
]}
},
}},
])Performance Optimization
Index Usage
// Use indexes for $match and $sort
db.orders.createIndex({ status: 1, createdAt: -1 });
db.orders.aggregate([
{ $match: { status: "active" } }, // Uses index
{ $sort: { createdAt: -1 } }, // Uses index
// ... other stages
])
// Check index usage
db.orders.explain().aggregate([...])$match and $project Early
// ✅ Good: Filter and project early
db.large_collection.aggregate([
{ $match: { active: true } }, // Reduce documents
{ $project: { needed_field: 1 } }, // Reduce field count
{ $lookup: ... }, // Expensive operation on smaller dataset
])
// ❌ Bad: Expensive operations on full collection
db.large_collection.aggregate([
{ $lookup: ... }, // Processes all documents
{ $match: { active: true } }, // Filter after expensive operation
])Limit Result Size
// Limit intermediate results
db.products.aggregate([
{ $match: { inStock: true } },
{ $sort: { popularity: -1 } },
{ $limit: 100 }, // Limit before expensive stages
{ $lookup: { /* join details */ } },
])Pipeline Validation
// Use $merge for debugging
db.orders.aggregate([
{ $match: { status: "pending" } },
{ $merge: { into: "debug_stage1" } }, // Save intermediate result
{ $group: { _id: "$userId", total: { $sum: "$amount" } } },
{ $merge: { into: "debug_stage2" } },
])Best Practices
1. $match early - Filter before expensive operations 2. Use indexes - Ensure $match and $sort use indexes 3. $project unwanted fields - Reduce memory usage 4. Limit results - Use $limit early when possible 5. Avoid $lookup on large collections - Index foreign keys 6. Test with explain() - Verify index usage 7. Use $facet sparingly - Multiple sub-pipelines are expensive 8. Consider denormalization - Avoid $lookup for frequent queries
Resources
- MongoDB Aggregation Docs: https://www.mongodb.com/docs/manual/aggregation/
- Aggregation Pipeline Operators: https://www.mongodb.com/docs/manual/reference/operator/aggregation/
MongoDB Anti-Patterns
Common mistakes and how to avoid them.
Table of Contents
- 1. Unbounded Arrays
- 2. Over-Embedding
- 3. Collection Scans
- 4. Large Documents (>1MB)
- 5. Inefficient $lookup
- 6. Massive Projections
- 7. Client-Side Joins
- 8. Unbounded $group
- 9. No Error Handling
- 10. Wrong Data Types
- Summary of Best Practices
- Resources
1. Unbounded Arrays
❌ Problem:
{
_id: ObjectId("..."),
userId: 123,
events: [
{ type: "login", timestamp: "..." },
{ type: "click", timestamp: "..." },
// ... 10,000 more events (document grows forever)
]
}✅ Solution: Use separate collection with reference
// User document (bounded)
{ _id: ObjectId("..."), userId: 123, email: "..." }
// Events collection
{ _id: ObjectId("..."), userId: 123, type: "login", timestamp: "..." }
// Query recent events
db.events.find({ userId: 123 }).sort({ timestamp: -1 }).limit(100)Rule: Arrays with potential for >100 elements should be separate collections.
2. Over-Embedding
❌ Problem:
{
_id: ObjectId("..."),
title: "Blog Post",
author: {
_id: ObjectId("..."),
name: "John",
email: "john@example.com",
bio: "Long biography...",
socialLinks: [...], // Embedded author data duplicated in every post
},
comments: [
{
author: { /* full author embedded again */ },
replies: [
{ author: { /* embedded again */ } } // Deeply nested
]
}
]
}✅ Solution: Reference pattern
// Post (minimal author info)
{
_id: ObjectId("..."),
title: "Blog Post",
authorId: ObjectId("author_id"), // Reference only
authorName: "John", // Denormalize only display name
}
// Fetch author details when needed
const post = db.posts.findOne({ _id: postId })
const author = db.users.findOne({ _id: post.authorId })3. Collection Scans
❌ Problem:
// No index on status
db.orders.find({ status: "pending" }) // COLLSCAN on 1M documents✅ Solution: Create index
db.orders.createIndex({ status: 1, createdAt: -1 })
db.orders.find({ status: "pending" }) // IXSCAN4. Large Documents (>1MB)
❌ Problem:
{
_id: ObjectId("..."),
productId: 123,
largeImage: "<base64 encoded 5MB image>", // 16MB doc limit approaching
}✅ Solution: Use GridFS or external storage
// Reference S3/GridFS
{
_id: ObjectId("..."),
productId: 123,
imageUrl: "https://cdn.example.com/products/123.jpg",
thumbnailUrl: "https://cdn.example.com/products/123_thumb.jpg",
}5. Inefficient $lookup
❌ Problem:
// $lookup without index on foreign key
db.orders.aggregate([
{ $lookup: {
from: "users", // No index on users._id
localField: "userId",
foreignField: "_id",
as: "user"
}}
])✅ Solution: Index foreign keys
// Create index on lookup field
db.users.createIndex({ _id: 1 }) // Usually exists by default
// Or denormalize frequently accessed fields
{
_id: ObjectId("..."),
userId: 123,
userName: "John", // Denormalized for display
userEmail: "john@example.com",
}6. Massive Projections
❌ Problem:
// Selecting all fields when only need few
db.users.find({}, { password: 0 }) // Returns everything except password✅ Solution: Explicit projection
// Select only needed fields
db.users.find({}, { email: 1, name: 1, _id: 0 })
// Covering index (no document fetch)
db.users.createIndex({ email: 1, name: 1 })
db.users.find({}, { email: 1, name: 1, _id: 0 })7. Client-Side Joins
❌ Problem:
// N+1 queries
const posts = await db.posts.find({}).toArray();
for (const post of posts) {
post.author = await db.users.findOne({ _id: post.authorId }); // N queries!
}✅ Solution: Aggregation or denormalization
// Aggregation (server-side join)
db.posts.aggregate([
{ $lookup: {
from: "users",
localField: "authorId",
foreignField: "_id",
as: "author"
}},
{ $unwind: "$author" },
])
// Or denormalize
{
_id: ObjectId("..."),
authorId: ObjectId("..."),
authorName: "John", // Cached for display
}8. Unbounded $group
❌ Problem:
// Group by high-cardinality field
db.events.aggregate([
{ $group: {
_id: "$userId", // Millions of unique users
events: { $push: "$$ROOT" } // Massive memory usage
}}
])✅ Solution: Add $match and $limit
db.events.aggregate([
{ $match: { timestamp: { $gte: recentDate } } }, // Filter first
{ $group: { _id: "$userId", count: { $sum: 1 } } }, // Don't $push all docs
{ $limit: 1000 }, // Limit results
])9. No Error Handling
❌ Problem:
const user = await db.users.insertOne({ email: "duplicate@example.com" });
// Throws on duplicate email if unique index exists✅ Solution: Handle errors
try {
const user = await db.users.insertOne({ email: "user@example.com" });
} catch (error) {
if (error.code === 11000) { // Duplicate key error
throw new Error("Email already exists");
}
throw error;
}10. Wrong Data Types
❌ Problem:
{
createdAt: "2025-12-03T10:00:00Z", // String, not Date
price: "49.99", // String, not Number
isActive: "true", // String, not Boolean
}✅ Solution: Use proper types
{
createdAt: ISODate("2025-12-03T10:00:00Z"), // Date object
price: 49.99, // Number
isActive: true, // Boolean
}Summary of Best Practices
✅ DO:
- Reference for one-to-many (>100 related docs)
- Index all query filters
- Use cursor-based pagination
- Implement soft deletes for important data
- Denormalize display fields
- Use proper data types
- Handle errors explicitly
- Limit array sizes (<100 elements)
- Use aggregation for complex queries
- Monitor slow queries
❌ DON'T:
- Embed unbounded arrays
- Do client-side joins (N+1 queries)
- Skip indexing query fields
- Store large binary data in documents
- Use offset pagination for large datasets
- Store strings when you need dates/numbers
- Ignore 16MB document limit
- Over-normalize (too many $lookups)
- Create 10+ indexes per collection
- Use $regex without index
Resources
- MongoDB Anti-Patterns: https://www.mongodb.com/developer/products/mongodb/schema-design-anti-pattern-summary/
Common MongoDB Patterns
Frequently-used patterns for pagination, soft deletes, audit logs, and data modeling.
Table of Contents
- Pagination Patterns
- Cursor-Based (Recommended)
- Offset-Based (Simple Cases)
- Range-Based (Time Series)
- Soft Deletes
- Pattern 1: Boolean Flag
- Pattern 2: Status Field
- Audit Logs
- Pattern 1: Embedded History
- Pattern 2: Separate Audit Collection
- Versioning Documents
- Pattern: Version Number + History
- Counter Pattern (Atomic Increments)
- Hierarchical Data
- Pattern 1: Parent Reference
- Pattern 2: Materialized Path
- Upsert Pattern
- Bulk Operations
- Caching Pattern
- Best Practices
- Resources
Pagination Patterns
Cursor-Based (Recommended)
Handles real-time changes, no skipped records:
// First page
db.posts.find({})
.sort({ _id: -1 })
.limit(20)
// Next page (using last _id as cursor)
db.posts.find({ _id: { $lt: lastSeenId } })
.sort({ _id: -1 })
.limit(20)API response:
{
"items": [...],
"nextCursor": "507f1f77bcf86cd799439011",
"hasMore": true
}Offset-Based (Simple Cases)
Only for static datasets <10K records:
const page = 2;
const perPage = 20;
db.posts.find({})
.sort({ createdAt: -1 })
.skip(page * perPage)
.limit(perPage)Problem: Performance degrades with large skip values.
Range-Based (Time Series)
// Page by date range
db.events.find({
createdAt: {
$gte: ISODate("2025-12-01"),
$lt: ISODate("2025-12-02")
}
})Soft Deletes
Pattern 1: Boolean Flag
// Schema
{
_id: ObjectId("..."),
email: "user@example.com",
deletedAt: null, // or ISODate("...")
isDeleted: false,
}
// Soft delete
db.users.updateOne(
{ _id: userId },
{ $set: { isDeleted: true, deletedAt: new Date() } }
)
// Query (exclude deleted)
db.users.find({ isDeleted: { $ne: true } })
// Create index for efficient querying
db.users.createIndex({ isDeleted: 1, createdAt: -1 })Pattern 2: Status Field
// Schema with status
{
_id: ObjectId("..."),
status: "active", // active | archived | deleted
statusChangedAt: ISODate("..."),
}
// Archive (soft delete)
db.posts.updateOne(
{ _id: postId },
{ $set: { status: "archived", statusChangedAt: new Date() } }
)
// Query active only
db.posts.find({ status: "active" })
// Index
db.posts.createIndex({ status: 1, createdAt: -1 })Audit Logs
Pattern 1: Embedded History
{
_id: ObjectId("..."),
email: "user@example.com",
name: "John Doe",
history: [
{
action: "created",
timestamp: ISODate("2025-01-01T00:00:00Z"),
by: ObjectId("admin_id"),
},
{
action: "updated",
timestamp: ISODate("2025-02-01T00:00:00Z"),
by: ObjectId("user_id"),
changes: { name: { old: "John", new: "John Doe" } },
},
],
}Use when: <100 updates per document
Pattern 2: Separate Audit Collection
// Main collection
db.users.updateOne({ _id: userId }, { $set: { name: "New Name" } })
// Audit log collection
db.audit_log.insertOne({
collection: "users",
documentId: userId,
action: "update",
changes: { name: { old: "John", new: "New Name" } },
userId: currentUserId,
timestamp: new Date(),
ip: "192.168.1.1",
})Use when: Many updates, need queryable audit history
Versioning Documents
Pattern: Version Number + History
{
_id: ObjectId("..."),
version: 3,
content: "Current content",
versions: [
{ version: 1, content: "Original", createdAt: ISODate("...") },
{ version: 2, content: "Updated", createdAt: ISODate("...") },
{ version: 3, content: "Current content", createdAt: ISODate("...") },
],
}
// Update with versioning
db.documents.updateOne(
{ _id: docId },
{
$inc: { version: 1 },
$set: { content: newContent },
$push: {
versions: {
version: currentVersion + 1,
content: newContent,
createdAt: new Date(),
}
}
}
)Counter Pattern (Atomic Increments)
// Page view counter
db.posts.updateOne(
{ _id: postId },
{ $inc: { views: 1 } }
)
// Multiple counters
db.posts.updateOne(
{ _id: postId },
{
$inc: { views: 1, shares: 1 },
$set: { lastViewed: new Date() }
}
)Hierarchical Data
Pattern 1: Parent Reference
// Category tree
{
_id: ObjectId("..."),
name: "Electronics",
parentId: null, // Root category
}
{
_id: ObjectId("..."),
name: "Laptops",
parentId: ObjectId("electronics_id"), // Child
}
// Find all children
db.categories.find({ parentId: electronicsId })
// Find path to root
function getPath(categoryId) {
const path = [];
let current = db.categories.findOne({ _id: categoryId });
while (current) {
path.unshift(current.name);
current = current.parentId ?
db.categories.findOne({ _id: current.parentId }) :
null;
}
return path;
}Pattern 2: Materialized Path
{
_id: ObjectId("..."),
name: "Laptops",
path: "Electronics,Computers,Laptops", // Full path
}
// Find all descendants
db.categories.find({ path: /^Electronics,Computers/ })
// Index for performance
db.categories.createIndex({ path: 1 })Upsert Pattern
// Insert if not exists, update if exists
db.users.updateOne(
{ email: "user@example.com" },
{
$set: { name: "John", lastLogin: new Date() },
$setOnInsert: { createdAt: new Date() }, // Only on insert
},
{ upsert: true }
)Bulk Operations
// Batch inserts (faster than individual)
db.users.insertMany([
{ email: "user1@example.com", name: "User 1" },
{ email: "user2@example.com", name: "User 2" },
// ... thousands more
], { ordered: false }) // Continue on error
// Bulk write operations
db.users.bulkWrite([
{
insertOne: {
document: { email: "new@example.com", name: "New User" }
}
},
{
updateOne: {
filter: { _id: ObjectId("...") },
update: { $set: { name: "Updated" } }
}
},
{
deleteOne: {
filter: { _id: ObjectId("...") }
}
}
])Caching Pattern
// Cache frequently accessed data
db.users.aggregate([
{ $match: { _id: userId } },
{ $lookup: {
from: "posts",
localField: "_id",
foreignField: "userId",
as: "recentPosts",
pipeline: [
{ $sort: { createdAt: -1 } },
{ $limit: 5 }
]
}},
{ $project: {
email: 1,
name: 1,
postCount: { $size: "$recentPosts" },
recentPosts: { $slice: ["$recentPosts", 3] }
}},
{ $merge: {
into: "user_cache",
whenMatched: "replace",
whenNotMatched: "insert"
}}
])Best Practices
1. Index all queries - Every filter should have supporting index 2. Compound index order - Equality, Range, Sort 3. Use partial indexes - Index only needed subset 4. Cursor pagination - Avoid skip for large offsets 5. Soft deletes - Preserve data, add isDeleted flag 6. Audit critical changes - Log who/what/when 7. Version important docs - Maintain history 8. Use upserts - Idempotent operations 9. Bulk operations - Batch for performance 10. Monitor slow queries - Enable profiling
Resources
- MongoDB Schema Design: https://www.mongodb.com/docs/manual/core/data-modeling-introduction/
- Index Best Practices: https://www.mongodb.com/docs/manual/applications/indexes/
DynamoDB Complete Guide
AWS DynamoDB single-table design, GSI patterns, and serverless best practices.
Table of Contents
- Core Concepts
- Single-Table Design
- Primary Keys
- Global Secondary Indexes
- Query Patterns
- DynamoDB Streams
- Pricing Optimization
- Performance Best Practices
---
Core Concepts
DynamoDB vs Traditional Databases
| Feature | DynamoDB | MongoDB/SQL |
|---|---|---|
| Data Model | Key-value, document | Document, relational |
| Queries | By primary key + GSI only | Flexible queries |
| Scaling | Automatic, unlimited | Manual sharding/replication |
| Consistency | Eventual (default), strong (optional) | Tunable |
| Latency | Single-digit ms guaranteed | Varies |
| Pricing | Per request or provisioned | Per instance/cluster |
Key Terminology
- Partition Key (PK): Hash key for data distribution
- Sort Key (SK): Range key for sorting within partition
- GSI: Global Secondary Index (alternate query patterns)
- LSI: Local Secondary Index (different sort key, same PK)
- WCU: Write Capacity Unit (1 KB/sec)
- RCU: Read Capacity Unit (4 KB/sec for eventual, 4 KB/sec for strong)
---
Single-Table Design
Philosophy
Design for access patterns, not normalization. Store multiple entity types in one table using composite keys.
Entity Types in One Table
// Users table with multiple entity types
// User metadata
{
PK: "USER#12345",
SK: "METADATA",
email: "user@example.com",
name: "Jane Doe",
createdAt: "2025-01-15T10:00:00Z"
}
// User's orders
{
PK: "USER#12345",
SK: "ORDER#2025-001234",
orderNumber: "ORD-2025-001234",
totalAmount: 249.97,
status: "shipped",
orderDate: "2025-11-25T14:30:00Z"
}
// Order detail (different access pattern)
{
PK: "ORDER#2025-001234",
SK: "METADATA",
userId: "USER#12345",
totalAmount: 249.97,
status: "shipped"
}
// Order items
{
PK: "ORDER#2025-001234",
SK: "ITEM#001",
productId: "PROD-456",
quantity: 2,
price: 49.99,
name: "Widget Pro"
}
// Product catalog
{
PK: "PRODUCT#456",
SK: "METADATA",
name: "Widget Pro",
category: "widgets",
price: 49.99,
inventory: 245
}Access Patterns Enabled
import boto3
from boto3.dynamodb.conditions import Key
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('AppData')
# 1. Get user metadata
response = table.query(
KeyConditionExpression=Key('PK').eq('USER#12345') & Key('SK').eq('METADATA')
)
# 2. Get all orders for user
response = table.query(
KeyConditionExpression=Key('PK').eq('USER#12345') & Key('SK').begins_with('ORDER#')
)
# 3. Get order with items
response = table.query(
KeyConditionExpression=Key('PK').eq('ORDER#2025-001234')
)
# 4. Get specific order
response = table.query(
KeyConditionExpression=Key('PK').eq('ORDER#2025-001234') & Key('SK').eq('METADATA')
)Composite Key Strategies
Pattern 1: Entity Type Prefix
PK: USER#12345
SK: METADATA
SK: ORDER#001
SK: ORDER#002Pattern 2: Hierarchical
PK: TENANT#abc
SK: USER#12345
SK: USER#12345#ORDER#001
SK: USER#12345#ORDER#002Pattern 3: Reverse Relationship
// Same item, two representations
PK: USER#12345, SK: ORDER#001
PK: ORDER#001, SK: USER#12345---
Primary Keys
Partition Key Only
# Create table with partition key only
dynamodb = boto3.client('dynamodb')
dynamodb.create_table(
TableName='Users',
KeySchema=[
{'AttributeName': 'userId', 'KeyType': 'HASH'} # PK only
],
AttributeDefinitions=[
{'AttributeName': 'userId', 'AttributeType': 'S'}
],
BillingMode='PAY_PER_REQUEST'
)
# Put item
table.put_item(Item={
'userId': 'user123',
'email': 'user@example.com',
'name': 'Jane Doe'
})
# Get item
response = table.get_item(Key={'userId': 'user123'})Partition Key + Sort Key
# Create table with composite key
dynamodb.create_table(
TableName='AppData',
KeySchema=[
{'AttributeName': 'PK', 'KeyType': 'HASH'}, # Partition Key
{'AttributeName': 'SK', 'KeyType': 'RANGE'} # Sort Key
],
AttributeDefinitions=[
{'AttributeName': 'PK', 'AttributeType': 'S'},
{'AttributeName': 'SK', 'AttributeType': 'S'}
],
BillingMode='PAY_PER_REQUEST'
)Partition Key Design
Good Partition Keys:
- High cardinality (many unique values)
- Even access distribution
- No hot partitions
# GOOD: User ID (unique per user)
PK: "USER#12345"
# GOOD: Composite (tenant + user)
PK: "TENANT#abc#USER#12345"
# BAD: Status (hot partition on "active")
PK: "STATUS#active" # All active users in one partition!
# BAD: Date (hot partition for current date)
PK: "DATE#2025-11-28" # All today's writes to one partition!Fix Hot Partitions:
# Add random suffix to distribute writes
import random
suffix = random.randint(0, 9)
PK: f"DATE#2025-11-28#{suffix}"---
Global Secondary Indexes (GSI)
When to Use GSI
- Query by attributes other than primary key
- Different sort order
- Sparse indexes (not all items have attribute)
GSI Example: Query Orders by Status
# Base table: PK = userId, SK = timestamp
# GSI: PK = status, SK = timestamp (query all pending orders)
dynamodb.create_table(
TableName='Orders',
KeySchema=[
{'AttributeName': 'userId', 'KeyType': 'HASH'},
{'AttributeName': 'timestamp', 'KeyType': 'RANGE'}
],
AttributeDefinitions=[
{'AttributeName': 'userId', 'AttributeType': 'S'},
{'AttributeName': 'timestamp', 'AttributeType': 'S'},
{'AttributeName': 'status', 'AttributeType': 'S'}
],
GlobalSecondaryIndexes=[
{
'IndexName': 'StatusIndex',
'KeySchema': [
{'AttributeName': 'status', 'KeyType': 'HASH'},
{'AttributeName': 'timestamp', 'KeyType': 'RANGE'}
],
'Projection': {'ProjectionType': 'ALL'},
'BillingMode': 'PAY_PER_REQUEST'
}
],
BillingMode='PAY_PER_REQUEST'
)
# Query GSI
table = dynamodb.Table('Orders')
response = table.query(
IndexName='StatusIndex',
KeyConditionExpression=Key('status').eq('pending')
)GSI Projection Types
# ALL: Project all attributes
'Projection': {'ProjectionType': 'ALL'}
# KEYS_ONLY: Project only keys (smallest index)
'Projection': {'ProjectionType': 'KEYS_ONLY'}
# INCLUDE: Project specific attributes
'Projection': {
'ProjectionType': 'INCLUDE',
'NonKeyAttributes': ['email', 'name', 'totalAmount']
}GSI Best Practices
1. Use sparse indexes (only items with attribute are indexed) 2. Projection: Use KEYS_ONLY or INCLUDE to reduce storage 3. Limit GSIs: Maximum 20 per table 4. GSI writes: Every item update may update multiple GSIs (cost!)
---
Query Patterns
Query vs Scan
# GOOD: Query (efficient, uses index)
response = table.query(
KeyConditionExpression=Key('PK').eq('USER#12345')
)
# BAD: Scan (reads entire table, expensive!)
response = table.scan(
FilterExpression=Attr('email').eq('user@example.com')
)Query Operators
from boto3.dynamodb.conditions import Key, Attr
# Equals
Key('PK').eq('USER#12345')
# Less than, greater than
Key('timestamp').lt('2025-11-01')
Key('timestamp').gte('2025-11-01')
# Between
Key('timestamp').between('2025-11-01', '2025-11-30')
# Begins with (sort key only)
Key('SK').begins_with('ORDER#')Filter Expressions (Post-Query Filtering)
# Query + filter (filter after query, doesn't save RCUs)
response = table.query(
KeyConditionExpression=Key('PK').eq('USER#12345'),
FilterExpression=Attr('status').eq('active') & Attr('age').gte(18)
)Pagination
# Paginate through large result sets
last_evaluated_key = None
while True:
if last_evaluated_key:
response = table.query(
KeyConditionExpression=Key('PK').eq('USER#12345'),
ExclusiveStartKey=last_evaluated_key
)
else:
response = table.query(
KeyConditionExpression=Key('PK').eq('USER#12345')
)
items = response['Items']
# Process items
last_evaluated_key = response.get('LastEvaluatedKey')
if not last_evaluated_key:
break---
Update Operations
Update Expressions
# Set attribute
table.update_item(
Key={'userId': 'user123'},
UpdateExpression='SET #name = :name',
ExpressionAttributeNames={'#name': 'name'},
ExpressionAttributeValues={':name': 'Jane Smith'}
)
# Increment counter
table.update_item(
Key={'userId': 'user123'},
UpdateExpression='SET loginCount = loginCount + :inc',
ExpressionAttributeValues={':inc': 1}
)
# Add to list
table.update_item(
Key={'userId': 'user123'},
UpdateExpression='SET tags = list_append(tags, :tag)',
ExpressionAttributeValues={':tag': ['new-tag']}
)
# Add to set
table.update_item(
Key={'userId': 'user123'},
UpdateExpression='ADD emailSet :email',
ExpressionAttributeValues={':email': {'user@example.com'}}
)
# Remove attribute
table.update_item(
Key={'userId': 'user123'},
UpdateExpression='REMOVE deprecated_field'
)Conditional Updates
# Update only if condition met
from botocore.exceptions import ClientError
try:
table.update_item(
Key={'userId': 'user123'},
UpdateExpression='SET balance = balance - :amount',
ConditionExpression='balance >= :amount',
ExpressionAttributeValues={':amount': 100}
)
except ClientError as e:
if e.response['Error']['Code'] == 'ConditionalCheckFailedException':
print("Insufficient balance")Atomic Counters
# Increment view count (atomic)
response = table.update_item(
Key={'postId': 'post123'},
UpdateExpression='SET viewCount = if_not_exists(viewCount, :start) + :inc',
ExpressionAttributeValues={':start': 0, ':inc': 1},
ReturnValues='UPDATED_NEW'
)
print(f"New count: {response['Attributes']['viewCount']}")---
DynamoDB Streams
Enable Change Data Capture
# Create table with streams enabled
dynamodb.create_table(
TableName='Orders',
# ... (key schema, attributes)
StreamSpecification={
'StreamEnabled': True,
'StreamViewType': 'NEW_AND_OLD_IMAGES' # Full document before/after
}
)Stream View Types
KEYS_ONLY: Only key attributesNEW_IMAGE: Entire item after updateOLD_IMAGE: Entire item before updateNEW_AND_OLD_IMAGES: Both before and after
Process Streams with Lambda
# Lambda function triggered by DynamoDB Stream
def lambda_handler(event, context):
for record in event['Records']:
if record['eventName'] == 'INSERT':
new_item = record['dynamodb']['NewImage']
print(f"New order: {new_item}")
# Send notification
send_email(new_item['email']['S'], "Order Confirmed")
elif record['eventName'] == 'MODIFY':
old_item = record['dynamodb']['OldImage']
new_item = record['dynamodb']['NewImage']
# Check if status changed
if old_item['status']['S'] != new_item['status']['S']:
print(f"Status changed: {old_item['status']['S']} -> {new_item['status']['S']}")
elif record['eventName'] == 'REMOVE':
old_item = record['dynamodb']['OldImage']
print(f"Order deleted: {old_item}")---
Pricing Optimization
Billing Modes
| Mode | Use Case | Cost |
|---|---|---|
| On-Demand | Unpredictable traffic, dev/test | $1.25/million writes, $0.25/million reads |
| Provisioned | Predictable traffic | $0.47/WCU/month, $0.09/RCU/month |
| Reserved | Steady workloads (1-3 year) | Save up to 77% |
Cost Optimization Strategies
# 1. Use BatchGetItem (up to 100 items)
response = dynamodb.batch_get_item(
RequestItems={
'Users': {
'Keys': [
{'userId': 'user1'},
{'userId': 'user2'},
{'userId': 'user3'}
]
}
}
)
# 2. Use BatchWriteItem (up to 25 items)
dynamodb.batch_write_item(
RequestItems={
'Users': [
{'PutRequest': {'Item': {'userId': 'user1', 'name': 'User 1'}}},
{'PutRequest': {'Item': {'userId': 'user2', 'name': 'User 2'}}}
]
}
)
# 3. Use projection expressions (fetch only needed attributes)
response = table.query(
KeyConditionExpression=Key('PK').eq('USER#12345'),
ProjectionExpression='email, #name, createdAt',
ExpressionAttributeNames={'#name': 'name'}
)
# 4. Use eventually consistent reads (half the RCUs)
response = table.get_item(
Key={'userId': 'user123'},
ConsistentRead=False # Default
)
# 5. Use TTL for auto-expiring data (free deletes!)
table.meta.client.update_time_to_live(
TableName='Sessions',
TimeToLiveSpecification={
'Enabled': True,
'AttributeName': 'ttl'
}
)
# Add TTL to item (Unix timestamp)
import time
ttl_value = int(time.time()) + 86400 # Expire in 24 hours
table.put_item(Item={
'sessionId': 'session123',
'ttl': ttl_value
})---
Performance Best Practices
Design for Even Distribution
# GOOD: High cardinality partition key
PK: f"USER#{uuid.uuid4()}"
# GOOD: Composite key with multiple dimensions
PK: f"TENANT#{tenant_id}#USER#{user_id}"
# BAD: Low cardinality (hot partitions)
PK: f"STATUS#{status}" # Only 3 values: active, pending, archivedUse Sparse Indexes
# Only index items with specific attribute (saves storage)
# GSI: PK = emailVerified, SK = timestamp
# Only items with emailVerified=true are indexed
table.put_item(Item={
'userId': 'user123',
'email': 'user@example.com',
'emailVerified': True, # This item appears in GSI
'timestamp': '2025-11-28T10:00:00Z'
})
table.put_item(Item={
'userId': 'user456',
'email': 'other@example.com',
# No emailVerified - this item NOT in GSI
'timestamp': '2025-11-28T11:00:00Z'
})Adjacent Item Pattern (Reduce Queries)
# Store related items with adjacent sort keys
# Query once, get all related data
# User metadata
PK: "USER#12345", SK: "A#METADATA"
# User's addresses
PK: "USER#12345", SK: "B#ADDRESS#001"
PK: "USER#12345", SK: "B#ADDRESS#002"
# User's orders (most recent first)
PK: "USER#12345", SK: "C#ORDER#2025-11-28#001"
PK: "USER#12345", SK: "C#ORDER#2025-11-27#002"
# One query gets everything
response = table.query(
KeyConditionExpression=Key('PK').eq('USER#12345')
)Composite Sort Keys
# Hierarchical data in sort key
SK: "COUNTRY#USA#STATE#MA#CITY#Boston"
# Query all items in USA
Key('SK').begins_with('COUNTRY#USA')
# Query all items in Massachusetts
Key('SK').begins_with('COUNTRY#USA#STATE#MA')
# Query all items in Boston
Key('SK').begins_with('COUNTRY#USA#STATE#MA#CITY#Boston')---
TypeScript Examples (AWS SDK v3)
import {
DynamoDBClient,
PutItemCommand,
GetItemCommand,
QueryCommand,
UpdateItemCommand
} from '@aws-sdk/client-dynamodb'
import { marshall, unmarshall } from '@aws-sdk/util-dynamodb'
const client = new DynamoDBClient({ region: 'us-east-1' })
// Put item
await client.send(new PutItemCommand({
TableName: 'Users',
Item: marshall({
userId: 'user123',
email: 'user@example.com',
name: 'Jane Doe',
createdAt: new Date().toISOString()
})
}))
// Get item
const response = await client.send(new GetItemCommand({
TableName: 'Users',
Key: marshall({ userId: 'user123' })
}))
const user = unmarshall(response.Item!)
// Query
const queryResponse = await client.send(new QueryCommand({
TableName: 'Orders',
KeyConditionExpression: 'PK = :pk',
ExpressionAttributeValues: marshall({
':pk': 'USER#12345'
})
}))
const items = queryResponse.Items!.map(item => unmarshall(item))
// Update
await client.send(new UpdateItemCommand({
TableName: 'Users',
Key: marshall({ userId: 'user123' }),
UpdateExpression: 'SET #name = :name',
ExpressionAttributeNames: { '#name': 'name' },
ExpressionAttributeValues: marshall({ ':name': 'Jane Smith' })
}))---
Common Patterns
Multi-Tenant Architecture
# Partition by tenant
PK: "TENANT#abc#USER#12345"
SK: "METADATA"
# Query all users in tenant
response = table.query(
KeyConditionExpression=Key('PK').begins_with('TENANT#abc#USER#')
)Time-Series Data
# Partition by entity, sort by timestamp
PK: "SENSOR#sensor123"
SK: "2025-11-28T10:30:45.123Z"
# Query range
response = table.query(
KeyConditionExpression=Key('PK').eq('SENSOR#sensor123') &
Key('SK').between('2025-11-28T00:00:00Z', '2025-11-28T23:59:59Z')
)
# Use GSI for cross-sensor queries
# GSI: PK = date, SK = sensorIdVersioning
# Store versions with sort key
PK: "DOC#doc123"
SK: "VERSION#001"
SK: "VERSION#002"
SK: "VERSION#003"
# Get latest version
response = table.query(
KeyConditionExpression=Key('PK').eq('DOC#doc123'),
ScanIndexForward=False, # Descending order
Limit=1
)---
This guide covers DynamoDB single-table design and AWS-specific patterns. For Python FastAPI + MongoDB examples, see ../examples/dynamodb-serverless/.
Firestore Complete Guide
Firebase/GCP Firestore real-time sync, security rules, and mobile-first patterns.
Table of Contents
- Core Concepts
- Data Model
- Real-Time Listeners
- Security Rules
- Queries
- Offline Support
- Performance Best Practices
---
Core Concepts
Firestore vs Realtime Database
| Feature | Firestore | Realtime Database |
|---|---|---|
| Data Model | Collections & documents | JSON tree |
| Queries | Rich queries, indexes | Limited queries |
| Scaling | Automatic | Manual sharding |
| Offline | Full offline support | Limited |
| Pricing | Per operation | Per GB downloaded |
Key Features
- Real-time sync: Live updates across all clients
- Offline-first: Local cache, auto-sync when online
- Security rules: Declarative access control
- Atomic operations: Batched writes, transactions
- Automatic indexing: Composite indexes for queries
---
Data Model
Collections and Documents
users (collection)
├── user123 (document)
│ ├── email: "user@example.com"
│ ├── name: "Jane Doe"
│ └── orders (subcollection)
│ ├── order001 (document)
│ └── order002 (document)
└── user456 (document)
└── ...Document Structure
// Document in users collection
{
// Auto-generated ID
id: "user123",
// Document data
email: "user@example.com",
name: "Jane Doe",
age: 32,
address: {
street: "123 Main St",
city: "Boston",
state: "MA"
},
tags: ["premium", "verified"],
createdAt: Timestamp,
metadata: {
lastLogin: Timestamp,
loginCount: 42
}
}Document Limits:
- Max size: 1 MB
- Max depth: 20 levels
- Max field name: 1,500 bytes
---
Real-Time Listeners
React Component with Real-Time Updates
import { collection, query, where, onSnapshot } from 'firebase/firestore'
import { useEffect, useState } from 'react'
function OrderList({ userId }: { userId: string }) {
const [orders, setOrders] = useState([])
const [loading, setLoading] = useState(true)
useEffect(() => {
const q = query(
collection(db, 'orders'),
where('userId', '==', userId),
where('status', '==', 'pending')
)
// Real-time listener
const unsubscribe = onSnapshot(q, (snapshot) => {
const orderData = snapshot.docs.map(doc => ({
id: doc.id,
...doc.data()
}))
setOrders(orderData)
setLoading(false)
}, (error) => {
console.error('Error:', error)
})
// Cleanup on unmount
return () => unsubscribe()
}, [userId])
if (loading) return <div>Loading...</div>
return (
<div>
{orders.map(order => (
<div key={order.id}>{order.orderNumber}</div>
))}
</div>
)
}Listening to Document Changes
import { doc, onSnapshot } from 'firebase/firestore'
// Listen to single document
const unsubscribe = onSnapshot(doc(db, 'users', userId), (doc) => {
if (doc.exists()) {
console.log('User data:', doc.data())
}
})
// Detect change type
onSnapshot(collection(db, 'orders'), (snapshot) => {
snapshot.docChanges().forEach((change) => {
if (change.type === 'added') {
console.log('New order:', change.doc.data())
}
if (change.type === 'modified') {
console.log('Modified order:', change.doc.data())
}
if (change.type === 'removed') {
console.log('Removed order:', change.doc.data())
}
})
})---
Security Rules
Basic Rules Structure
// firestore.rules
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Rules go here
}
}Common Patterns
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// 1. Public read, authenticated write
match /products/{productId} {
allow read: if true;
allow write: if request.auth != null;
}
// 2. User can only access their own data
match /users/{userId} {
allow read, write: if request.auth != null && request.auth.uid == userId;
}
// 3. Orders: users can only see their own
match /orders/{orderId} {
// Read: must be authenticated and own the order
allow read: if request.auth != null &&
resource.data.userId == request.auth.uid;
// Create: must be authenticated and set userId to their own ID
allow create: if request.auth != null &&
request.resource.data.userId == request.auth.uid;
// Update/Delete: must own the order
allow update, delete: if request.auth != null &&
resource.data.userId == request.auth.uid;
}
// 4. Admin-only writes
match /config/{document} {
allow read: if true;
allow write: if request.auth != null &&
request.auth.token.admin == true;
}
// 5. Validate data on write
match /posts/{postId} {
allow create: if request.auth != null &&
request.resource.data.title is string &&
request.resource.data.title.size() > 0 &&
request.resource.data.title.size() <= 100 &&
request.resource.data.userId == request.auth.uid;
}
// 6. Subcollection access
match /users/{userId}/orders/{orderId} {
allow read, write: if request.auth != null && request.auth.uid == userId;
}
// 7. Rate limiting (prevent abuse)
match /posts/{postId} {
allow create: if request.auth != null &&
request.time > resource.data.lastPost + duration.value(1, 'm');
}
}
}Rule Functions
// Helper functions
function isSignedIn() {
return request.auth != null;
}
function isOwner(userId) {
return request.auth.uid == userId;
}
function isAdmin() {
return isSignedIn() && request.auth.token.admin == true;
}
function validString(field, minLen, maxLen) {
let value = request.resource.data[field];
return value is string &&
value.size() >= minLen &&
value.size() <= maxLen;
}
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /posts/{postId} {
allow read: if true;
allow create: if isSignedIn() &&
isOwner(request.resource.data.userId) &&
validString('title', 1, 100);
}
}
}---
Queries
Basic Queries
import { collection, query, where, orderBy, limit, getDocs } from 'firebase/firestore'
// Simple equality
const q = query(
collection(db, 'orders'),
where('userId', '==', 'user123')
)
const snapshot = await getDocs(q)
snapshot.forEach(doc => console.log(doc.data()))
// Multiple conditions (AND)
const q = query(
collection(db, 'products'),
where('category', '==', 'electronics'),
where('price', '<=', 500),
orderBy('price', 'asc')
)
// OR queries (requires composite index)
const q = query(
collection(db, 'products'),
or(
where('category', '==', 'electronics'),
where('category', '==', 'books')
)
)
// In queries (up to 10 values)
const q = query(
collection(db, 'products'),
where('category', 'in', ['electronics', 'books', 'toys'])
)
// Array contains
const q = query(
collection(db, 'users'),
where('tags', 'array-contains', 'premium')
)
// Array contains any
const q = query(
collection(db, 'users'),
where('tags', 'array-contains-any', ['premium', 'verified'])
)Pagination
// First page
const first = query(
collection(db, 'products'),
orderBy('price'),
limit(20)
)
const snapshot = await getDocs(first)
const lastVisible = snapshot.docs[snapshot.docs.length - 1]
// Next page
const next = query(
collection(db, 'products'),
orderBy('price'),
startAfter(lastVisible),
limit(20)
)Composite Indexes
Firestore automatically creates indexes for simple queries. Composite indexes required for:
- Multiple inequality filters
- Inequality + orderBy on different fields
- OR queries
// Requires composite index: category (asc), price (asc)
const q = query(
collection(db, 'products'),
where('category', '==', 'electronics'),
orderBy('price', 'asc')
)Create index via Firebase Console or CLI:
firebase deploy --only firestore:indexes---
CRUD Operations
Create
import { collection, addDoc, setDoc, doc } from 'firebase/firestore'
// Auto-generate ID
const docRef = await addDoc(collection(db, 'users'), {
email: 'user@example.com',
name: 'Jane Doe',
createdAt: new Date()
})
console.log('Created with ID:', docRef.id)
// Custom ID
await setDoc(doc(db, 'users', 'user123'), {
email: 'user@example.com',
name: 'Jane Doe'
})
// Merge (update if exists, create if not)
await setDoc(doc(db, 'users', 'user123'), {
lastLogin: new Date()
}, { merge: true })Read
import { doc, getDoc, collection, getDocs } from 'firebase/firestore'
// Get single document
const docSnap = await getDoc(doc(db, 'users', 'user123'))
if (docSnap.exists()) {
console.log(docSnap.data())
}
// Get all documents in collection
const querySnapshot = await getDocs(collection(db, 'users'))
querySnapshot.forEach(doc => console.log(doc.id, doc.data()))Update
import { doc, updateDoc, increment, arrayUnion, serverTimestamp } from 'firebase/firestore'
// Update fields
await updateDoc(doc(db, 'users', 'user123'), {
name: 'Jane Smith',
'address.city': 'Boston' // Nested field
})
// Increment counter
await updateDoc(doc(db, 'posts', 'post123'), {
views: increment(1)
})
// Add to array (no duplicates)
await updateDoc(doc(db, 'users', 'user123'), {
tags: arrayUnion('premium')
})
// Remove from array
await updateDoc(doc(db, 'users', 'user123'), {
tags: arrayRemove('trial')
})
// Server timestamp
await updateDoc(doc(db, 'users', 'user123'), {
updatedAt: serverTimestamp()
})Delete
import { doc, deleteDoc, deleteField } from 'firebase/firestore'
// Delete document
await deleteDoc(doc(db, 'users', 'user123'))
// Delete field
await updateDoc(doc(db, 'users', 'user123'), {
phoneNumber: deleteField()
})---
Transactions and Batches
Transactions (Atomic Reads and Writes)
import { runTransaction } from 'firebase/firestore'
// Transfer credits between users
await runTransaction(db, async (transaction) => {
const fromRef = doc(db, 'users', 'user123')
const toRef = doc(db, 'users', 'user456')
const fromDoc = await transaction.get(fromRef)
if (!fromDoc.exists()) {
throw new Error('User not found')
}
const currentBalance = fromDoc.data().credits
if (currentBalance < 100) {
throw new Error('Insufficient credits')
}
transaction.update(fromRef, { credits: currentBalance - 100 })
transaction.update(toRef, { credits: increment(100) })
})Batched Writes
import { writeBatch } from 'firebase/firestore'
// Batch write (up to 500 operations)
const batch = writeBatch(db)
batch.set(doc(db, 'users', 'user1'), { name: 'User 1' })
batch.update(doc(db, 'users', 'user2'), { active: true })
batch.delete(doc(db, 'users', 'user3'))
await batch.commit()---
Offline Support
Enable Offline Persistence
import { initializeFirestore, persistentLocalCache } from 'firebase/firestore'
const db = initializeFirestore(app, {
localCache: persistentLocalCache()
})Offline Behavior
import { onSnapshot } from 'firebase/firestore'
// Listener works offline
onSnapshot(collection(db, 'orders'), (snapshot) => {
snapshot.forEach(doc => {
// fromCache indicates if data is from local cache
console.log(`${doc.id} (from cache: ${doc.metadata.fromCache})`)
})
})
// Writes queued offline, synced when online
await addDoc(collection(db, 'orders'), {
userId: 'user123',
items: [...]
})
// If offline, write is queued and will sync when online---
Performance Best Practices
Minimize Document Reads
// BAD: Read same document multiple times
const userDoc = await getDoc(doc(db, 'users', userId))
// ... later ...
const userDoc2 = await getDoc(doc(db, 'users', userId)) // Duplicate read!
// GOOD: Cache document in memory
const userDoc = await getDoc(doc(db, 'users', userId))
const userData = userDoc.data()
// Use userData throughout componentUse Subcollections for Large Arrays
// BAD: Store unbounded array in document
{
userId: "user123",
orders: [
{ orderId: "order1", ... },
{ orderId: "order2", ... },
// ... 1000s of orders (exceeds 1MB limit!)
]
}
// GOOD: Use subcollection
// users/user123/orders/order1
// users/user123/orders/order2
const ordersRef = collection(db, 'users', userId, 'orders')Denormalize for Read Performance
// Store frequently accessed data together
{
postId: "post123",
title: "My Post",
content: "...",
// Denormalize author info (instead of reference)
author: {
id: "user123",
name: "Jane Doe",
avatar: "/avatars/jane.jpg"
},
// Update pattern: when user updates profile, update all posts
}Use Server Timestamps
import { serverTimestamp } from 'firebase/firestore'
// Better than client timestamp (avoids clock skew)
await addDoc(collection(db, 'posts'), {
title: 'My Post',
createdAt: serverTimestamp()
})---
Mobile Integration (React Native)
import { initializeApp } from 'firebase/app'
import { getFirestore, collection, onSnapshot } from 'firebase/firestore'
import { useEffect, useState } from 'react'
const firebaseConfig = {
apiKey: "...",
authDomain: "...",
projectId: "..."
}
const app = initializeApp(firebaseConfig)
const db = getFirestore(app)
function OrdersScreen({ userId }) {
const [orders, setOrders] = useState([])
useEffect(() => {
const unsubscribe = onSnapshot(
collection(db, 'orders'),
where('userId', '==', userId),
(snapshot) => {
const orderData = snapshot.docs.map(doc => ({
id: doc.id,
...doc.data()
}))
setOrders(orderData)
}
)
return () => unsubscribe()
}, [userId])
return (
<FlatList
data={orders}
keyExtractor={item => item.id}
renderItem={({ item }) => <OrderItem order={item} />}
/>
)
}---
This guide covers Firestore real-time patterns and mobile-first architecture. For complete React implementation, see ../examples/firestore-react/.
MongoDB Indexing Strategies
Complete guide to index types, optimization, and best practices for document databases.
Table of Contents
- Index Types
- Single Field Index
- Compound Index
- Text Index (Full-Text Search)
- Geospatial Index
- Partial Index (Index Subset)
- TTL Index (Auto-Delete)
- Sparse Index
- Index Selection Rules
- Covering Indexes
- Performance Optimization
- Index Intersection
- Index Prefix Usage
- Monitoring and Analysis
- List Indexes
- Index Usage Stats
- Explain Query
- Slow Query Log
- Index Maintenance
- Rebuild Index
- Drop Unused Indexes
- Best Practices
- Anti-Patterns
- Resources
Index Types
Single Field Index
db.users.createIndex({ email: 1 }, { unique: true }) // Ascending
db.posts.createIndex({ createdAt: -1 }) // DescendingCompound Index
Order matters! Equality → Range → Sort
// Query: WHERE status = 'active' AND createdAt > date ORDER BY createdAt
db.orders.createIndex({ status: 1, createdAt: -1 })
// Query: WHERE userId = 123 AND status = 'active' ORDER BY createdAt
db.orders.createIndex({ userId: 1, status: 1, createdAt: -1 })Text Index (Full-Text Search)
db.articles.createIndex({
title: "text",
content: "text",
tags: "text"
}, {
weights: { title: 3, content: 1, tags: 2 }, // Title 3x more important
name: "article_text_index"
})
// Search
db.articles.find({ $text: { $search: "mongodb indexing" } })
.sort({ score: { $meta: "textScore" } })Geospatial Index
db.locations.createIndex({ location: "2dsphere" })
// Find nearby
db.locations.find({
location: {
$near: {
$geometry: { type: "Point", coordinates: [-73.97, 40.77] },
$maxDistance: 5000, // 5km
}
}
})Partial Index (Index Subset)
// Only index active users
db.users.createIndex(
{ email: 1 },
{ partialFilterExpression: { status: { $eq: "active" } } }
)
// Only index large orders
db.orders.createIndex(
{ userId: 1, createdAt: -1 },
{ partialFilterExpression: { amount: { $gte: 1000 } } }
)TTL Index (Auto-Delete)
// Auto-delete sessions after 30 days
db.sessions.createIndex(
{ createdAt: 1 },
{ expireAfterSeconds: 2592000 } // 30 days
)Sparse Index
// Only index documents with field
db.users.createIndex({ phone: 1 }, { sparse: true })Index Selection Rules
Compound index order: 1. Equality filters first 2. Range filters second 3. Sort fields last
// Query: status = X AND createdAt > Y ORDER BY createdAt
// Index: { status: 1, createdAt: -1 } ✓ Correct order
// Index: { createdAt: -1, status: 1 } ✗ Wrong orderCovering Indexes
Index includes all queried fields (no document fetch needed):
// Query needs: userId, createdAt, amount
db.orders.createIndex({ userId: 1, createdAt: -1, amount: 1 })
// Query
db.orders.find(
{ userId: 123 },
{ userId: 1, createdAt: 1, amount: 1, _id: 0 } // Only indexed fields
).sort({ createdAt: -1 })
// COVERED - no document fetch!Performance Optimization
Index Intersection
MongoDB can combine multiple indexes:
db.orders.createIndex({ userId: 1 })
db.orders.createIndex({ status: 1 })
// MongoDB automatically intersects indexes
db.orders.find({ userId: 123, status: "pending" })Index Prefix Usage
Compound indexes can support prefix queries:
db.orders.createIndex({ userId: 1, status: 1, createdAt: -1 })
// Supports queries on:
// { userId }
// { userId, status }
// { userId, status, createdAt }
// Does NOT support:
// { status }
// { createdAt }Monitoring and Analysis
List Indexes
db.collection.getIndexes()Index Usage Stats
db.collection.aggregate([{ $indexStats: {} }])Explain Query
db.orders.explain("executionStats").find({ userId: 123 })
// Check for:
// - "stage": "IXSCAN" (good - uses index)
// - "stage": "COLLSCAN" (bad - full collection scan)
// - "totalDocsExamined" should be close to "nReturned"Slow Query Log
// Enable profiling
db.setProfilingLevel(1, { slowms: 100 }) // Log queries >100ms
// View slow queries
db.system.profile.find().sort({ ts: -1 }).limit(10)Index Maintenance
Rebuild Index
db.collection.reIndex() // Rebuilds all indexesDrop Unused Indexes
// Find unused indexes
db.collection.aggregate([{ $indexStats: {} }])
// Drop index
db.collection.dropIndex("index_name")Best Practices
1. Index queried fields - Every filter/sort should have index 2. Compound index order - Equality, Range, Sort 3. Use covering indexes - Avoid document fetches 4. Partial indexes - Index only needed subset 5. Monitor index usage - Drop unused indexes 6. Limit index count - Each index slows writes 7. Use explain() - Verify index usage 8. Index foreign keys - For $lookup performance 9. TTL indexes - Auto-cleanup for temp data 10. Text indexes - One per collection max
Anti-Patterns
❌ Over-indexing - 10+ indexes per collection slows writes ❌ Wrong compound order - Range before equality ❌ No index on filters - Collection scans on large data ❌ Index low-cardinality - Boolean fields rarely help ❌ Ignore index size - Indexes consume RAM
Resources
- MongoDB Indexes: https://www.mongodb.com/docs/manual/indexes/
- Index Performance: https://www.mongodb.com/docs/manual/core/index-performance/
Related skills
FAQ
When should I use a document database over relational?
Use one for flexible schemas that evolve rapidly without migrations, nested JSON-like structures, and horizontal scaling.
Which document database does this skill recommend?
MongoDB for general-purpose and complex queries, DynamoDB for AWS serverless, and Firestore for real-time and mobile-first apps.