Lesson 10 of 108 min read

Deploy a Real App

Share:WhatsAppLinkedIn

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):

  1. Push your project to GitHub.
  2. Go to vercel.com, click "New Project", import the GitHub repo.
  3. Vercel auto-detects Vite: build command npm run build, output directory dist.
  4. 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:

  1. Push your Express API to GitHub.
  2. Go to Render, click "New" → "Web Service", connect the repo.
  3. Set:
    • Build command: npm install
    • Start command: node server.js
    • Environment variables: Add DATABASE_URL and any other .env variables in Render's dashboard.
  4. 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:

  1. In your hosting dashboard, go to "Custom Domain" and enter your domain (e.g., todo.yourname.dev).
  2. The platform gives you a CNAME or A record to add to your domain's DNS.
  3. Log in to your domain registrar (Namecheap, GoDaddy, Hostinger, etc.) and add the DNS record.
  4. Wait 5, 30 minutes for DNS to propagate.
  5. 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 .env to 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. .env is not uploaded by any platform. Every variable in .env must 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 dev is not for production. It serves unminified files and has no optimisations. Always deploy the output of npm 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.8 to see what Google's DNS reports, it often propagates faster than your ISP's resolver.

What to try next

  1. Set up GitHub Actions to run npm run build on every push. If the build fails, you get an email before a broken version reaches your users. Create .github/workflows/build.yml with a basic checkout → install → build workflow.
  2. 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.
  3. Set up a staging environment: create a second Vercel deployment connected to a staging git branch. Deploy to staging first, test, then merge to main to deploy to production. This is how professional teams avoid shipping untested code to users.

Test Your Knowledge

Take a quick quiz on this lesson

Start Quiz →

Prefer watching over reading?

Subscribe for free.

Subscribe on YouTube