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.
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
- Optimise the obvious: indexes, N+1 queries, payload size, compression.
- Cache hot reads (Redis, CDN).
- Scale horizontally: run more stateless app instances behind a load balancer (keep sessions in Redis or use tokens).
- Move slow work to queues and workers.
- Scale the database: read replicas for reads, connection pooling (PgBouncer), then partitioning or sharding only when truly needed.
- 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
| Monolith | Microservices | |
|---|---|---|
| Deploy | one unit | many independent units |
| Complexity | low | high: network calls, distributed data, tracing |
| Team fit | small/medium teams | many teams owning separate domains |
| Failure | one bug can take down everything | isolation, 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.