DEV Community

Muhammad Zulqarnain Akram
Muhammad Zulqarnain Akram

Posted on

Building Production-Ready MERN Stack Applications: Lessons from 50M+ Users

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/
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 }
]);
Enter fullscreen mode Exit fullscreen mode

3. Connection Pooling

mongoose.connect(MONGO_URI, {
  maxPoolSize: 50,
  minPoolSize: 10,
  serverSelectionTimeoutMS: 5000,
});
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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
});
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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();
};
Enter fullscreen mode Exit fullscreen mode

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();
});
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

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)
    }
  });
};
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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());
}
Enter fullscreen mode Exit fullscreen mode

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

  1. Start simple: Don't over-engineer from day one
  2. Monitor everything: You can't improve what you don't measure
  3. Security first: It's harder to add later
  4. Test at scale: Use tools like k6 or Artillery for load testing
  5. 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)