Deploy a Real App
What you'll build
By the end of this lesson your to-do app, the React front-end and the Express API from the previous lessons, will be live on the internet, reachable at a custom domain, served over HTTPS. You will understand every step between "it works on my machine" and "it works for anyone on earth".
Concepts
Git basics
If you are not already using Git, start now. Git is the version control system used by virtually every software project on the planet. It tracks changes, lets you roll back, and is the mechanism by which you push code to deployment platforms.
# Initialise a new repo in the current directory
git init
# Stage all changed files
git add .
# Commit with a message
git commit -m "Initial commit"
# Connect to a GitHub repo and push
git remote add origin https://github.com/yourname/todo-app.git
git push -u origin main
The three-state model: files are either untracked, staged (added with git add), or committed. Only committed changes are permanent and shareable.
git status # see what has changed
git log --oneline # see commit history
git diff # see uncommitted changes
Before any deploy, always commit your current changes. This gives you a rollback point: if the deploy breaks something, git revert HEAD or git reset --hard <previous-commit-hash> gets you back.
A few important files to keep in .gitignore:
node_modules/
dist/
.env
*.log
Never commit .env, it contains your database password and API keys. Push only the .env.example template with dummy values so other developers know what variables to set.
Building for production
A production build is different from the development server in three ways: code is minified (smaller file size), dead code is removed (tree-shaking), and file names include content hashes (for cache busting).
# React / Vite front-end
npm run build
# Creates dist/, this is what you upload or deploy
# Express API, no build step needed for plain JavaScript
# If you are using TypeScript, compile with:
npx tsc
# Creates dist/, run with: node dist/index.js
Always test the production build locally before deploying:
npm run preview # Vite serves dist/ on http://localhost:4173
If something works in npm run dev but breaks in npm run preview, you have found a build-time issue. Fix it before deploying.
Deploying a static site (Vercel / Netlify)
Static sites, the output of npm run build, are just HTML, CSS, and JavaScript files. Any static hosting service can serve them.
Vercel (recommended for Vite/React projects):
- Push your project to GitHub.
- Go to
vercel.com, click "New Project", import the GitHub repo. - Vercel auto-detects Vite: build command
npm run build, output directorydist. - Click Deploy.
Every git push to main after that triggers a new deploy automatically. Pull requests get preview URLs so you can check changes before merging.
Netlify works the same way. Same import → build → deploy flow. For basic static hosting on a shared server like Hostinger, run npm run build and upload the contents of dist/ via FTP.
Deploying a Node API (Render)
Node servers need a platform that runs a persistent process. Render (render.com) is the easiest option:
- Push your Express API to GitHub.
- Go to Render, click "New" → "Web Service", connect the repo.
- Set:
- Build command:
npm install - Start command:
node server.js - Environment variables: Add
DATABASE_URLand any other.envvariables in Render's dashboard.
- Build command:
- Click Create Web Service.
Render gives you a URL like https://todo-api-xxxx.onrender.com. Update your React app's fetch URL to point to this instead of localhost:3001.
Free tier on Render spins down after 15 minutes of inactivity and takes ~30 seconds to spin up on the next request. For a portfolio project this is fine. For production, use a paid plan or a Hetzner/DigitalOcean VPS.
Custom domain and HTTPS
Both Vercel and Render let you add a custom domain in their dashboard. The steps are always the same:
- In your hosting dashboard, go to "Custom Domain" and enter your domain (e.g.,
todo.yourname.dev). - The platform gives you a CNAME or A record to add to your domain's DNS.
- Log in to your domain registrar (Namecheap, GoDaddy, Hostinger, etc.) and add the DNS record.
- Wait 5, 30 minutes for DNS to propagate.
- The platform automatically provisions a free TLS certificate via Let's Encrypt. HTTPS is on.
Domain registrar DNS settings:
Type | Name | Value
------+-------+------------------------------------------
CNAME | todo | cname.vercel-dns.com
This makes todo.yourname.dev point to Vercel, which serves your app.
Post-launch checklist
Before telling anyone the URL, run through these:
# 1. Test the build locally
npm run build && npm run preview
# 2. Check that environment variables are set on the platform, not just in .env
# (Your .env is not deployed, set them in the dashboard)
# 3. Check that CORS is configured for the production domain
# Change: app.use(cors())
# To: app.use(cors({ origin: "https://todo.yourname.dev" }))
# 4. Verify HTTPS works
curl -I https://todo.yourname.dev # look for "HTTP/2 200"
# 5. Check the browser console for errors after deploying
# (Missing assets, CORS errors, and 404s show up here)
Hands-on
Let's deploy the full stack app. Follow these steps in order.
Step 1: Prepare the React app for a real API URL.
In the React app, change the hardcoded localhost:3001 to an environment variable:
// src/App.jsx, change this line
const API_URL = import.meta.env.VITE_API_URL || "http://localhost:3001";
// Then use it in your fetch calls
const res = await fetch(`${API_URL}/todos`);
Create a .env file in the React project root:
VITE_API_URL=http://localhost:3001
Vite exposes environment variables prefixed with VITE_ to the browser. On Vercel, set VITE_API_URL=https://todo-api-xxxx.onrender.com in the project's Environment Variables settings.
Step 2: Deploy the Express API to Render.
cd todo-api
git init
git add .
git commit -m "Initial Express API"
git remote add origin https://github.com/yourname/todo-api.git
git push -u origin main
On Render, create a new Web Service from this repo. Add DATABASE_URL as an environment variable. After deploy, copy the service URL.
Step 3: Deploy the React front-end to Vercel.
cd post-browser # or whatever your React project folder is called
git init
git add .
git commit -m "Initial React app"
git remote add origin https://github.com/yourname/todo-frontend.git
git push -u origin main
On Vercel, import this repo. Add VITE_API_URL=https://your-render-service-url.onrender.com as an environment variable. Deploy.
Step 4: Update CORS in the API.
Once you have the Vercel URL, update server.js:
app.use(cors({ origin: process.env.FRONTEND_URL || "http://localhost:5173" }));
Add FRONTEND_URL=https://your-vercel-url.vercel.app to Render's environment variables. Redeploy the API (push a commit to trigger it).
Step 5: Add a custom domain (optional but recommended).
In Vercel → Project → Settings → Domains, add your domain. Add the CNAME record in your registrar. Wait for DNS. HTTPS is automatic.
Open your app at the custom domain. Check DevTools for any errors. You are live.
Common pitfalls
- Committing
.envto git. This exposes your database password to anyone who can see your repository. If you accidentally commit it, revoke and regenerate all credentials in that file, deleting the commit is not sufficient because git history persists. - Forgetting to set environment variables on the hosting platform.
.envis not uploaded by any platform. Every variable in.envmust be entered manually in the platform's dashboard or via their CLI. - Not updating CORS for the production domain.
cors()with no arguments allows any origin, fine for development but a security issue in production. Restrict it to your front-end's domain. - Serving the dev server in production.
npm run devis not for production. It serves unminified files and has no optimisations. Always deploy the output ofnpm run build. - DNS TTL confusion. DNS changes take time to propagate. If your domain is not working minutes after updating the DNS record, check
nslookup yourdomain.com 8.8.8.8to see what Google's DNS reports, it often propagates faster than your ISP's resolver.
What to try next
- Set up GitHub Actions to run
npm run buildon every push. If the build fails, you get an email before a broken version reaches your users. Create.github/workflows/build.ymlwith a basic checkout → install → build workflow. - Add a health check endpoint to your Express API:
app.get("/health", (req, res) => res.json({ status: "ok", uptime: process.uptime() })). Configure Render's health check to hit this URL. Render will restart the service automatically if it stops responding. - Set up a staging environment: create a second Vercel deployment connected to a
staginggit branch. Deploy to staging first, test, then merge tomainto deploy to production. This is how professional teams avoid shipping untested code to users.
Prefer watching over reading?
Subscribe for free.