What you'll learn
By the end of this tutorial you'll be able to deploy a full MERN application to production-grade cloud services. You'll understand the three-service deployment topology (Atlas for the database, Render for Express, Vercel for React), how to configure environment variables for each service, how to fix the CORS issues that always appear between deployed services, and how to verify the full stack is working after every deploy.
Deployment is where many MERN projects stall — not because any individual step is hard, but because the order of operations isn't obvious and the error messages when something goes wrong aren't always helpful. This tutorial goes in the right order.
Who this is for
- Developers who have a working MERN app on localhost and need to put it on the internet
- Anyone who has tried to deploy before and hit confusing errors around CORS, environment variables, or Atlas connection strings
- Self-taught developers building portfolio projects for job applications who need a live URL
You can skip this if you're deploying with Docker on your own VPS or a cloud VM — that workflow differs significantly from the managed platform approach covered here. Come back if you want to switch to platform-as-a-service later.
What is the MERN deployment topology?
A deployed MERN app runs on three separate services: MongoDB Atlas (a managed cloud database), a Node.js hosting platform where Express runs, and a static hosting platform where the React build is served. They communicate over the internet rather than over localhost.
Plain English: in development, everything runs on your laptop on localhost. In production, each layer runs on a server in a data center. The only thing that changes in your actual code is the URLs — environment variables carry them so you don't hardcode them.
Simple idea: three services, one job each — Atlas stores data, Render runs your API, Vercel serves your frontend. HTTP connects them, the same way localhost connects them in development.
Prerequisites
- A working MERN app running on localhost (all four layers functional, tested locally)
- A GitHub account (Render and Vercel both deploy from GitHub repositories)
- A MongoDB Atlas account (free tier is sufficient to start)
- Your code in a git repository, ideally with
server/andclient/in separate folders
Setup from zero
Step 1 — Set up MongoDB Atlas
- Sign in at [cloud.mongodb.com](https://cloud.mongodb.com)
- Create a new Project → create a free M0 Shared cluster in a region close to where you'll host the API
- Under Database Access, create a database user with a strong username and password — save both
- Under Network Access, add
0.0.0.0/0(allow connections from anywhere) — you can restrict this later once Render's IP addresses are known - Click Connect → Drivers → copy the connection string
The string looks like:
mongodb+srv://youruser:yourpassword@cluster0.abc123.mongodb.net/yourdb?retryWrites=true&w=majority
Replace the placeholders with your actual values. This is your MONGO_URI for the next step.
> Little tip: Never paste your Atlas connection string directly into code or commit it to GitHub. Atlas strings contain your database password. If it ends up in a public repo, rotate it immediately from the Atlas dashboard — Database Access → Edit → Generate New Password.
Step 2 — Deploy the Express backend on Render
1. Go to [render.com](https://render.com) and create a free account, connecting your GitHub
2. New → Web Service → select your repository
3. Set Root Directory to server
4. Build Command: npm install
5. Start Command: node index.js (or your entry file)
6. Under Environment Variables, add all of these:
- MONGO_URI — your Atlas connection string
- JWT_SECRET — a fresh long random string (generate a new one for production; never reuse the development value)
- NODE_ENV — production
- CLIENT_URL — leave blank for now; you'll fill this after Vercel deploys
7. Click Create Web Service and wait for the first deploy to complete
Render gives you a URL like https://your-app-name.onrender.com. Visit https://your-app-name.onrender.com/api/posts to confirm it returns JSON. If it does, the backend is live.
Step 3 — Deploy the React frontend on Vercel
1. Go to [vercel.com](https://vercel.com) and sign in with GitHub
2. Add New → Project → import your repository
3. Set Root Directory to client
4. Vercel auto-detects Vite and Create React App — build settings are filled in automatically
5. Under Environment Variables, add:
- VITE_API_URL (for Vite) or REACT_APP_API_URL (for Create React App) — set to your Render URL: https://your-app-name.onrender.com
6. Click Deploy
Once Vercel gives you a URL (e.g., https://your-app.vercel.app), go back to Render and set CLIENT_URL to that URL. Redeploy the Render service. Now both sides know about each other.
> Little tip: Prefix Vite environment variables with VITE_ and Create React App variables with REACT_APP_. These prefixes are required — variables without them are not injected into the browser bundle and will be undefined at runtime. This is one of the most common "why is my API URL undefined in production?" bugs.
The mental model
Think of deployment as replacing all localhost references with real URLs, then telling each service to trust the others.
Development configuration:
- React fetches from http://localhost:5000
- Express connects to mongodb://localhost:27017
- CORS allows everything because it's your own machine
Production configuration:
- React reads VITE_API_URL → https://your-api.onrender.com
- Express reads MONGO_URI → Atlas connection string
- CORS allows only https://your-app.vercel.app
Your application logic doesn't change. Only the configuration values change, and environment variables are what carry those values safely between environments.
Key terms
Environment variable — a named value set outside the code, read at runtime with process.env.NAME (server) or import.meta.env.VITE_NAME (Vite). The mechanism for keeping URLs and secrets out of source code.
CORS (Cross-Origin Resource Sharing) — a browser security policy that blocks requests from one origin (e.g., https://your-app.vercel.app) to a different origin (e.g., https://your-api.onrender.com) unless the server explicitly sends the right headers permitting it.
Build output — the dist/ or build/ folder created when you run npm run build in the React project. This contains optimized static HTML, JavaScript, and CSS. Vercel serves this folder — not your source files.
Health check endpoint — a simple route that returns 200 to signal the server is alive. GET /health returning { status: "ok" } is the conventional form. Monitoring tools and load balancers call this to detect crashes.
Cold start — on Render's free tier, services spin down after 15 minutes of inactivity. The first request after a period of inactivity can take 30–60 seconds while the service restarts. Expected behavior on the free tier; a paid plan eliminates it.
Step-by-step: production CORS configuration
Update your Express CORS setup to allow only known origins:
// server/index.js — production-ready CORS
const allowedOrigins = [
"http://localhost:5173",
"http://localhost:3000",
process.env.CLIENT_URL,
].filter(Boolean);
app.use(cors({
origin: (origin, callback) => {
// allow requests with no origin (curl, Postman, server-to-server)
if (!origin || allowedOrigins.includes(origin)) {
return callback(null, true);
}
callback(new Error(`CORS: origin ${origin} not allowed`));
},
credentials: true,
}));
Update your React API client to read the URL from an environment variable:
// client/src/api/client.ts
const BASE_URL = import.meta.env.VITE_API_URL ?? "http://localhost:5000";
export async function apiFetch(path: string, options?: RequestInit) {
const res = await fetch(`${BASE_URL}${path}`, options);
if (!res.ok) {
const data = await res.json().catch(() => ({}));
throw new Error(data.error ?? `Request failed: ${res.status}`);
}
return res.json();
}
Add a health check to your Express server:
app.get("/health", (req, res) =>
res.json({ status: "ok", timestamp: new Date().toISOString() })
);
Patterns
Log the database connection on startup — make Atlas connection success or failure visible immediately in Render's logs:
mongoose.connect(process.env.MONGO_URI)
.then(() => {
console.log("MongoDB connected");
app.listen(process.env.PORT || 5000, () => console.log("Server running"));
})
.catch(err => {
console.error("MongoDB connection failed:", err.message);
process.exit(1);
});
Use process.env.PORT for the server port — Render sets this automatically. Hardcoding 5000 works locally but may conflict on the platform:
app.listen(process.env.PORT || 5000);
Common mistakes
Hardcoded localhost in React fetches — if any React file contains fetch("http://localhost:5000/...") without reading from an environment variable, production requests will try to reach your laptop from Vercel. Always use VITE_API_URL or REACT_APP_API_URL.
Forgetting to set environment variables on Render — the service deploys successfully but crashes on first request because process.env.MONGO_URI is undefined. Render's logs will show a connection error in the first few lines. Environment variables are always the first thing to check.
Atlas Network Access restricting Render's IPs — Render's free tier uses rotating IP addresses. Restricting Atlas access to a specific IP will break on the first IP rotation. Use 0.0.0.0/0 until you upgrade to a paid Render plan that provides static outbound IPs.
Troubleshooting
React shows a blank page on Vercel — open the browser console. Almost every blank-page issue is a JavaScript runtime error. The most common causes: VITE_API_URL not set in Vercel environment variables, or an unhandled fetch error thrown during initial render.
Render logs show MongooseServerSelectionError on startup — the Express server can't reach Atlas. Check that MONGO_URI is set correctly in Render's Environment section. Copy the exact connection string directly from the Atlas Connect dialog to avoid typos.
CORS error in the browser — "Access to fetch at ... has been blocked by CORS policy". The Vercel origin isn't in the allowedOrigins array, or CLIENT_URL isn't set in Render's environment. Check both. The exact origin matters — https://your-app.vercel.app and https://your-app.vercel.app/ (trailing slash) are treated differently.
Checklist
- [ ] MongoDB Atlas cluster created with a database user and network access set to
0.0.0.0/0 - [ ]
MONGO_URIenvironment variable set on Render - [ ] Fresh
JWT_SECRET— not the development value — set on Render - [ ]
CLIENT_URLset on Render pointing to the Vercel deployment URL - [ ] Express CORS allows the Vercel origin via
CLIENT_URL - [ ] React reads API URL from
VITE_API_URLorREACT_APP_API_URL, never hardcoded - [ ]
VITE_API_URLset on Vercel pointing to the Render service URL - [ ]
GET /healthendpoint returns 200 on the deployed API - [ ] Browser console shows no errors on the deployed Vercel app
- [ ] Full user flow tested end-to-end on the live URLs
Practice task
Deploy the notes app from the previous tutorials. Verify the complete flow on the live deployment: create an account, log in, create a note, confirm the note persists after a full page refresh, and confirm the note list only shows your notes. Check Render's logs for the "MongoDB connected" and "Server running" lines. Check the browser console for any errors throughout.
FAQ
Is Render's free tier production-ready?
For portfolio projects and demos, yes. For anything with real users, the 30–60 second cold start after inactivity is a poor experience. A paid Render instance starts at $7/month and eliminates cold starts — worth it once the app has actual users.
Should I serve React from Express instead of using Vercel?
You can — Express can serve React's dist/ folder as static files, consolidating to one deployed service. This simplifies deployment but couples frontend and backend deploys. Separate deployments on Vercel and Render are cleaner for most projects and let you deploy frontend changes without touching the backend.
How do I set up continuous deployment so new pushes auto-deploy?
Both Render and Vercel connect to GitHub and auto-deploy on every push to the main branch by default. You don't need to configure anything — it works from the moment you connect the repository.
What to learn next
After a stable deployment: adding a custom domain (Vercel and Render both support custom domains with free SSL), setting up uptime monitoring with a free tool like UptimeRobot, and structuring a CI pipeline with GitHub Actions for automated testing before deploys.
Related on Baseline
- MERN stack architecture — the app structure this deployment guide assumes
- MERN auth basics — ensuring auth secrets are configured correctly for production
- MERN API design — structuring the Express layer that Render runs
Takeaways
MERN deployment is three services replacing three localhost addresses: Atlas replaces your local MongoDB, Render replaces localhost:5000, Vercel replaces localhost:5173. Environment variables carry the URLs; CORS configuration tells Express which origins to trust. The application code stays identical — only the configuration values change between development and production.
If you remember only one thing: every hardcoded localhost URL in your React code is a production bug waiting to happen. Replace every single one with an environment variable before you deploy — it takes minutes and saves hours of confused production debugging.