Working on Quran.com, serving over 50 million users monthly, has taught me invaluable lessons about building scalable MERN stack applications. Here's what I've learned about taking projects from development to production.
The Foundation: Architecture Matters
Before writing a single line of code, I learned that architecture decisions can make or break your application's scalability.
Microservices vs Monolith
For most projects, start with a well-structured monolith:
// Good structure
project/
├── src/
│ ├── controllers/
│ ├── services/
│ ├── models/
│ ├── middlewares/
│ ├── utils/
│ └── routes/
Database Design for Scale
1. Proper Indexing
This single change improved our query performance by 80%:
// MongoDB indexing
userSchema.index({ email: 1 });
userSchema.index({ createdAt: -1 });
userSchema.index({ 'profile.location': 1, createdAt: -1 }); // Compound
2. Aggregation Pipeline
Use aggregation for complex queries:
const stats = await User.aggregate([
{ $match: { active: true } },
{ $group: {
_id: '$country',
count: { $sum: 1 },
avgAge: { $avg: '$age' }
}},
{ $sort: { count: -1 } },
{ $limit: 10 }
]);
3. Connection Pooling
mongoose.connect(MONGO_URI, {
maxPoolSize: 50,
minPoolSize: 10,
serverSelectionTimeoutMS: 5000,
});
API Design Best Practices
1. Proper Error Handling
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.isOperational = true;
}
}
// Usage
if (!user) {
throw new AppError('User not found', 404);
}
2. Request Validation
Use Joi or Zod:
const { z } = require('zod');
const userSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
age: z.number().min(18).max(120)
});
app.post('/users', async (req, res) => {
const validated = userSchema.parse(req.body);
// Process validated data
});
3. Rate Limiting
Protect your APIs:
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
message: 'Too many requests'
});
app.use('/api/', limiter);
Authentication & Security
JWT Best Practices
// Generate token
const generateToken = (userId) => {
return jwt.sign(
{ id: userId },
process.env.JWT_SECRET,
{ expiresIn: '7d' }
);
};
// Verify middleware
const protect = async (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
throw new AppError('Not authenticated', 401);
}
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = await User.findById(decoded.id);
next();
};
Secure Password Hashing
const bcrypt = require('bcrypt');
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();
this.password = await bcrypt.hash(this.password, 12);
next();
});
Performance Optimization
1. Caching with Redis
const redis = require('redis');
const client = redis.createClient();
const getUser = async (userId) => {
// Check cache first
const cached = await client.get(`user:${userId}`);
if (cached) return JSON.parse(cached);
// Fetch from DB
const user = await User.findById(userId);
// Store in cache
await client.setEx(
`user:${userId}`,
3600,
JSON.stringify(user)
);
return user;
};
2. Implement Pagination
const getPaginatedResults = async (req, res) => {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 10;
const skip = (page - 1) * limit;
const results = await Model.find()
.skip(skip)
.limit(limit)
.lean(); // Use lean() for better performance
const total = await Model.countDocuments();
res.json({
results,
pagination: {
page,
limit,
total,
pages: Math.ceil(total / limit)
}
});
};
3. Database Query Optimization
// Bad: Multiple queries
for (const post of posts) {
post.author = await User.findById(post.authorId);
}
// Good: Single query with populate
const posts = await Post.find()
.populate('author', 'name email')
.lean();
Monitoring & Logging
Winston Logger
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' })
]
});
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console());
}
Deployment Checklist
- [ ] Environment variables properly configured
- [ ] CORS configured correctly
- [ ] Rate limiting implemented
- [ ] Error handling in place
- [ ] Logging configured
- [ ] Database indexes created
- [ ] Security headers added (Helmet.js)
- [ ] HTTPS enabled
- [ ] Database backup strategy
- [ ] Monitoring set up (e.g., PM2, New Relic)
Key Takeaways
- Start simple: Don't over-engineer from day one
- Monitor everything: You can't improve what you don't measure
- Security first: It's harder to add later
- Test at scale: Use tools like k6 or Artillery for load testing
- Document as you go: Future you will thank present you
Real-World Impact
Implementing these practices on Quran.com helped us:
- Reduce API response times by 60%
- Handle 50M+ monthly users
- Achieve 99.9% uptime
- Scale to multiple regions
Building for scale isn't about using the fanciest tools—it's about making smart architectural decisions and following best practices consistently.
What challenges have you faced scaling MERN applications? Share your experiences!
Working on large-scale applications at Quran Foundation. Connect with me for more MERN stack insights!
Top comments (0)