E-NO
Express API troubleshooting 14 Min Read

Express API troubleshooting with practical examples: practical implementation guide

calendar_today Published: 2026-08-03
update Last Updated: 2026-08-03
analytics SEO Efficiency: 100%
Technical guide illustration for Express API troubleshooting with practical examples: practical implementation guide.

Express is popular because it is small and flexible. That same flexibility means small misconfigurations or code paths can produce hard-to-explain behavior under load or after a seemingly harmless change. This guide gives you a practical path to quickly isolate issues, verify fixes, and recover safely.

It focuses on what you can observe and change with minimal risk: versions, environment variables, port usage, logs, JSON parsing, CORS, error handling, and typical data layer failures (for example, MongoDB). Each step includes commands or configuration examples only where they directly apply.

Constructed examples are clearly labeled as such. Replace sample ports, environment names, and paths with your own.

Intro

Troubleshooting works best when it is visible and scoped. Start by inventorying what you are actually running, then adopt a minimal, safe configuration to remove blind spots (logging, error handling, health endpoints). Verify the basics locally before you expand the investigation. Keep recovery steps reversible and easy to validate.

Version and Environment Inventory

Before changing anything, record exactly what is running. This avoids arguing with ghosts caused by mismatched versions or environment variables.

  1. Record runtime versions
  • Node.js version:
node -v
  • npm version:
npm -v
  • Local Express version used by the project:
npm list express

If the result shows multiple versions in a monorepo, record the one under the API service path.

  1. Capture OS and port usage
  • Current user and OS basics:
uname -a   # macOS/Linux
ver        # Windows Command Prompt
  • Port occupancy for a typical Express port (replace 3000):
lsof -i :3000 -P -n    # macOS/Linux
netstat -anp | grep :3000   # Linux alternative
netstat -ano | findstr :3000  # Windows

If another process is bound to the port, you will see its PID and can decide whether to stop it or use a different port for the API.

  1. Snapshot environment variables that affect the API

Typical variables include PORT, NODE_ENV, and database connection settings.

# macOS/Linux
printenv | grep -E '^(PORT|NODE_ENV|MONGODB_|DB_|JWT_|API_)'

# Windows PowerShell
Get-ChildItem Env: | Where-Object { $_.Name -match '^(PORT|NODE_ENV|MONGODB_|DB_|JWT_|API_)' }

Record the output in your incident notes. Small mismatches, like a missing PORT or a different NODE_ENV, explain many surprises.

  1. Inspect package.json and start command

Open package.json and note:

  • scripts.start and scripts.dev
  • engines (if present)
  • express, body-parser (if any), cors, logging libraries (morgan, pino, winston)

These determine how the service actually starts and what middleware is in play.

Safe Configuration Path

Adopt small, reversible changes that improve observability and safety without altering business logic.

  1. Minimal logging and error handling

Add request logging with morgan (development-friendly) and a centralized error handler. This example is constructed.

// app.js (constructed example)
const express = require('express');
const morgan = require('morgan');
const cors = require('cors');

const app = express();

// Parse JSON with a sane limit; surface syntax errors as 400
app.use(express.json({ limit: '1mb' }));
app.use((err, req, res, next) => {
  if (err && err.type === 'entity.parse.failed') {
    return res.status(400).json({ error: 'Invalid JSON body' });
  }
  return next(err);
});

// Request logging
app.use(morgan('combined'));

// CORS: start narrow and explicit
app.use(cors({ origin: ['https://your-frontend.example.com'], methods: ['GET','POST','PUT','DELETE','OPTIONS'], credentials: true }));

// Health and info endpoints
app.get('/healthz', (req, res) => res.status(200).json({ status: 'ok' }));
app.get('/info', (req, res) => res.json({ name: 'example-api', version: process.env.BUILD_SHA || 'dev' }));

// Example route
app.get('/api/v1/ping', (req, res) => res.json({ pong: true }));

// Centralized error handler
app.use((err, req, res, next) => {
  console.error('Unhandled error', { message: err.message, stack: err.stack });
  if (res.headersSent) return next(err);
  res.status(500).json({ error: 'Internal Server Error' });
});

module.exports = app;
  1. A clear server entry with timeouts

Set server-level timeouts to avoid stalls and log when the server starts. Adjust values to match your upstream time budgets.

// server.js (constructed example)
const http = require('http');
const app = require('./app');

const PORT = Number(process.env.PORT || 3000);
const server = http.createServer(app);

// Avoid requests hanging forever
server.requestTimeout = 60_000; // 60s
server.headersTimeout = 65_000; // 65s

server.listen(PORT, () => {
  console.log(`Server listening on port ${PORT} env=${process.env.NODE_ENV || 'development'}`);
});
  1. Graceful process-level handlers

Do not mask problems; log and exit on fatal signals so your process manager can restart.

// index.js (constructed example)
require('dotenv').config();
require('./server');

process.on('unhandledRejection', (reason) => {
  console.error('Unhandled Rejection', reason);
  process.exit(1);
});

process.on('uncaughtException', (err) => {
  console.error('Uncaught Exception', err);
  process.exit(1);
});
  1. Keep CORS explicit

Avoid wildcard origins during troubleshooting. If you must allow multiple origins, list them explicitly so you can see which one is failing.

  1. Health endpoints

Return simple JSON and HTTP 200 only when core dependencies are reachable. For example, if the database is down, return 503 with a clear body.

// health.js (constructed example)
app.get('/healthz', async (req, res) => {
  const checks = { db: 'down' };
  try {
    // Pseudocode: ping your DB here
    // await db.admin().command({ ping: 1 });
    checks.db = 'up';
  } catch (e) {
    return res.status(503).json({ status: 'degraded', checks });
  }
  res.json({ status: 'ok', checks });
});

Verification and Diagnostics

With safe defaults in place, prove the basics work and gather signals for later debugging.

  1. Start the service and confirm the port
node index.js
# Expected: Server listening on port 3000 env=development

If nothing prints, confirm you are running the expected file and that console.log has not been suppressed.

  1. Basic endpoint checks
  • Health:
curl -i http://localhost:3000/healthz
# Expected: HTTP/1.1 200 OK and a small JSON body
  • Info:
curl -i http://localhost:3000/info
# Expected: 200 with name and version fields
  • Sample route:
curl -i http://localhost:3000/api/v1/ping
# Expected: 200 with {"pong":true}
  1. JSON parsing verification
  • Valid JSON:
curl -i -X POST http://localhost:3000/api/v1/echo \
  -H 'Content-Type: application/json' \
  -d '{"hello":"world"}'
# Expected: 200 with echoed JSON (constructed example route)
  • Invalid JSON (constructed example):
curl -i -X POST http://localhost:3000/api/v1/echo \
  -H 'Content-Type: application/json' \
  -d '{"hello":"world"'   # missing closing brace
# Expected: 400 with {"error":"Invalid JSON body"}

If you see a stack trace instead of a 400, confirm your JSON parse error middleware runs before other handlers.

  1. CORS verification (from command line)
curl -I http://localhost:3000/api/v1/ping \
  -H 'Origin: https://your-frontend.example.com'
# Expected: Access-Control-Allow-Origin: https://your-frontend.example.com

If the header is missing, review the cors() configuration and confirm the route is under app.use(cors(...)) or that you apply CORS at the router level.

  1. Port and process checks

If binding fails with EADDRINUSE, find the process using the port and decide whether to terminate it or change PORT.

lsof -i :3000 -P -n  # macOS/Linux
netstat -ano | findstr :3000  # Windows
  1. Database connectivity check

Surface connectivity in /healthz. For a manual check (constructed example with a Mongo-like command):

# Hypothetical shell check, replace with your DB tool
mongo --eval 'db.adminCommand({ ping: 1 })'
# Expected: ok: 1

If your Express logs show ECONNREFUSED or ETIMEDOUT to the database, verify host, port, credentials, and any network ACLs.

Failure Modes and Recovery

The table below maps common Express API symptoms to likely causes and safe first actions.

Symptom or log snippetLikely causeFirst checksSafe fix
EADDRINUSE :3000 on startupPort already in uselsof/netstat for port 3000Change PORT or stop conflicting process
ECONNREFUSED to DB hostDatabase down or wrong host/portPing DB host and portCorrect config or restart DB; return 503 in healthz
Unexpected token in JSON at position XMalformed client JSONInspect request body and Content-TypeCatch parse error, return 400
Cannot set headers after they are sentDouble response sendSearch for multiple res.send/res.json/res.endEnsure one code path responds; add return statements
socket hang up or ETIMEDOUTUpstream or server timeoutCheck server.requestTimeout; latency in logsTune timeouts; improve handler performance
404 for known routeWrong mount path or prefixConfirm app.use('/api', router) vs requestsAlign route and client path
CORS error in browser consoleOrigin not allowedInspect Access-Control-Allow-OriginAdd explicit origin or adjust CORS config

Scenario A: Port conflict (EADDRINUSE)

  1. Confirm the conflict
lsof -i :3000 -P -n
  1. Containment
  • If the other process is yours (for example, an old dev server), stop it using its normal stop command.
  • If it is not yours, do not kill it blindly. Change your API PORT to a free one and continue troubleshooting.
  1. Safe fix
  • Set PORT to a free value and restart:
PORT=3001 node index.js
  1. Verification
  • Access http://localhost:3001/healthz and confirm 200.
  1. Rollback
  • Revert PORT to the original once the conflicting process is no longer using it.

Scenario B: Crashes on unhandled promise rejections

Symptoms: process exits unexpectedly, logs show UnhandledPromiseRejectionWarning or similar.

  1. Containment
  • Add a top-level unhandledRejection handler that logs and exits (shown earlier). Exiting avoids a corrupted process continuing to serve traffic.
  1. Root cause
  • Find async handlers that throw without passing errors to next(err) or without try/catch.

Constructed example fix:

// Before
app.get('/api/v1/user', async (req, res) => {
  const user = await loadUser(req.query.id); // throws -> crash path
  res.json(user);
});

// After
app.get('/api/v1/user', async (req, res, next) => {
  try {
    const user = await loadUser(req.query.id);
    res.json(user);
  } catch (err) {
    next(err);
  }
});
  1. Verification
  • Reproduce the failing call and expect a 500 with the centralized error response, not a crash.
  1. Rollback
  • If a recent change introduced the issue, revert that route handler file to the last known good commit.

Scenario C: JSON parse failures flooding logs

Symptoms: 500s and stack traces for invalid JSON bodies.

  1. Containment
  • Ensure express.json middleware is installed before routes and include the parse error handler from Safe Configuration Path.
  1. Root cause
  • Clients sending malformed JSON or wrong Content-Type.
  1. Safe fix
  • Return 400 for parse errors and include a short message.
  • Optionally tighten Content-Type check:
app.use((req, res, next) => {
  if (['POST','PUT','PATCH'].includes(req.method)) {
    if (!req.is('application/json')) {
      return res.status(415).json({ error: 'Expected application/json' });
    }
  }
  next();
});
  1. Verification
  • Send valid and invalid JSON requests. Expect 200 for valid, 400 or 415 for invalid.
  1. Rollback
  • If clients depend on looser behavior, temporarily remove the Content-Type check but keep the parse error handler.

Scenario D: Database outage or misconfiguration

Symptoms: 500 errors on DB-backed routes; health endpoint shows degraded.

  1. Containment
  • Short-circuit requests when the DB is down to avoid long timeouts. Return 503 with Retry-After.

Constructed example guard:

let dbIsUp = false;

// Update dbIsUp in a small interval or connection events
setInterval(async () => {
  try {
    // await db.admin().command({ ping: 1 });
    dbIsUp = true;
  } catch {
    dbIsUp = false;
  }
}, 5000);

app.use((req, res, next) => {
  if (!dbIsUp && req.path.startsWith('/api/v1/orders')) {
    res.set('Retry-After', '30');
    return res.status(503).json({ error: 'DB unavailable' });
  }
  next();
});
  1. Root cause
  • Wrong host/port, credentials rotated, firewall rules, or the database process crashed.
  1. Safe fix
  • Correct the connection string and credentials.
  • Add a connection timeout and a limited retry policy so your app fails fast instead of hanging:
// Pseudocode: when creating DB client
const client = new DBClient({ connectTimeoutMS: 5000 });
  1. Verification
  • healthz should switch from 503 to 200 within one or two intervals after the DB recovers.
  • Business routes should respond within normal latency budgets.
  1. Rollback
  • If a schema or driver upgrade caused the issue, revert the package version and configuration to last known good.

Scenario E: High memory or CPU usage over time

Symptoms: latency increases, process restarts, or out-of-memory errors.

  1. Containment
  • Reduce traffic if possible, or temporarily scale out using multiple instances behind a load balancer where available. If not, restart the process during a maintenance window to restore service while you investigate.
  1. Root cause
  • Leaks from global arrays, setInterval without clearInterval, or repeatedly attaching event listeners.

Constructed example of a leak and fix:

// Leaky: pushes into a global array on every request
const cache = [];
app.get('/api/v1/items', (req, res) => {
  cache.push(Date.now());
  res.json({ ok: true });
});

// Fixed: use a bounded cache
const cache2 = [];
app.get('/api/v1/items', (req, res) => {
  if (cache2.length > 1000) cache2.shift();
  cache2.push(Date.now());
  res.json({ ok: true });
});
  1. Safe diagnostics
  • Log memory every minute during investigation:
setInterval(() => {
  const m = process.memoryUsage();
  console.log(`rss=${m.rss} heapUsed=${m.heapUsed}`);
}, 60_000);
  1. Verification
  • After the fix, the heapUsed should stabilize during a steady load period.
  1. Rollback
  • Revert the feature suspected to introduce the leak; confirm memory returns to baseline values (hypothetical example: rss ~ 120MB under steady load).

Operations Checklist

Use this checklist during development and incidents. Keep it short and objective.

TaskWhenHow to verify
Record Node, npm, Express versionsStart of work or incidentnode -v, npm -v, npm list express
Confirm port bindingOn startup or after EADDRINUSElsof/netstat shows single listener
Verify health and info endpointsAfter each deploy or fix/healthz returns 200 when dependencies are up
Check JSON error handlingAfter parser config changesInvalid JSON returns 400, not 500
Validate CORSWhen front-end origin changesResponse has correct Access-Control-Allow-Origin
Observe timeoutsAfter adding slow logicLong requests end within requestTimeout
Test DB connectivityBefore enabling DB-heavy routeshealthz reports db: up
Monitor memory and CPUDuring investigationrss and heapUsed stable over time

Expected Results and What Good Looks Like

  • Startup prints one clear line with port and environment, and the process remains stable under light traffic.
  • /healthz returns 200 only when core dependencies are up; otherwise, 503 with a simple explanation.
  • Invalid JSON bodies yield 400 with a concise message; valid bodies succeed.
  • CORS responses include the exact allowed origin for relevant routes.
  • Timeouts are finite, visible in logs, and aligned with your upstream limits.

Failure Verification and Recovery Workflow

Use this repeatable sequence:

  1. Identify
  • Capture the exact error message and timestamp from logs.
  • Note the route, method, and any correlation or request ID.
  1. Contain
  • Prefer configuration toggles and guard clauses (return 503) over code rewrites while you learn more.
  • If a single route is failing, isolate it behind a feature flag or route-level guard.
  1. Diagnose
  • Reproduce locally with curl using the same method, headers, and body.
  • Compare behavior across environments by checking NODE_ENV and PORT differences.
  1. Fix
  • Apply the smallest change that addresses the root cause (for example, add try/catch, adjust CORS origin, correct DB host).
  1. Verify
  • Confirm with both a success case and a representative failure case.
  • Watch logs for 5-10 minutes to ensure no recurring errors (hypothetical observation window).
  1. Roll back if needed
  • If the change increases error rates, revert to the last known good version of the specific file or configuration.

Practical Tips

  • Keep error messages brief for clients but log full details server-side.
  • Prefer explicit lists (CORS origins, routes) over wildcards while investigating.
  • Set conservative JSON limits and body sizes, then relax as needed with evidence.
  • When in doubt about async errors, wrap handlers and always call next(err) on failure.
  • Encode time budgets: requestTimeout and upstream client timeouts should be consistent.

Conclusion

Solid Express troubleshooting starts with visibility: know your versions, ports, and environment variables, and add minimal logging and error handling. Verify basic routes, JSON parsing, CORS, and health checks before diving into deeper changes. Common failures like port conflicts, unhandled rejections, parse errors, and database outages all have safe first steps and clear verification signals.

Start with a narrow, measurable pilot locally, then expand your checks. Use the checklist to keep each incident focused and repeatable. With these practices in place, you will spend less time guessing and more time delivering a stable API.

Article Quality Score

Reader usefulness 100%
  • check_circle Reader-ready guide
  • check_circle Practical examples included
  • check_circle Clean SEO article URL