## 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 () 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 - 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.