DEV Community

syed bilal
syed bilal

Posted on

MERN Stack Best Practices: Lessons from 3 Years in Production

Building Scalable Web Applications with the MERN Stack: Lessons from 3 Years in Production

By Syed Khizer Ali

Introduction

Over the past three years working as a MERN Stack Developer, I've built and maintained full-stack applications ranging from small client tools to production systems serving real users. Along the way, I've learned that writing code that works is very different from writing code that scales. This article shares practical lessons learned while building with MongoDB, Express.js, React, and Node.js — the stack that has powered most of my professional projects.

1. Structuring the Backend for Maintainability

Early in my career, I made the mistake of putting business logic directly inside route handlers. As applications grew, this became difficult to test and maintain. The fix was adopting a layered architecture:

routes/       → define endpoints only
controllers/  → handle request/response logic
services/     → contain business logic
models/       → Mongoose schemas
middlewares/  → auth, validation, error handling
Enter fullscreen mode Exit fullscreen mode

This separation makes it far easier to unit test business logic independently of Express, and keeps route files short and readable.

2. MongoDB Schema Design: Don't Just "Store Data"

A common beginner mistake with MongoDB is treating it like a relational database with foreign keys everywhere, which leads to excessive population queries and performance issues. Instead:

  • Embed data that is read together and doesn't change often (e.g., an order's shipping address).
  • Reference data that grows unbounded or is shared across many documents (e.g., a user referenced by many orders).
  • Use indexes deliberately — a missing index on a frequently queried field can silently degrade performance as data grows.

3. State Management in React: Keep It Simple Until You Can't

Not every project needs Redux. For most medium-sized applications, a combination of React's built-in useContext and useReducer handles global state perfectly well. I now reach for external state libraries only when:

  • State needs to persist across many disconnected components
  • There's complex async flow (e.g., caching server data, optimistic updates)

For server state specifically, libraries like React Query (TanStack Query) solve caching, refetching, and loading states far better than manually managing them with useEffect.

4. Authentication: Beyond "Just Use JWT"

JWT-based authentication is the MERN standard, but there are details that matter in production:

  • Store access tokens in memory (not localStorage) to reduce XSS exposure.
  • Use short-lived access tokens paired with longer-lived refresh tokens stored in httpOnly cookies.
  • Always validate and sanitize input on the backend — never trust the frontend, even for your own applications.

5. Error Handling That Actually Helps

A centralized error-handling middleware in Express saves enormous time:

app.use((err, req, res, next) => {
  const statusCode = err.statusCode || 500;
  res.status(statusCode).json({
    success: false,
    message: err.message || "Internal Server Error",
  });
});
Enter fullscreen mode Exit fullscreen mode

Pairing this with custom error classes (e.g., NotFoundError, ValidationError) makes debugging and API consistency much easier across a growing codebase.

6. Performance: Small Habits That Compound

  • Use .lean() on Mongoose queries when you don't need full document methods — it significantly reduces overhead.
  • Paginate any endpoint that could return unbounded lists.
  • On the frontend, lazy-load routes and heavy components using React.lazy() and Suspense.
  • Compress API responses with compression middleware in Express.

7. Deployment Lessons

Moving from "it works on my machine" to a real production deployment taught me to:

  • Keep environment-specific configuration in .env files, never hard-coded.
  • Set up proper logging (e.g., Winston or Pino) instead of relying on console.log in production.
  • Use process managers like PM2 to keep Node.js applications alive and to enable zero-downtime restarts.

Conclusion

The MERN stack's flexibility is both its biggest strength and its biggest trap — it gives you very few opinions out of the box, which means the quality of the final application depends heavily on the architectural decisions made early on. The lessons above aren't exhaustive, but they represent the patterns that have consistently saved time and prevented bugs across the projects I've worked on.


Syed Khizer Ali is a Software Engineer and MERN Stack Developer at Brandive Media Solutions, with three years of experience building full-stack web applications.

Top comments (0)