A CI/CD pipeline automates the two most critical steps in software delivery: verifying that changes do not break anything (CI โ Continuous Integration), and deploying changes to production automatically when they pass (CD โ Continuous Deployment). For the MERN Blog, this means every push to the main branch runs the Jest and Vitest test suites โ and if they pass, triggers a Render and Netlify deployment. In this final lesson you will wire together a GitHub Actions workflow for CI, review the production readiness checklist, and add the security and performance essentials that make the MERN Blog safe for real users.
GitHub Actions CI Workflow
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
# โโ Server tests โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
server:
name: Server Tests
runs-on: ubuntu-latest
services:
mongodb:
image: mongo:7
ports: ['27017:27017']
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20', cache: 'npm', cache-dependency-path: server/package-lock.json }
- name: Install dependencies
run: cd server && npm ci
- name: Run tests
run: cd server && npm test -- --forceExit
env:
NODE_ENV: test
MONGODB_URI: mongodb://localhost:27017/blogdb_test
JWT_SECRET: test-secret-minimum-32-characters-long
# โโ Client tests โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
client:
name: Client Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20', cache: 'npm', cache-dependency-path: client/package-lock.json }
- name: Install dependencies
run: cd client && npm ci
- name: Run tests
run: cd client && npm test -- --run
- name: Build (verify no build errors)
run: cd client && npm run build
env:
VITE_API_URL: https://placeholder.onrender.com
# โโ Deploy on main (after both test jobs pass) โโโโโโโโโโโโโโโโโโโโโโโโโโโ
deploy:
name: Deploy
runs-on: ubuntu-latest
needs: [server, client] # only runs if both test jobs pass
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
steps:
- name: Trigger Render deploy
run: |
curl -X POST ${{ secrets.RENDER_DEPLOY_HOOK_URL }}
- name: Trigger Netlify deploy
run: |
curl -X POST ${{ secrets.NETLIFY_BUILD_HOOK_URL }}
needs: [server, client] โ it only runs if both test jobs succeed. This is the core of CI/CD: automated tests act as a gate before deployment. A failing test prevents the broken code from reaching production. Store the Render deploy hook URL and Netlify build hook URL as GitHub repository secrets (Settings โ Secrets and variables โ Actions) โ never hardcode them in the workflow file.cache: 'npm'), skip unnecessary matrix builds, and only run E2E tests on the main branch.Production Readiness Checklist
| Category | Item | Status |
|---|---|---|
| Security | Helmet.js security headers | โ
Add app.use(helmet()) |
| Rate limiting on auth routes | โ Add express-rate-limit | |
| HTTPS everywhere | โ Render and Netlify provide HTTPS automatically | |
| Secrets in environment variables | โ Never in code or .env committed to Git | |
| Performance | Gzip compression | โ
Add app.use(compression()) |
| Vite production build (minified) | โ
npm run build |
|
| Image CDN (Cloudinary) | โ Covered in Chapter 23 | |
| Reliability | MongoDB Atlas with replication | โ Atlas provides replica set by default |
| Process crash restart | โ Render restarts crashed services automatically | |
| Health check endpoint | โ
GET /api/health |
|
| Observability | Structured error logging | โ Console logs captured by Render |
| Error monitoring (Sentry) | โ ๏ธ Recommended โ see below | |
| Uptime monitoring | โ ๏ธ UptimeRobot (free) |
Rate Limiting on Auth Routes
npm install express-rate-limit
// server/src/middleware/rateLimiter.js
const rateLimit = require('express-rate-limit');
// General API rate limiter
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // 100 requests per window
standardHeaders: true,
legacyHeaders: false,
message: { success: false, message: 'Too many requests โ please try again in 15 minutes' },
});
// Strict limiter for auth endpoints
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 10, // only 10 login/register attempts per 15 minutes
message: { success: false, message: 'Too many auth attempts โ please try again later' },
skipSuccessfulRequests: true, // do not count successful logins
});
module.exports = { apiLimiter, authLimiter };
// server/index.js โ apply limiters
const { apiLimiter, authLimiter } = require('./src/middleware/rateLimiter');
app.use('/api', apiLimiter);
app.use('/api/auth', authLimiter); // stricter on auth routes
Error Monitoring with Sentry (Optional but Recommended)
npm install @sentry/node @sentry/react
// server/index.js โ Sentry for Express
const Sentry = require('@sentry/node');
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV,
tracesSampleRate: 0.1, // 10% of requests for performance tracing
});
app.use(Sentry.Handlers.requestHandler());
// ... routes ...
app.use(Sentry.Handlers.errorHandler()); // captures unhandled errors
// client/src/main.jsx โ Sentry for React
import * as Sentry from '@sentry/react';
Sentry.init({
dsn: import.meta.env.VITE_SENTRY_DSN,
environment: import.meta.env.MODE,
tracesSampleRate: 0.1,
});
Common Mistakes
Mistake 1 โ No rate limiting on auth routes
โ Wrong โ unlimited login attempts allowed:
POST /api/auth/login โ no limit
# Attacker can try 100,000 passwords per minute โ brute-force attack
โ Correct โ strict rate limiter on auth routes: 10 attempts per 15 minutes.
Mistake 2 โ Running CI tests against the production database
โ Wrong โ CI job uses the production MONGODB_URI:
MONGODB_URI: ${{ secrets.MONGODB_URI }} # production Atlas URI in CI!
# CI tests create and delete real data โ corrupts production database
โ Correct โ CI uses a separate MongoDB service container or a test-only Atlas connection string.
Mistake 3 โ Deploying without running tests first
โ Wrong โ deploy job runs unconditionally:
jobs:
deploy:
steps: [ ... ] # runs even if tests failed!
โ
Correct โ use needs: [server, client] so deploy only runs after all tests pass.
Quick Reference โ Deploy Hooks
| Service | Deploy Hook Location | Trigger |
|---|---|---|
| Render | Service โ Settings โ Deploy hooks โ Add hook | curl -X POST $RENDER_HOOK_URL |
| Netlify | Site settings โ Build and deploy โ Build hooks โ Add | curl -X POST $NETLIFY_HOOK_URL |