← Backend development with Node.js and REST APIs

Lesson 5 of 5

Testing, deployment and scaling an API

2 min read

Testing pyramid

  • Unit tests: pure functions and services with the DB mocked; fast and many.
  • Integration tests: real routes against a real test database (Supertest + Jest/Vitest + a Docker Postgres). These catch the bugs that matter.
  • End-to-end tests: a few critical journeys through the whole system.
js
test("student cannot apply twice", async () => {
  await request(app).post("/api/jobs/1/applications").set(auth(student)).send(body).expect(201);
  await request(app).post("/api/jobs/1/applications").set(auth(student)).send(body).expect(409);
});

Deployment basics

  • Package with Docker: a small image, non-root user, NODE_ENV=production, only production dependencies.
  • CI/CD: on every push run lint, tests and build; deploy on merge to main.
  • Run database migrations as a separate, versioned step; make them backward-compatible (add a column, deploy code, then remove the old column later) so you can deploy without downtime.
  • Put a reverse proxy (Nginx, Caddy) or load balancer in front for TLS and compression.
  • Health checks (/health) so the platform restarts unhealthy instances.

Observability

  • Logs: structured, with a request id passed through every service.
  • Metrics: request rate, error rate, latency percentiles (p50, p95, p99), DB pool usage.
  • Tracing: follow one request across services (OpenTelemetry).
  • Alerts on symptoms users feel (error rate, latency), not on every CPU spike.

Scaling step by step

  1. Optimise the obvious: indexes, N+1 queries, payload size, compression.
  2. Cache hot reads (Redis, CDN).
  3. Scale horizontally: run more stateless app instances behind a load balancer (keep sessions in Redis or use tokens).
  4. Move slow work to queues and workers.
  5. Scale the database: read replicas for reads, connection pooling (PgBouncer), then partitioning or sharding only when truly needed.
  6. Split into services only when team size or very different scaling needs demand it; a well-structured monolith goes a long way.

Monolith vs microservices

MonolithMicroservices
Deployone unitmany independent units
Complexitylowhigh: network calls, distributed data, tracing
Team fitsmall/medium teamsmany teams owning separate domains
Failureone bug can take down everythingisolation, but partial failures to handle

Good interview answer: "Start with a modular monolith; extract a service when there's a clear boundary and a reason (independent scaling, separate team, different tech needs)."

Check yourself

  • What's the difference between a unit and an integration test for an API?
  • How do you deploy a schema change without downtime?
  • What would you do first if an endpoint's p95 latency doubled?

Sign up free to save your progress.