Intro
This guide shows how to operate an Express API safely using concrete, minimal commands. You will move from observation to a verified change, with examples you can run locally or in production-like environments. The focus is operational safety: observe first, limit the blast radius, use placeholders instead of secrets, verify results, and document how to recover if the expected state is not reached.
Scope and audience: developers, DevOps engineers, and startup teams who run Node.js Express APIs. Examples target Express 4.x/5.x on Node.js LTS and work with common deployment setups (local node, PM2, systemd, Docker). Replace placeholders like <PORT> and <SERVICE_NAME> with your own values.
Version and Environment Inventory
Goal: capture what is running, where, and how, using read-only commands before you change anything.
Prerequisites:
- Node.js LTS (v18 or later recommended) and npm
- curl or httpie for HTTP checks
- Access to your app directory (<PROJECT_DIR>) or container/orchestrator
- Record versions and timestamps
# Timestamp (record in your change notes)
date -u
# Node and npm versions
node -v
npm -v
# Express version in the current project
cd <PROJECT_DIR>
npm ls express --depth=0 || echo "Express not installed here"
# Application version (from package.json)
node -p "require('./package.json').version" 2>/dev/null || echo "No package.json version"
- Identify topology and runtime
- Local node process:
ps aux | grep node | grep <PROJECT_DIR> | grep -v grep
lsof -i :<PORT> -sTCP:LISTEN || ss -lntp | grep :<PORT>
- PM2:
pm2 status
pm2 describe <SERVICE_NAME>
pm2 logs <SERVICE_NAME> --lines 50
- systemd:
systemctl status <SERVICE_NAME>.service
journalctl -u <SERVICE_NAME>.service -n 100 --no-pager
- Docker:
docker ps --filter "name=<SERVICE_NAME>"
docker logs <CONTAINER_ID> --tail 100
docker inspect <CONTAINER_ID> --format '{{json .NetworkSettings.Ports}}' | jq
- Read-only API observations
Start with endpoints that do not change state. If you do not have health endpoints yet, add them later using the Safe Configuration Path.
# Replace with your host/port and route
curl -sS -D - http://localhost:<PORT>/health || echo "Health not reachable"
# View status code and timing
curl -sS -o /dev/null -w "status=%{http_code} time=%{time_total}\n" http://localhost:<PORT>/health
Define your expected result and a failure signal before acting. For example: expected 200 OK within 100 ms; failure is connection refused, timeout, or 5xx.
Safety notes:
- Keep credentials, tokens, and service URLs out of terminal history. Use environment variables like $DB_URI and never paste real secrets into documentation or tickets.
- Do not restart or redeploy during inventory. The goal is to learn, not to change.
Safe Configuration Path
Goal: make the smallest justified change, verify it, and have a tested path to undo it.
Example change A: add basic operational endpoints (/health, /ready, /version) without exposing secrets.
Prerequisites:
- You control <PROJECT_DIR>
- You can restart the process (PM2/systemd/Docker) with minimal impact
- Observe current state
# Baseline: latency and status
curl -sS -o /dev/null -w "status=%{http_code} time=%{time_total}\n" http://localhost:<PORT>/health
- Make the minimal change
app.js (or server.js):
const express = require('express');
const pkg = require('./package.json');
const app = express();
const PORT = process.env.PORT || 3000;
// Safe body size limit; adjust as needed
app.use(express.json({ limit: process.env.JSON_LIMIT || '1mb' }));
// Health endpoints: read-only, no secrets
app.get('/health', (req, res) => res.status(200).json({ status: 'ok' }));
app.get('/ready', (req, res) => {
// Optionally check downstreams (DB ping, cache). Return 503 if not ready.
res.status(200).json({ ready: true });
});
app.get('/version', (req, res) => res.json({ name: pkg.name, version: pkg.version }));
app.listen(PORT, () => {
console.log(`API listening on :${PORT}`);
});
- Verify the outcome
# Restart via PM2
pm2 restart <SERVICE_NAME> && pm2 logs <SERVICE_NAME> --lines 20
# Or systemd
sudo systemctl restart <SERVICE_NAME>.service
sudo systemctl status <SERVICE_NAME>.service --no-pager
# Or Docker Compose
docker compose up -d --no-deps --build <SERVICE_NAME>
echo "HEALTH:"; curl -sS -D - http://localhost:<PORT>/health -o /dev/null | head -n 1
echo "READY :"; curl -sS -D - http://localhost:<PORT>/ready -o /dev/null | head -n 1
echo "VER :"; curl -sS http://localhost:<PORT>/version | jq
Expected result: /health and /ready return 200, /version returns name and version fields.
- Recovery path
- If the process fails to start: revert the code change (git restore), redeploy, and confirm status returns to baseline.
- If only one endpoint is failing (e.g., /ready due to a downstream check): return a static 200 temporarily, create a follow-up task to implement proper readiness checks.
Example change B: configure strict CORS for a single origin.
const cors = require('cors');
app.use(cors({ origin: process.env.CORS_ORIGIN || 'https://app.example.com', methods: ['GET','POST','PUT','DELETE','OPTIONS'] }));
Verification:
# Preflight check
curl -sS -i -X OPTIONS \
-H "Origin: https://app.example.com" \
-H "Access-Control-Request-Method: GET" \
http://localhost:<PORT>/health | sed -n '1,10p'
Rollback: remove the CORS middleware line and restart, or set CORS_ORIGIN to a safe value.
Verification and Diagnostics
Goal: confirm behavior objectively and isolate issues quickly using read-only tools first.
- HTTP-level checks
# Status + timing
curl -sS -o /dev/null -w "status=%{http_code} time=%{time_total}\n" http://localhost:<PORT>/health
# Headers and body preview
curl -sS -D - http://localhost:<PORT>/version | sed -n '1,20p'
# JSON correctness
curl -sS http://localhost:<PORT>/version | jq type
- Routing and content-type errors
Common signals:
- 404 Not Found: wrong path or base URL mismatch. Verify route registration and mount path.
- 415 Unsupported Media Type: client missed Content-Type header. Try:
curl -sS -X POST http://localhost:<PORT>/items \
-H 'Content-Type: application/json' \
-d '{"name":"demo"}' -i
- CORS diagnostics
# Preflight OPTIONS must return 204/200 and Access-Control-Allow-* headers
curl -sS -i -X OPTIONS \
-H "Origin: https://app.example.com" \
-H "Access-Control-Request-Method: POST" \
http://localhost:<PORT>/items | sed -n '1,20p'
- Process and logs
# PM2
pm2 logs <SERVICE_NAME> --lines 50
# systemd
journalctl -u <SERVICE_NAME>.service -n 100 --no-pager
# Docker
docker logs <CONTAINER_ID> --tail 100
- Port conflicts and binding
# Check if the port is already used
lsof -i :<PORT> -sTCP:LISTEN || ss -lntp | grep :<PORT>
If the port is in use and you cannot stop the other service, set a new PORT in the environment and restart your API.
Failure Modes and Recovery
- EADDRINUSE: address already in use
- Signal: startup error mentioning EADDRINUSE.
- Fix: identify the conflicting process and pick a new port or stop the conflict.
lsof -i :<PORT>
# Safer recovery: change PORT
export PORT=<NEW_PORT>
pm2 restart <SERVICE_NAME>
- 503 on /ready but /health is 200
- Signal: readiness depends on a downstream (database, cache) that is not reachable.
- Fix: verify downstream connectivity separately, keep /health green.
# Example DB TCP reachability (replace host/port)
nc -zv <DB_HOST> <DB_PORT> || echo "DB not reachable"
- Recovery: gate traffic at the load balancer on /ready until downstream is restored.
- 413 Payload Too Large on JSON POST
- Signal: clients receive 413 for larger requests.
- Fix: raise the body size limit cautiously.
app.use(express.json({ limit: process.env.JSON_LIMIT || '5mb' }));
- Verify with a test payload; rollback by restoring the previous limit.
- Unhandled promise rejections causing crashes
- Signal: process exits intermittently, logs mention unhandledRejection.
- Fix: log and handle rejections; use a process manager to auto-restart.
process.on('unhandledRejection', (err) => {
console.error('unhandledRejection', err);
});
- Recovery: deploy the logging fix, monitor; if instability continues, rollback to the last known good build.
- Docker port not exposed
- Signal: container healthy, host port not reachable.
- Fix: correct port mapping.
# Wrong: container listens on 3000 but no mapping on host
# Right:
docker run -d --name <SERVICE_NAME> -p 3000:3000 <IMAGE_TAG>
- Verify with curl to the host port; rollback by stopping the incorrect container and recreating with proper mapping.
Operations Checklist
Use this when proposing or executing a change to an Express API.
Before you change anything:
- Confirm component and versions (Node, npm, Express, app version).
- Capture current health: status codes and latency for /health and key routes.
- Gather logs for the last 100 lines and note the timestamp.
- Identify deployment topology (local, PM2, systemd, Docker) and access method.
- Define success criteria (e.g., 200 on /health, p95 < 150 ms) and failure signals.
- Define a rollback (git revert, previous image tag, env var reset).
During the change:
- Touch one scoped item: a single env var, a single middleware, or a single port mapping.
- Add comments or commit messages with the change ID and timestamp.
- Avoid editing multiple unrelated files.
After the change:
- Verify endpoints with curl, including timing output.
- Check logs for errors or warnings introduced by the change.
- Compare latency and error rate to pre-change baselines.
- Document the outcome and whether rollback was needed.
Example verification block you can paste into a runbook:
set -e
BASE=http://localhost:<PORT>
for path in /health /ready /version; do
echo "Checking $path";
curl -sS -o /dev/null -w "$path status=%{http_code} time=%{time_total}\n" "$BASE$path";
done
Conclusion
Express API operations are reliable when every step is version-scoped, observable, and reversible. Start with inventory and read-only checks, make the smallest change that solves a clear problem, verify with concrete signals (status codes, headers, and timing), and keep a real rollback ready. Use placeholders instead of secrets, limit the blast radius to one scoped item at a time, and document expected outcomes and recovery steps. With this playbook, you can diagnose issues faster, implement safer changes, and keep your Express API stable in development and production.