Deploying a MERN Stack App: Netlify + Render + MongoDB Atlas

Deploying a MERN Stack App: Netlify + Render + MongoDB Atlas

Building a MERN stack app locally is one thing. Getting it live on the internet — with a real database, a real backend, and a real frontend URL — is where a lot of developers get stuck. The process involves three separate platforms that each need to be configured correctly and connected to each other, and a single misconfiguration can leave you with a working app locally that's completely broken in production.

This guide walks you through the entire deployment process end to end. We'll set up MongoDB Atlas for the database, deploy the Express backend to Render, and ship the Vite React frontend to Netlify. We'll also cover environment variables on both platforms and the most common post-deployment issues and how to fix them. The same stack powering the Music Academy and E-Commerce projects on ProjectSchool — projects built with this exact setup — is what you'll be deploying here.

The Stack and Why These Three Platforms

MongoDB Atlas is MongoDB's fully managed cloud database. Instead of running MongoDB on your own server, Atlas handles the infrastructure, backups, and scaling. You get a free tier (M0) that's more than enough for small to medium projects, and a connection string that works identically in development and production.

Render is a cloud platform for deploying backend services. It supports Node.js natively, connects directly to your GitHub repository, and redeploys automatically whenever you push to main. The free tier is generous and the developer experience is significantly smoother than alternatives like Heroku or AWS for straightforward Express APIs.

Netlify is the go-to platform for deploying frontend applications. It detects Vite projects automatically, handles the build process, serves your app from a global CDN, and gives you features like form handling, redirects, and environment variables out of the box. Deployments happen automatically on every push to your connected branch.

The combination of these three platforms gives you a production-grade deployment with zero server management, automatic deploys, and a generous free tier — all without a DevOps background.

Recommended Project Structure Before You Deploy

Before touching any deployment platform, make sure your project is structured in a way that separates the frontend and backend cleanly. The most common pattern is a monorepo with a client folder and a server folder at the root:

javascript
my-mern-app/
├── client/          # Your Vite React frontend
│   ├── src/
│   ├── public/
│   ├── index.html
│   ├── vite.config.js
│   └── package.json
├── server/          # Your Express backend
│   ├── models/
│   ├── routes/
│   ├── middleware/
│   ├── server.js
│   └── package.json
└── .gitignore

Both client and server have their own package.json and their own node_modules. They are deployed independently — the server goes to Render, the client goes to Netlify. This separation is important: Netlify only needs to know about the client folder, and Render only needs to know about the server folder.

Make sure your root .gitignore covers both: add node_modules, .env, and dist to it. Never commit your .env files — this is how API keys and database credentials get exposed.

Part 1: Setting Up MongoDB Atlas

Step 1: Create Your Atlas Account and Cluster

  • Go to mongodb.com/atlas and sign up for a free account
  • Click 'Build a Database' and select the free M0 tier
  • Choose a cloud provider (AWS, Google Cloud, or Azure) and a region closest to your users
  • Give your cluster a name and click 'Create'

Atlas will take a minute or two to provision your cluster. While it's creating, set up your access credentials.

Step 2: Create a Database User

  • In the left sidebar go to Security > Database Access
  • Click 'Add New Database User'
  • Choose 'Password' authentication and create a strong username and password
  • Set the role to 'Read and write to any database'
  • Save the username and password — you'll need them for the connection string
Use a generated password without special characters like @, /, or : — these can break your connection string if not URL-encoded. Stick to alphanumeric characters for simplicity.

Step 3: Whitelist IP Addresses

  • In the left sidebar go to Security > Network Access
  • Click 'Add IP Address'
  • For development: add your current IP address
  • For production (Render): click 'Allow Access From Anywhere' (0.0.0.0/0) — Render uses dynamic IPs so a fixed whitelist won't work

Allowing access from anywhere is safe as long as your database user has a strong password. Atlas authentication still applies — the IP whitelist is just a secondary layer.

Step 4: Get Your Connection String

  • Go to your cluster and click 'Connect'
  • Choose 'Connect your application'
  • Select 'Node.js' as the driver
  • Copy the connection string — it looks like: mongodb+srv://<username>:<password>@cluster0.xxxxx.mongodb.net/<dbname>?retryWrites=true&w=majority

Replace,, andwith your actual values. Save this string — it becomes your MONGO_URI environment variable. Never hardcode it in your source files.

Step 5: Connect Atlas to Your Express App

In your server folder, install Mongoose if you haven't already:

bash
cd server
npm install mongoose

Then connect in your server.js. The connection should happen before you start listening for requests:

javascript
// server/server.js
const express = require("express");
const mongoose = require("mongoose");
const cors = require("cors");
require("dotenv").config();

const app = express();

app.use(cors());
app.use(express.json());

// Routes
app.use("/api/users", require("./routes/users"));

// Connect to MongoDB Atlas, then start the server
mongoose
  .connect(process.env.MONGO_URI)
  .then(() => {
    console.log("Connected to MongoDB Atlas");
    app.listen(process.env.PORT || 5000, () => {
      console.log(`Server running on port ${process.env.PORT || 5000}`);
    });
  })
  .catch((err) => {
    console.error("MongoDB connection failed:", err.message);
    process.exit(1); // Exit if DB connection fails — no point running without it
  });

Create a .env file in your server folder for local development:

javascript
# server/.env
MONGO_URI=mongodb+srv://youruser:yourpassword@cluster0.xxxxx.mongodb.net/myapp?retryWrites=true&w=majority
PORT=5000

Part 2: Deploying the Backend to Render

Step 6: Prepare Your Backend for Production

Before deploying, make sure your server/package.json has a start script that Render can use:

javascript
// server/package.json
{
  "name": "my-mern-server",
  "version": "1.0.0",
  "scripts": {
    "start": "node server.js",
    "dev": "nodemon server.js"
  },
  "dependencies": {
    "cors": "^2.8.5",
    "dotenv": "^16.0.0",
    "express": "^4.18.0",
    "mongoose": "^7.0.0"
  }
}

Render runs npm start in production, so this must exist and point to your entry file. Also make sure dotenv is in dependencies, not devDependencies — Render needs it at runtime.

Step 7: Create a Render Web Service

  • Go to render.com and sign up or log in
  • Click 'New' and select 'Web Service'
  • Connect your GitHub account and select your repository
  • Give your service a name (e.g. my-mern-api)
  • Set the Root Directory to server (this tells Render to only look inside the server folder)
  • Set the Build Command to: npm install
  • Set the Start Command to: npm start
  • Select the free instance type and click 'Create Web Service'

Render will start deploying immediately. You can watch the build logs in real time. The first deploy usually takes 2-3 minutes.

Step 8: Add Environment Variables on Render

Your .env file is in .gitignore so it was never pushed to GitHub. Render needs these values but can't read your local file. You add them directly in the Render dashboard:

  • Go to your Web Service in the Render dashboard
  • Click the 'Environment' tab in the left sidebar
  • Click 'Add Environment Variable'
  • Add MONGO_URI with your full Atlas connection string as the value
  • Add NODE_ENV with the value production
  • Click 'Save Changes' — Render will automatically redeploy with the new variables
Never set a PORT variable on Render. Render assigns the port dynamically and injects it as process.env.PORT automatically. If you hardcode a port or override it, your service won't start correctly.

Once deployed, Render gives you a public URL for your backend — something like https://my-mern-api.onrender.com. Test it by visiting https://my-mern-api.onrender.com/api/users in your browser. You should get a JSON response. Copy this URL — you'll need it for the frontend.

Part 3: Deploying the Frontend to Netlify

Step 9: Update Your API Base URL for Production

In development, your frontend calls http://localhost:5000/api. In production, it needs to call your Render URL. The clean way to handle this is with a Vite environment variable. In your client folder, create two .env files:

bash
# client/.env.development  (used by npm run dev)
VITE_API_URL=http://localhost:5000/api

# client/.env.production  (used by npm run build)
VITE_API_URL=https://my-mern-api.onrender.com/api

Vite automatically loads the correct file based on the environment. In development mode it uses .env.development, and when you run npm run build it uses .env.production. In your React code, access it like this:

javascript
// src/api/users.js
const API_URL = import.meta.env.VITE_API_URL;

export const getUsers = async () => {
  const res = await fetch(`${API_URL}/users`);
  if (!res.ok) throw new Error("Failed to fetch users");
  return res.json();
};
Vite environment variables must start with VITE_ to be exposed to the browser. Variables without this prefix are kept server-side and will be undefined in your React code.

Step 10: Add a Netlify Redirects File

This step is critical and very commonly missed. When a user visits your app at /dashboard directly or refreshes the page on a route like /profile, Netlify tries to find a file at that path. Since your app is a single-page application, that file doesn't exist — only index.html does. Netlify returns a 404.

The fix is a _redirects file that tells Netlify to always serve index.html for any path, and let React Router handle the routing client-side. Create this file inside your client/public folder:

javascript
# client/public/_redirects
/*    /index.html    200

Vite copies everything in the public folder into the build output automatically, so this file will be in the right place after npm run build. Without it, any direct URL visit or page refresh will break on Netlify.

Step 11: Deploy to Netlify

  • Go to netlify.com and sign up or log in
  • Click 'Add new site' and choose 'Import an existing project'
  • Connect your GitHub account and select your repository
  • Set the Base Directory to client
  • Set the Build Command to: npm run build
  • Set the Publish Directory to: client/dist
  • Click 'Deploy site'

Netlify will run npm run build inside the client folder and deploy the contents of dist. The first deploy takes about a minute. Once done, Netlify gives you a URL like https://my-mern-app.netlify.app.

Step 12: Add Environment Variables on Netlify

  • Go to your site in the Netlify dashboard
  • Navigate to Site Configuration > Environment Variables
  • Click 'Add a variable'
  • Add VITE_API_URL with your full Render backend URL as the value: https://my-mern-api.onrender.com/api
  • Click 'Save' and then trigger a new deploy from the Deploys tab

Netlify injects environment variables at build time for Vite apps — not at runtime. This means after adding or changing a variable, you must redeploy for it to take effect. The variable is baked into the built JavaScript bundle during the build process.

Part 4: Fixing Common Post-Deployment Issues

Issue 1: CORS Errors

CORS (Cross-Origin Resource Sharing) errors are the most common issue after a first deployment. You'll see something like: "Access to fetch at 'https://my-mern-api.onrender.com' from origin 'https://my-mern-app.netlify.app' has been blocked by CORS policy."

This happens because your backend is on a different domain to your frontend. In development, both were on localhost so the browser didn't enforce CORS. In production, they're on completely different domains and the browser blocks the request unless your server explicitly allows it.

Fix it by configuring CORS on your Express server to allow your Netlify domain:

javascript
// server/server.js
const cors = require("cors");

const allowedOrigins = [
  "http://localhost:5173",              // Local development
  "https://my-mern-app.netlify.app",   // Production frontend
];

app.use(
  cors({
    origin: function (origin, callback) {
      // Allow requests with no origin (e.g. Postman, server-to-server)
      if (!origin || allowedOrigins.includes(origin)) {
        callback(null, true);
      } else {
        callback(new Error("Not allowed by CORS"));
      }
    },
    credentials: true, // Required if you're sending cookies or auth headers
  })
);

Issue 2: Environment Variables Not Working

  • On Render: check that the variable name matches exactly — MONGO_URI not MongoURI or MONGODB_URI
  • On Netlify: remember variables are baked in at build time — always redeploy after adding or changing them
  • In your React code: make sure all Vite variables start with VITE_ — anything else is undefined in the browser
  • Never use process.env in Vite React code — use import.meta.env instead
  • Check you're not accidentally committing a .env file — run git status and verify it's listed as ignored

Issue 3: Render Cold Starts

On Render's free tier, your web service spins down after 15 minutes of inactivity. The next request that comes in after the service has spun down triggers a cold start — the server needs to boot up, install nothing (dependencies are cached), and connect to MongoDB before it can respond. This can take anywhere from 30 seconds to 2 minutes, and during that time your frontend will appear to hang.

There are a few ways to handle this. The simplest is to show a loading state in your frontend that has a longer timeout and a message like "This may take a moment on first load." A more proactive solution is to ping your backend on a schedule using a free service like cron-job.org — set up a cron job to hit your health check endpoint every 10 minutes to keep the service warm.

javascript
// Add a health check endpoint to your Express server
// Cron-job.org (or similar) can ping this every 10 minutes to prevent cold starts
app.get("/health", (req, res) => {
  res.status(200).json({ status: "ok", timestamp: new Date().toISOString() });
});

If cold starts are unacceptable for your project, upgrading to Render's paid tier ($7/month) keeps your service running continuously. For production apps with real users, this is absolutely worth it.

Issue 4: 404 on Page Refresh (Netlify)

If you see a "Page not found" error when refreshing a page or visiting a URL directly, you've missed the _redirects file from Step 10. Go back and add client/public/_redirects with the contents /* /index.html 200, then redeploy. This fixes it instantly.

Issue 5: MongoDB Connection Failing on Render

  • Double check your MONGO_URI on Render matches exactly what Atlas gave you
  • Make sure 0.0.0.0/0 is in your Atlas Network Access whitelist — Render uses dynamic IPs
  • Check your database username and password have no special characters that need URL-encoding
  • In the Render logs, look for 'Connected to MongoDB Atlas' — if it's not there, the connection is failing silently
  • Make sure mongoose is in dependencies (not devDependencies) in your server/package.json

Pre-Launch Checklist

Before you share your live URL with anyone, run through this checklist:

  • MongoDB Atlas: cluster created, database user set up, 0.0.0.0/0 whitelisted
  • server/.env is in .gitignore and was never committed to GitHub
  • MONGO_URI and NODE_ENV=production are set in Render environment variables
  • Express CORS config includes your Netlify domain
  • Health check endpoint added to keep the free tier warm
  • client/public/_redirects file exists with /* /index.html 200
  • VITE_API_URL is set in Netlify environment variables and points to your Render URL
  • Frontend redeployed after adding Netlify environment variables
  • Test all API calls from the live frontend — not just locally
  • Test a page refresh on a non-root route — should not 404

Final Thoughts

Deploying a MERN stack app for the first time feels complex because you're coordinating three separate platforms that all need to know about each other. But once you've done it once, the process becomes second nature. The same steps that deploy a small side project will deploy a full production application — the scale changes, but the process doesn't.

The most important habits to build are keeping credentials out of your codebase, using environment variables for anything that changes between development and production, and testing your live deployment thoroughly rather than assuming it works because it worked locally.

Have a question about a specific deployment error you're hitting, or want to see how to add a custom domain to your Netlify site? Drop a comment in LiveChat and I'll help you get it sorted.