MERN Deployment Architecture Overview

Deploying a MERN application means taking three separate concerns โ€” the database, the API server, and the React frontend โ€” and hosting each in the right environment for its purpose. MongoDB runs best on a managed cloud service (Atlas) that handles replication, backups, and scaling. The Express API runs on a Node.js host (Render, Railway, or a VPS) that can execute server code. The React build output is static HTML, CSS, and JavaScript that can be served from a global CDN (Netlify, Vercel, or Cloudflare Pages) at essentially zero cost and with near-instant load times worldwide. Understanding this architecture โ€” and how the three pieces communicate through environment variables and CORS โ€” is the foundation for everything in this chapter.

Production Architecture Overview

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                         MERN Blog Production                         โ”‚
โ”‚                                                                      โ”‚
โ”‚  Browser                                                             โ”‚
โ”‚    โ”‚                                                                 โ”‚
โ”‚    โ”œโ”€โ”€โ†’ https://mernblog.netlify.app  (React SPA)                   โ”‚
โ”‚    โ”‚       Netlify CDN ยท Vite build output ยท _redirects fallback     โ”‚
โ”‚    โ”‚                                                                 โ”‚
โ”‚    โ””โ”€โ”€โ†’ https://api.mernblog.onrender.com  (Express API)            โ”‚
โ”‚            Render Node.js service ยท PORT=10000                       โ”‚
โ”‚            MONGODB_URI โ†’ MongoDB Atlas cluster                       โ”‚
โ”‚            JWT_SECRET, CLOUDINARY_*, EMAIL_*, ...                    โ”‚
โ”‚                                                                      โ”‚
โ”‚  MongoDB Atlas (M0 free tier)                                        โ”‚
โ”‚    ยท mernblog.cluster0.mongodb.net                                   โ”‚
โ”‚    ยท Network access: Render service IP + your dev IP                 โ”‚
โ”‚    ยท Database user: mernblog-prod (strong password)                  โ”‚
โ”‚    ยท Automated backups enabled                                       โ”‚
โ”‚                                                                      โ”‚
โ”‚  Cloudinary CDN  (images, avatars, cover photos)                     โ”‚
โ”‚  Resend / SendGrid  (transactional email)                            โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
Note: The React SPA and Express API are hosted on completely separate domains in production. This is intentional โ€” the static React files do not need a Node.js server, so hosting them on a CDN (Netlify) is faster and cheaper. The Express API runs on a separate Node.js host (Render). CORS is configured on Express to allow requests from the Netlify domain, and the React app uses VITE_API_URL=https://api.mernblog.onrender.com to know where to send API requests.
Tip: Keep your development and production environments as similar as possible. Use the same versions of Node.js (specify in .nvmrc or Render’s environment settings), the same MongoDB major version (Atlas lets you choose), and the same npm package versions (commit package-lock.json). The more different production is from development, the more likely you are to encounter “works on my machine” bugs.
Warning: Never commit production secrets to Git. Production environment variables โ€” MONGODB_URI, JWT_SECRET, CLOUDINARY_API_SECRET, email passwords โ€” must be set in your hosting platform’s environment variable UI, not in any file that is committed to the repository. Add .env, .env.local, and .env.production to .gitignore before your first commit and never remove them.

Environment Variables by Layer

Layer Variable Value in Production Set In
React (Vite) VITE_API_URL https://api.mernblog.onrender.com Netlify โ†’ Site settings โ†’ Env vars
Express MONGODB_URI Atlas connection string Render โ†’ Environment
Express JWT_SECRET 64-char random hex Render โ†’ Environment
Express JWT_EXPIRES_IN 7d Render โ†’ Environment
Express CLIENT_URL https://mernblog.netlify.app Render โ†’ Environment
Express NODE_ENV production Render โ†’ Environment
Express CLOUDINARY_* From Cloudinary dashboard Render โ†’ Environment
Express RESEND_API_KEY From Resend dashboard Render โ†’ Environment

Production CORS Configuration

// server/index.js โ€” production-ready CORS
const corsOptions = {
  origin: (origin, callback) => {
    const allowed = [
      process.env.CLIENT_URL,      // https://mernblog.netlify.app
      'http://localhost:5173',     // local dev (optional โ€” remove in strict prod)
    ].filter(Boolean);

    // Allow requests with no origin (Postman, server-to-server)
    if (!origin || allowed.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error(`CORS: ${origin} not allowed`));
    }
  },
  credentials:    true,
  methods:        ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
  allowedHeaders: ['Content-Type', 'Authorization'],
};

app.use(cors(corsOptions));
app.options('*', cors(corsOptions)); // preflight for all routes

Production vs Development Differences

Concern Development Production
Database Local MongoDB or Atlas free tier Atlas M0 free or paid cluster
API URL Vite proxy โ†’ localhost:5000 https://api.mernblog.onrender.com
Email Ethereal (fake SMTP) Resend or SendGrid
File storage Local disk (or Cloudinary) Cloudinary CDN
Error messages Full stack trace in response Generic message (stack hidden)
Logging Console Console + structured (Render captures)
HTTPS HTTP is fine Always HTTPS โ€” cookies, JWT

Common Mistakes

Mistake 1 โ€” Hardcoding the API URL in React

โŒ Wrong โ€” URL hardcoded, breaks in production:

const api = axios.create({ baseURL: 'http://localhost:5000/api' }); // breaks in prod!

โœ… Correct โ€” use Vite environment variable:

const api = axios.create({
  baseURL: import.meta.env.VITE_API_URL || 'http://localhost:5000/api',
}); // โœ“ different per environment

Mistake 2 โ€” Forgetting to set NODE_ENV=production

โŒ Wrong โ€” NODE_ENV undefined โ†’ Express shows full error stack traces to users, uses dev email transporter.

โœ… Correct โ€” set NODE_ENV=production in the Render environment variables. This enables error message sanitisation, production email transport, and performance optimisations in Express and other libraries.

Mistake 3 โ€” Not adding a health check endpoint

โŒ Wrong โ€” hosting platforms cannot verify the API is running:

// No health endpoint โ€” Render uses root / which may redirect or return HTML

โœ… Correct โ€” add a simple health check route:

app.get('/api/health', (req, res) => res.json({ status: 'ok', env: process.env.NODE_ENV }));
// Render health check URL: https://api.mernblog.onrender.com/api/health โœ“

Quick Reference โ€” Hosting Platforms

Service What to Host Free Tier
MongoDB Atlas Database M0: 512MB storage, shared cluster
Render Express API (Node.js) 750 hrs/month, sleeps after 15min idle
Netlify React SPA (static) 100GB bandwidth, 300 build min/month
Cloudinary Image storage + CDN 25GB storage, 25GB bandwidth
Resend Transactional email 3,000 emails/month

🧠 Test Yourself

Your production React app calls the API but gets CORS errors. The Express server’s CORS configuration has origin: 'http://localhost:5173'. The production React app is at https://mernblog.netlify.app. What needs to change?