Deploying an Express App and Best Practices

advanced
16 min

Deploying an Express App and Best Practices

Your API works on localhost. The final step is making it work for everyone, reliably, on the public internet. This lesson walks through preparing an Express app for production, deploying it to a modern host, and the security and performance practices that separate hobby projects from professional services.

Production Readiness Checklist

Before deploying, make sure the app itself is production-shaped:

  • Read the port and every secret from environment variables, never from code.
  • Set NODE_ENV=production, which disables verbose errors and speeds up Express and EJS.
  • Ensure a start script exists, because hosts run npm start by convention.
  • Never commit .env; commit a .env.example documenting required variables instead.
json
"scripts": { "start": "node index.js", "dev": "nodemon index.js" }
js
const PORT = process.env.PORT || 3000; app.listen(PORT, () => console.log('Listening on ' + PORT));

Security Hardening

Three middleware packages give you enormous protection for three lines of code:

bash
npm install helmet cors express-rate-limit compression
js
const helmet = require('helmet'); const cors = require('cors'); const rateLimit = require('express-rate-limit'); const compression = require('compression'); app.use(helmet()); // security headers app.use(cors({ origin: process.env.CLIENT_URL })); // explicit allowed origin app.use(rateLimit({ windowMs: 15 * 60 * 1000, max: 200 })); app.use(compression()); // gzip responses

helmet sets headers that block clickjacking and MIME sniffing. cors should name your real frontend origin rather than allowing every site. Rate limiting blunts brute-force and scraping. compression shrinks JSON and HTML responses dramatically.

Graceful Shutdown

Hosts send SIGTERM before restarting your process. Finish in-flight requests instead of dropping them:

js
const server = app.listen(PORT); process.on('SIGTERM', () => { server.close(() => { console.log('Server closed cleanly'); process.exit(0); }); });

Choosing a Host

  • Render and Railway: connect a GitHub repo, set environment variables in the dashboard, and every push deploys automatically. The easiest path for this course.
  • Fly.io: containers close to your users, generous free allowance, needs a small config file.
  • A VPS (DigitalOcean, Hetzner): full control; you manage Node, a reverse proxy like Nginx or Caddy for TLS, and a process manager. More work, more learning.

For a typical flow on Render: push your repo, create a Web Service, set the build command to npm install, the start command to npm start, add MONGODB_URI and JWT_SECRET as environment variables, and deploy. Your MongoDB should live in Atlas so the database and app scale independently.

Keeping It Alive with PM2

On a VPS, a crash would take your site down until you notice. PM2 restarts the process, balances across CPU cores, and survives reboots:

bash
npm install -g pm2 pm2 start index.js -i max --name my-api pm2 save && pm2 startup

Platform hosts like Render handle restarts for you, which is a big part of what you pay for.

Observability

You cannot fix what you cannot see. At minimum, log every request with morgan in combined mode, add a /health endpoint returning 200 for uptime monitors, and send crash reports somewhere persistent. Structured JSON logging with pino is the professional upgrade when logs need searching.

Key Takeaways

  • Configuration lives in environment variables; set NODE_ENV=production.
  • helmet, cors, rate limiting, and compression are the minimum production middleware.
  • Handle SIGTERM so deploys never drop in-flight requests.
  • Platform hosts (Render, Railway, Fly.io) deploy from Git; a VPS with PM2 gives full control.
  • Add a /health endpoint and real logging before you need them.

Congratulations! You have completed the Express.js series. Revisit the REST API lesson and try shipping that project end to end. As a next step, explore testing Express apps with Jest and Supertest.