All posts
Backend & APIs

MERN Stack Architecture: Building Scalable Full-Stack Applications

17 min
Share:
MERNMongoDBExpressReactNode.jsFull Stack

Build production-ready MERN applications with proper architecture, authentication, state management, API design, and deployment strategies. Complete guide from development to production.

The MERN stack (MongoDB, Express, React, Node.js) powers modern web applications. This guide covers production-grade architecture patterns, security, state management, and deployment for scalable full-stack apps.

Project Structure and Architecture

1# Production-ready MERN project structure
2mern-app/
3├── client/                    # React frontend
4│   ├── public/
5│   ├── src/
6│   │   ├── components/        # Reusable UI components
7│   │   ├── pages/             # Page components
8│   │   ├── hooks/             # Custom hooks
9│   │   ├── context/           # React context providers
10│   │   ├── services/          # API calls
11│   │   ├── utils/             # Helper functions
12│   │   ├── App.jsx
13│   │   └── main.jsx
14│   ├── package.json
15│   └── vite.config.js
16├── server/                    # Express backend
17│   ├── src/
18│   │   ├── config/            # Configuration files
19│   │   │   ├── db.js          # MongoDB connection
20│   │   │   └── env.js         # Environment variables
21│   │   ├── controllers/       # Route handlers
22│   │   ├── models/            # Mongoose models
23│   │   ├── routes/            # API routes
24│   │   ├── middleware/        # Custom middleware
25│   │   ├── utils/             # Helper functions
26│   │   ├── validators/        # Input validation
27│   │   └── server.js          # App entry point
28│   ├── tests/
29│   └── package.json
30├── docker-compose.yml         # Development environment
31└── README.md
32
33# Why this structure?
34# - Separation of concerns (client/server)
35# - Easy to deploy separately (frontend CDN, backend servers)
36# - Clear module boundaries
37# - Testable code organization

Backend: Express API with MongoDB

1// server/src/config/db.js
2import mongoose from 'mongoose';
3
4export const connectDB = async () => {
5  try {
6    const conn = await mongoose.connect(process.env.MONGODB_URI, {
7      maxPoolSize: 50,
8      wtimeoutMS: 2500,
9      serverSelectionTimeoutMS: 5000
10    });
11    console.log(`MongoDB Connected: ${conn.connection.host}`);
12  } catch (error) {
13    console.error(`Error: ${error.message}`);
14    process.exit(1);
15  }
16};
17
18// server/src/models/User.js
19import mongoose from 'mongoose';
20import bcrypt from 'bcryptjs';
21
22const userSchema = new mongoose.Schema({
23  email: {
24    type: String,
25    required: true,
26    unique: true,
27    lowercase: true,
28    trim: true,
29    index: true
30  },
31  password: {
32    type: String,
33    required: true,
34    minlength: 8,
35    select: false  // Don't return password by default
36  },
37  name: {
38    type: String,
39    required: true,
40    trim: true
41  },
42  role: {
43    type: String,
44    enum: ['user', 'admin'],
45    default: 'user'
46  },
47  isVerified: {
48    type: Boolean,
49    default: false
50  }
51}, {
52  timestamps: true  // createdAt, updatedAt
53});
54
55// Hash password before saving
56userSchema.pre('save', async function(next) {
57  if (!this.isModified('password')) return next();
58  this.password = await bcrypt.hash(this.password, 12);
59  next();
60});
61
62// Method to check password
63userSchema.methods.comparePassword = async function(candidatePassword) {
64  return bcrypt.compare(candidatePassword, this.password);
65};
66
67export default mongoose.model('User', userSchema);
68
69// server/src/controllers/authController.js
70import User from '../models/User.js';
71import jwt from 'jsonwebtoken';
72
73export const register = async (req, res) => {
74  try {
75    const { email, password, name } = req.body;
76    
77    // Check if user exists
78    const existingUser = await User.findOne({ email });
79    if (existingUser) {
80      return res.status(400).json({ error: 'User already exists' });
81    }
82    
83    // Create user
84    const user = await User.create({ email, password, name });
85    
86    // Generate token
87    const token = jwt.sign(
88      { userId: user._id },
89      process.env.JWT_SECRET,
90      { expiresIn: '7d' }
91    );
92    
93    res.status(201).json({
94      token,
95      user: {
96        id: user._id,
97        email: user.email,
98        name: user.name
99      }
100    });
101  } catch (error) {
102    res.status(500).json({ error: error.message });
103  }
104};
105
106// server/src/middleware/auth.js
107export const authenticate = async (req, res, next) => {
108  try {
109    const token = req.headers.authorization?.replace('Bearer ', '');
110    
111    if (!token) {
112      return res.status(401).json({ error: 'No token provided' });
113    }
114    
115    const decoded = jwt.verify(token, process.env.JWT_SECRET);
116    const user = await User.findById(decoded.userId);
117    
118    if (!user) {
119      return res.status(401).json({ error: 'User not found' });
120    }
121    
122    req.user = user;
123    next();
124  } catch (error) {
125    res.status(401).json({ error: 'Invalid token' });
126  }
127};

Frontend: React with Context API

1// client/src/context/AuthContext.jsx
2import { createContext, useContext, useState, useEffect } from 'react';
3import { authService } from '../services/api';
4
5const AuthContext = createContext(null);
6
7export const AuthProvider = ({ children }) => {
8  const [user, setUser] = useState(null);
9  const [loading, setLoading] = useState(true);
10
11  useEffect(() => {
12    // Check if user is logged in on mount
13    const token = localStorage.getItem('token');
14    if (token) {
15      authService.getProfile()
16        .then(setUser)
17        .catch(() => localStorage.removeItem('token'))
18        .finally(() => setLoading(false));
19    } else {
20      setLoading(false);
21    }
22  }, []);
23
24  const login = async (email, password) => {
25    const { token, user } = await authService.login(email, password);
26    localStorage.setItem('token', token);
27    setUser(user);
28  };
29
30  const logout = () => {
31    localStorage.removeItem('token');
32    setUser(null);
33  };
34
35  return (
36    <AuthContext.Provider value={{ user, loading, login, logout }}>
37      {children}
38    </AuthContext.Provider>
39  );
40};
41
42export const useAuth = () => {
43  const context = useContext(AuthContext);
44  if (!context) {
45    throw new Error('useAuth must be used within AuthProvider');
46  }
47  return context;
48};
49
50// client/src/services/api.js
51import axios from 'axios';
52
53const api = axios.create({
54  baseURL: import.meta.env.VITE_API_URL || 'http://localhost:5000/api'
55});
56
57// Add token to requests
58api.interceptors.request.use((config) => {
59  const token = localStorage.getItem('token');
60  if (token) {
61    config.headers.Authorization = `Bearer ${token}`;
62  }
63  return config;
64});
65
66// Handle 401 errors
67api.interceptors.response.use(
68  (response) => response,
69  (error) => {
70    if (error.response?.status === 401) {
71      localStorage.removeItem('token');
72      window.location.href = '/login';
73    }
74    return Promise.reject(error);
75  }
76);
77
78export const authService = {
79  login: async (email, password) => {
80    const { data } = await api.post('/auth/login', { email, password });
81    return data;
82  },
83  register: async (email, password, name) => {
84    const { data } = await api.post('/auth/register', { email, password, name });
85    return data;
86  },
87  getProfile: async () => {
88    const { data } = await api.get('/auth/profile');
89    return data;
90  }
91};

State Management Patterns

1// Option 1: Context + useReducer for complex state
2import { createContext, useReducer, useContext } from 'react';
3
4const initialState = {
5  posts: [],
6  loading: false,
7  error: null,
8  currentPage: 1
9};
10
11function postsReducer(state, action) {
12  switch (action.type) {
13    case 'FETCH_START':
14      return { ...state, loading: true, error: null };
15    case 'FETCH_SUCCESS':
16      return { ...state, loading: false, posts: action.payload };
17    case 'FETCH_ERROR':
18      return { ...state, loading: false, error: action.payload };
19    case 'ADD_POST':
20      return { ...state, posts: [action.payload, ...state.posts] };
21    default:
22      return state;
23  }
24}
25
26export const PostsProvider = ({ children }) => {
27  const [state, dispatch] = useReducer(postsReducer, initialState);
28  
29  const fetchPosts = async () => {
30    dispatch({ type: 'FETCH_START' });
31    try {
32      const data = await api.get('/posts');
33      dispatch({ type: 'FETCH_SUCCESS', payload: data });
34    } catch (error) {
35      dispatch({ type: 'FETCH_ERROR', payload: error.message });
36    }
37  };
38  
39  return (
40    <PostsContext.Provider value={{ state, dispatch, fetchPosts }}>
41      {children}
42    </PostsContext.Provider>
43  );
44};
45
46// Option 2: Zustand for simpler state management
47import create from 'zustand';
48
49export const usePostsStore = create((set, get) => ({
50  posts: [],
51  loading: false,
52  error: null,
53  
54  fetchPosts: async () => {
55    set({ loading: true, error: null });
56    try {
57      const data = await api.get('/posts');
58      set({ posts: data, loading: false });
59    } catch (error) {
60      set({ error: error.message, loading: false });
61    }
62  },
63  
64  addPost: (post) => set((state) => ({
65    posts: [post, ...state.posts]
66  }))
67}));
68
69// Usage in component
70function PostsList() {
71  const { posts, loading, fetchPosts } = usePostsStore();
72  
73  useEffect(() => {
74    fetchPosts();
75  }, []);
76  
77  if (loading) return <Spinner />;
78  return <div>{posts.map(post => <PostCard key={post.id} post={post} />)}</div>;
79}

Production Deployment

1# docker-compose.yml for development
2version: '3.8'
3
4services:
5  mongodb:
6    image: mongo:7
7    ports:
8      - "27017:27017"
9    environment:
10      MONGO_INITDB_ROOT_USERNAME: admin
11      MONGO_INITDB_ROOT_PASSWORD: password
12    volumes:
13      - mongo-data:/data/db
14
15  backend:
16    build: ./server
17    ports:
18      - "5000:5000"
19    environment:
20      MONGODB_URI: mongodb://admin:password@mongodb:27017/myapp?authSource=admin
21      JWT_SECRET: your-secret-key
22      NODE_ENV: development
23    depends_on:
24      - mongodb
25    volumes:
26      - ./server:/app
27      - /app/node_modules
28
29  frontend:
30    build: ./client
31    ports:
32      - "5173:5173"
33    environment:
34      VITE_API_URL: http://localhost:5000/api
35    volumes:
36      - ./client:/app
37      - /app/node_modules
38
39volumes:
40  mongo-data:
41
42# Production: Deploy frontend to Vercel/Netlify
43# Deploy backend to AWS/Azure/Railway
44# Use MongoDB Atlas for database

Security Best Practices

  • Use HTTPS in production (Let's Encrypt for free SSL)
  • Validate all inputs with libraries like joi or express-validator
  • Use parameterized queries to prevent NoSQL injection
  • Rate limit API endpoints to prevent abuse (express-rate-limit)
  • Implement CORS properly - don't use * in production
  • Store sensitive data in environment variables, never in code
  • Hash passwords with bcrypt (min 12 rounds)
  • Use JWT with short expiration times, refresh tokens for long sessions
  • Sanitize user input before rendering (prevent XSS)
  • Use helmet middleware for security headers
  • Enable MongoDB authentication in production
  • Keep dependencies updated (npm audit fix)
  • Implement proper error handling - don't leak stack traces
  • Use CSRF tokens for state-changing operations
  • Log security events for audit trails