Skip to content

Backend Engineer

Designs, builds and runs the APIs, data and services behind every product

A Node.js and TypeScript first path from HTTP fundamentals to running distributed services in production, with concepts that carry over to any language. It is for frontend developers crossing into backend and junior backend engineers who want depth in API design, PostgreSQL, auth, async work, testing and operations.

Start at stage 01
stages
10stages
topics
52topics
weeks, part-time
~37weeks, part-time
  • Foundation
  • Core
  • Advanced
  • Node.jsNode.js
  • TypeScriptTypeScript
  • NestJSNestJS
  • PostgreSQLPostgreSQL
  • PrismaPrisma
  • RedisRedis
  • OpenTelemetry
  • DockerDocker

My take

I am a frontend developer, not a dedicated backend engineer, but over the past few years I have built backend services with NestJS, Prisma and PostgreSQL at a working level. Crossing that line changed how I write frontend code too: I stopped treating the API as a black box. This map is the path I pieced together from that experience, the gaps I hit, and the order I would follow if I started backend seriously today. Where I lack production depth, especially in scaling and distributed systems, I lean on official docs and well-known books rather than claim experience I do not have.

What I wish I had known earlier

  • The database is the product. Time spent on modeling, indexes and transactions paid off far more than time spent learning another framework.
  • Authentication is mostly a solved problem; authorization is where the real bugs are, so check ownership on every object you return.
  • Every network call can fail, time out or be retried, so design for idempotency from the first endpoint, not after the first duplicate payment.
  • Mocks made my tests green and my queries wrong. Testing against a real PostgreSQL in a container was the turning point for trusting my code.
0 of 10 stages passed

Foundation01–03

  1. 01Stage 01, 2 weeksHTTP and networking fundamentalsExplain what happens on the wire from DNS lookup to response, and debug a request with curl and the browser network panel.
    On the frontend I treated fetch as a black box that returned JSON or threw. Crossing into backend, most of my early bugs were really HTTP bugs: wrong status codes, missing cache headers, CORS misconfigured on the server instead of the client.

    What to learn

    Request path end to end
    DNS resolution, TCP handshake, TLS 1.3 handshake, then the HTTP exchange. Know where latency comes from (round trips, connection reuse via keep-alive) and why a reverse proxy or load balancer terminating TLS changes what your app sees, such as X-Forwarded-For and the real client IP.
    HTTP semantics
    Methods and their safety and idempotency guarantees (GET, PUT and DELETE are idempotent, POST is not), status code classes, and the difference between 401 and 403 or 400 and 422. RFC 9110 is the current single source for semantics, separate from the wire versions.
    HTTP/1.1, HTTP/2 and HTTP/3
    HTTP/2 multiplexes streams over one TCP connection, HTTP/3 runs over QUIC on UDP and removes TCP head-of-line blocking. In practice your Node service often speaks HTTP/1.1 behind a proxy or CDN that speaks HTTP/2 or HTTP/3 to clients.
    Caching and conditional requests
    Cache-Control (max-age, s-maxage, no-store, private), ETag with If-None-Match, and Vary. Getting these right on API responses lets CDNs and browsers absorb load before it ever reaches your database.
    CORS, cookies and headers that matter
    CORS is enforced by the browser, so the server only declares Access-Control-Allow-* policies; preflight OPTIONS requests happen for non-simple requests. Cookie attributes HttpOnly, Secure, SameSite and the __Host- prefix decide most of your session security.

    Do

    • Reproduce every bug with a raw curl -v command before touching code.
    • Pick status codes deliberately and keep a short table of them in the project README.
    • Set explicit Cache-Control on every API response, even if it is no-store.
    • Trust forwarded headers only from proxies you control and configure that trust explicitly.

    Watch out for

    • Returning 200 with an error field in the body, which breaks caches, retries and monitoring.
    • Fixing CORS by allowing every origin with credentials instead of listing real origins.

    Checkpoint project: Hand-made HTTP inspector

    Write a tiny server with node:http only (no framework) that echoes method, headers, and body, supports ETag with 304 responses, and sets proper CORS for one allowed origin. Done when curl -v shows correct 200/304/405 responses and a browser preflight from another origin succeeds only for the allowed one.

    Quick check

    0/4 answered · 0 correct

    Get at least 3 of 4 right to mark this stage as passed.

    1. Question 1 of 4StarterA client times out and retries automatically. For which request is a blind retry unsafe without extra protection?
    2. Question 2 of 4StarterA properly logged-in user calls an admin-only endpoint. Which status code fits?
    3. Question 3 of 4StarterA frontend at https://app.example.com calls the API with cookies. The server returns Access-Control-Allow-Origin: * with Access-Control-Allow-Credentials: true, but the browser still blocks it. What is the right fix?
    4. Question 4 of 4StarterThe app runs behind a load balancer and logs show every request coming from 10.0.0.5. What is the right fix?
  2. 02Stage 02, 3 weeksNode.js runtime in depthReason about the event loop, streams and worker threads well enough to keep a service responsive under load.
    I knew async/await from React code, but I did not really understand what blocks the event loop until a CPU-heavy loop froze every request on a side project. Learning the runtime is what turned Node from magic into a tool.

    What to learn

    Event loop phases
    Timers, pending callbacks, poll, check (setImmediate) and close phases, plus the microtask queue (promises, queueMicrotask) and process.nextTick that run between them. Understand why one synchronous JSON.parse on a huge payload stalls every concurrent request, and measure it with event loop delay from perf_hooks.
    Streams and backpressure
    Readable, Writable, Transform and Duplex streams, stream.pipeline and async iteration with for await. Backpressure (write() returning false, the drain event) is what keeps a file upload or CSV export from loading everything into memory.
    Worker threads and the libuv pool
    worker_threads for CPU-bound work like image processing or hashing, versus the libuv thread pool that already handles fs, dns.lookup and crypto. Clustering or running several processes behind a load balancer is still the main way to use multiple cores for request handling.
    Modern Node features
    Node 24 is the active LTS line and native TypeScript type stripping became stable in 24.12, but it only removes erasable syntax: enums, parameter properties and decorator metadata are not supported, which matters for NestJS. Built-in fetch, node:test, --watch and --env-file reduce the number of dev dependencies you need.
    Bun and Deno, briefly
    Bun offers a fast all-in-one runtime, package manager and test runner with high Node compatibility; Deno focuses on secure-by-default permissions and web standard APIs with npm support. Learn Node first because production ecosystems, docs and hosting still assume it, then evaluate the others per project.

    Do

    • Use stream.pipeline instead of manual pipe chains so errors and cleanup propagate.
    • Monitor event loop delay and heap usage from day one.
    • Move CPU-heavy work to worker threads or a separate queue worker, never inline in a request handler.
    • Handle unhandledRejection and SIGTERM explicitly and shut down gracefully.

    Watch out for

    • Reading an entire upload into a Buffer when a stream would use constant memory.
    • Assuming async code is parallel CPU work; it only overlaps I/O waiting.

    Checkpoint project: Streaming CSV exporter

    Build an endpoint that streams one million generated rows as CSV and a second endpoint that computes a password hash in a worker thread. Done when memory stays flat during the export, event loop delay stays low while hashes run, and the server shuts down cleanly on SIGTERM without dropping in-flight requests.

    Quick check

    0/4 answered · 0 correct

    Get at least 3 of 4 right to mark this stage as passed.

    1. Question 1 of 4StarterWhy does one JSON.parse on a 200MB payload slow down every concurrent request?
    2. Question 2 of 4StarterA handler runs await Promise.all(images.map((img) => resizeInJs(img))), where resizeInJs is async but pure JavaScript computation. What happens?
    3. Question 3 of 4StarterAn endpoint must return a 2GB CSV built from the database. What is the right approach?
    4. Question 4 of 4StarterA team wants to run a NestJS app with node src/main.ts using Node's built-in type stripping. What breaks?
  3. 03Stage 03, 3 weeksAPI design that lastsDesign a consistent, documented API with clear errors, safe retries, stable pagination and a versioning policy.
    As a frontend dev I was the API consumer who suffered from inconsistent shapes and silent breaking changes. That pain is the best teacher: design the API you wish you had been given.

    What to learn

    Resource modeling and REST conventions
    Nouns for resources, sub-resources for ownership, consistent naming and casing, and filtering, sorting and sparse fields through query parameters. Use RFC 9457 Problem Details (application/problem+json) as the single error format so clients can handle errors generically.
    OpenAPI as the contract
    OpenAPI 3.1 aligned schemas with JSON Schema, and 3.2 (released in 2025) adds streaming media types like SSE and JSON Lines, hierarchical tags and the QUERY method. Choose code-first (generate the spec from decorators or Zod schemas) or design-first, but always generate typed clients and lint the spec in CI.
    Idempotency and safe retries
    Clients retry on timeouts, so POST endpoints that create payments or orders need an Idempotency-Key header (an IETF draft that mirrors what Stripe popularized). Store the key with the request hash and response, return the stored result on replay, and reject the same key with a different body.
    Pagination
    Offset pagination is simple but slow and unstable on large or changing tables. Cursor or keyset pagination (WHERE (created_at, id) < ($1, $2) ORDER BY created_at DESC, id DESC) is stable and uses an index; return an opaque cursor and a next link.
    Versioning and evolution
    Prefer additive, backward-compatible changes; when breaking is unavoidable, version via URL path or a header and announce removal with the Deprecation and Sunset headers. Contract tests and a changelog make deprecation a process, not a surprise.
    GraphQL and tRPC trade-offs
    GraphQL fits many clients with different data needs but brings N+1 risk (use DataLoader), query cost limits and harder HTTP caching. tRPC gives end-to-end types with almost no ceremony inside a TypeScript monorepo, but it is not a public API contract; REST plus OpenAPI remains the safe default for external consumers.

    Do

    • Write the OpenAPI spec or schemas before the implementation and review them like code.
    • Return one error format everywhere, with a stable machine-readable type and a human-readable detail.
    • Require idempotency keys on every non-idempotent endpoint that moves money or creates important records.
    • Default to cursor pagination with a maximum page size enforced on the server.

    Watch out for

    • Leaking database column names and internal IDs straight into the public contract.
    • Shipping a breaking change under the same version because only one known client uses it.

    Checkpoint project: Orders API contract

    Design an OpenAPI spec for an orders service with list (cursor pagination), create (idempotency key), and Problem Details errors, then generate a typed TypeScript client from it. Done when the spec passes a linter, the client compiles against it, and replaying a create with the same key returns the original order.

    Quick check

    0/4 answered · 0 correct

    Get at least 3 of 4 right to mark this stage as passed.

    1. Question 1 of 4StarterWhy is keyset pagination better than offset on a large, constantly changing table?
    2. Question 2 of 4StarterWhy is returning 200 with { "error": "not found" } in the body a bad design?
    3. Question 3 of 4StarterA client sends POST /payments with Idempotency-Key: k1 and amount 100, then resends key k1 with amount 500. What should the server do?
    4. Question 4 of 4StarterYou need to rename the user_name field to username in a public API. What is the right approach?

Core04–08

  1. 04Stage 04, 3 weeksNestJS architectureStructure a NestJS service into cohesive modules with clear request lifecycle, validation and testable providers.
    NestJS was my bridge from Angular-style frontend thinking into backend, and it is where I have the most hands-on time. What I got wrong early was putting business logic into controllers and creating one giant shared module.

    What to learn

    Modules and boundaries
    Organize by feature or domain (orders, billing, users), export only what other modules need, and avoid circular imports instead of hiding them with forwardRef. Dynamic modules (forRoot, forRootAsync, ConfigurableModuleBuilder) are how you build reusable infrastructure like database or mail modules.
    Dependency injection
    Providers are singletons by default; request and transient scopes cascade and cost performance, so prefer AsyncLocalStorage (for example via nestjs-cls) for per-request context. Custom providers with useFactory, useClass and injection tokens let you swap implementations in tests.
    Request lifecycle
    Middleware, then guards (authorization decisions), interceptors before, pipes (validation and transformation), the handler, interceptors after, and exception filters. Knowing this order tells you where each concern belongs: auth in guards, logging and timing in interceptors, error mapping in filters.
    Validation and DTOs
    The global ValidationPipe with class-validator and class-transformer, whitelist and forbidNonWhitelisted to drop unknown fields, or Zod schemas through a custom pipe if you want one schema for runtime validation and types. Validate at the edge and keep domain services working with already-trusted types.
    NestJS 11 in practice
    NestJS 11 requires Node 20 or newer and uses Express 5 by default, with Fastify as an alternative adapter for higher throughput. Because Nest relies on emitted decorator metadata, compile with tsc or SWC rather than relying on Node's built-in type stripping.

    Do

    • Keep controllers thin: map HTTP to a service call and back, nothing else.
    • Enable a global ValidationPipe with whitelist and transform from the first commit.
    • Load and validate configuration at startup and fail fast on missing variables.
    • Use injection tokens and interfaces for external services so tests can provide fakes.

    Watch out for

    • Making providers request-scoped just to read the current user, which slows every request in the chain.
    • Catching errors in every service and returning null instead of throwing domain errors mapped by a filter.

    Checkpoint project: Modular NestJS service

    Implement the orders API from the previous stage in NestJS with separate orders, catalog and health modules, a global validation pipe, a logging interceptor, and an exception filter that emits Problem Details. Done when invalid input returns 400 with field errors, unknown fields are rejected, and each module can be unit tested with mocked providers.

    Quick check

    0/4 answered · 0 correct

    Get at least 3 of 4 right to mark this stage as passed.

    1. Question 1 of 4IntermediateA NestJS route has a middleware, a guard, an interceptor and a ValidationPipe. In what order do they run before the handler?
    2. Question 2 of 4IntermediateWhy should you not make a provider request-scoped just to read the current user?
    3. Question 3 of 4IntermediateCreateUserDto only has email and name. An attacker adds "isAdmin": true. Which ValidationPipe config returns a 400 instead of silently dropping the field?
    4. Question 4 of 4IntermediateOrderService catches every error and returns null, and the controller returns 200 with an empty body when an order is missing. What is the right fix?
  2. 05Stage 05, 6 weeksPostgreSQL and data modelingModel data in PostgreSQL, write and index efficient queries, handle transactions correctly and ship safe migrations through an ORM.
    Prisma let me ship features before I understood what SQL it generated, and that bit me later with slow queries and N+1 patterns. This is the longest stage on purpose: the database outlives every framework you put in front of it.

    What to learn

    Modeling and constraints
    Normalize first, then denormalize deliberately; enforce invariants with NOT NULL, CHECK, UNIQUE and foreign keys rather than only in application code. Use timestamptz, numeric for money, and consider uuidv7(), built into PostgreSQL 18, for time-ordered primary keys that index better than random UUIDs.
    Indexes and query plans
    B-tree for equality and ranges, composite index column order, partial and expression indexes, GIN for jsonb and full text. Read EXPLAIN (ANALYZE, BUFFERS) output to spot sequential scans, bad row estimates and sorts that spill to disk.
    Transactions and isolation
    Read Committed is the default and allows non-repeatable reads; Repeatable Read gives snapshot isolation; Serializable prevents anomalies like write skew but requires retrying serialization failures. Use SELECT ... FOR UPDATE or optimistic concurrency with a version column for counters and inventory.
    Migrations without downtime
    Use expand and contract: add nullable columns, backfill in batches, then add constraints with NOT VALID and VALIDATE later, and create indexes CONCURRENTLY. Never rename or drop a column in the same release that stops using it.
    ORMs: Prisma and Drizzle
    Prisma 7 replaced the Rust query engine with a TypeScript one and now uses driver adapters such as @prisma/adapter-pg, which makes it lighter and edge-friendly. Drizzle stays close to SQL with schema-in-TypeScript and no codegen; either way, know when to drop to raw SQL with typed parameters.
    Connections and pooling
    Each PostgreSQL connection is a process, so pool in the app and use PgBouncer or a managed pooler in transaction mode for serverless or many replicas. Set statement_timeout and idle_in_transaction_session_timeout so a bug cannot hold locks forever.

    Do

    • Put business invariants in database constraints, not only in validation code.
    • Run EXPLAIN ANALYZE on every new query that touches a large table before merging.
    • Keep transactions short and never call external HTTP services inside one.
    • Review generated migration SQL and test it against a copy of production-sized data.

    Watch out for

    • Loading relations in a loop through the ORM and creating N+1 queries.
    • Adding an index or NOT NULL constraint on a busy table in a way that locks writes for minutes.
    • Relying on application-level checks for uniqueness, which race under concurrent requests.

    Checkpoint project: Inventory with concurrency

    Model products, orders and stock in PostgreSQL with Prisma or Drizzle, and implement order placement that never oversells under 100 concurrent requests. Done when a load test shows zero negative stock, the hot queries use index scans in EXPLAIN, and one schema change is shipped with an expand and contract migration.

    Quick check

    0/4 answered · 0 correct

    Get at least 3 of 4 right to mark this stage as passed.

    1. Question 1 of 4IntermediateThe code runs a SELECT to check whether an email exists, then an INSERT. Why can duplicate emails still appear?
    2. Question 2 of 4IntermediateWhen using the Serializable isolation level in PostgreSQL, what must the application do?
    3. Question 3 of 4IntermediateThe query WHERE tenant_id = $1 AND created_at > $2 ORDER BY created_at is slow. Which index fits best?
    4. Question 4 of 4IntermediateYou must add a required status column (derived from existing data) to a 50 million row table under constant writes. Which approach avoids long write locks?
  3. 06Stage 06, 4 weeksAuthentication and authorizationImplement secure login with sessions or OIDC, support passkeys, and enforce fine-grained authorization on every resource.
    From the frontend side, auth was storing a token and attaching a header. Owning it on the backend taught me that authentication is mostly solved by standards and libraries, while authorization is where real bugs live.

    What to learn

    Sessions versus tokens
    Server-side sessions in a store like Redis with an HttpOnly, Secure, SameSite cookie are simple and revocable, and fit first-party web apps well. JWT access tokens suit service-to-service and third-party APIs, but keep them short-lived, validate iss, aud, exp and alg, and do not store them in localStorage.
    OAuth 2.1 and OpenID Connect
    OAuth 2.1 is still an IETF draft but consolidates current practice: authorization code with PKCE for every client, no implicit or password grants, exact redirect URI matching. RFC 9700 is the published security best practice; OIDC adds the ID token and discovery so you can delegate login to an identity provider.
    Passkeys and WebAuthn
    Passkeys are phishing-resistant public key credentials synced by platform providers; the server stores the public key and verifies signed challenges. Libraries such as SimpleWebAuthn handle registration and assertion; plan account recovery and keep a fallback factor.
    RBAC, ABAC and relationship-based access
    RBAC maps roles to permissions and is enough for many apps; ABAC adds attributes like tenant, owner or region; ReBAC (Zanzibar-style, as in OpenFGA or SpiceDB) models sharing and hierarchies. Policy engines like Cedar or OPA let you keep rules out of controller code and test them separately.
    Object-level checks and multi-tenancy
    Every query must scope by the caller's tenant or ownership, not just the route guard. PostgreSQL row-level security can enforce tenant isolation as a second line of defense.

    Do

    • Use a vetted library or identity provider for login flows instead of hand-rolling crypto.
    • Check authorization on every object access, close to the data, and deny by default.
    • Hash passwords with Argon2id or bcrypt and rate limit login and reset endpoints.
    • Rotate refresh tokens and detect reuse to catch stolen tokens.

    Watch out for

    • Trusting a user or tenant ID from the request body instead of the authenticated principal.
    • Issuing long-lived JWTs with no way to revoke them when a user logs out or is disabled.

    Checkpoint project: Multi-tenant auth

    Add OIDC login through a hosted or self-hosted provider, passkey registration as a second method, and role plus ownership-based authorization to the orders service. Done when automated tests prove that a user from tenant A gets 404 or 403 on every tenant B resource and that revoked sessions stop working immediately.

    Quick check

    0/4 answered · 0 correct

    Get at least 3 of 4 right to mark this stage as passed.

    1. Question 1 of 4IntermediateWhy are long-lived JWTs (say 30 days) a problem when a user logs out or is disabled?
    2. Question 2 of 4IntermediateWhat does PKCE protect the authorization code flow against?
    3. Question 3 of 4IntermediateThe GET /invoices/:id handler runs after an auth guard and calls invoiceRepo.findById(id). What is wrong and how do you fix it?
    4. Question 4 of 4IntermediateWhat is the right way to store user passwords?
  4. 07Stage 07, 3 weeksCaching, queues and async workUse Redis for caching and rate state, move slow work to reliable background jobs, and publish events without losing data.
    I used to think a cache was a free speedup. The hard part, which I learned slowly, is invalidation and what happens when the cache or the queue is down.

    What to learn

    Caching patterns
    Cache-aside with TTLs is the default; add jitter to TTLs and request coalescing (single-flight) to avoid stampedes when a hot key expires. Cache only what you can afford to be stale and design keys with a version prefix so you can invalidate a whole shape at once.
    Redis and Valkey
    Beyond strings: hashes, sorted sets for leaderboards and sliding windows, streams, and Lua or functions for atomic multi-step operations. Valkey is the Linux Foundation fork created after Redis changed its license, and managed clouds now offer both, so check which one your platform runs.
    Background jobs with BullMQ
    BullMQ gives Redis-backed queues with retries, exponential backoff, delayed and repeatable jobs, rate limits and flows (parent and child jobs). NestJS integrates via @nestjs/bullmq; make every job handler idempotent because at-least-once delivery means duplicates will happen.
    Transactional outbox
    Writing to the database and publishing to a broker are two systems, so one can fail after the other succeeds. Write the event to an outbox table in the same transaction, then relay it with a poller or CDC (Debezium, logical replication) to the queue or broker.
    When to use a real broker
    Redis queues fit job processing inside one system; Kafka, Redpanda, NATS JetStream or RabbitMQ fit durable event streams, fan-out to many consumers and replay. Pick based on ordering, retention and consumer group needs, not hype.

    Do

    • Make every job and event consumer idempotent using a processed-message key or natural unique constraint.
    • Send failed jobs to a dead-letter queue with enough context to replay them.
    • Set TTLs on every cache entry and measure hit ratio before adding more caching.
    • Keep the system correct when Redis is unavailable, even if it becomes slower.

    Watch out for

    • Publishing an event right after a commit without an outbox and silently losing it on crash.
    • Using the cache as the source of truth for data that is not stored anywhere else.

    Checkpoint project: Order pipeline

    When an order is placed, write an outbox row in the same transaction, relay it to a BullMQ queue, and have workers send a confirmation email and update a cached order summary. Done when killing the API or a worker mid-flow still results in exactly one email per order and the cache is refreshed within a set time.

    Quick check

    0/4 answered · 0 correct

    Get at least 3 of 4 right to mark this stage as passed.

    1. Question 1 of 4IntermediateA hot cache key expires and 1,000 requests hit the database at once. What reduces this?
    2. Question 2 of 4IntermediateWhy use a transactional outbox instead of publishing the event right after the commit?
    3. Question 3 of 4IntermediateA BullMQ job that sends a welcome email sometimes runs twice. What is the right fix?
    4. Question 4 of 4IntermediateRedis, used as a cache, goes down. How should the service behave?
  5. 08Stage 08, 3 weeksTesting backend servicesBuild a fast, trustworthy test suite with unit, integration against real dependencies, and contract tests.
    On the frontend I leaned on component tests and mocks. On the backend, mocking the database gave me green tests and broken production queries, so integration tests against real PostgreSQL became the layer I trust most.

    What to learn

    Test pyramid for services
    Unit tests for pure domain logic, integration tests for repositories, controllers and queue handlers, and a small number of end-to-end flows. Many teams now favor a testing trophy shape for services, where integration tests carry most of the confidence.
    Integration tests with Testcontainers
    Testcontainers starts real PostgreSQL, Redis or Kafka in Docker per test run, so you test real SQL, constraints and migrations. Run migrations once, isolate tests with transactions or truncation, and reuse containers locally to keep the loop fast.
    NestJS testing utilities
    Test.createTestingModule with overrideProvider lets you swap external services, and supertest drives the HTTP layer of a real app instance. Vitest or Jest both work; Vitest with SWC is common for speed in TypeScript projects.
    Contract testing
    Consumer-driven contracts with Pact verify that a provider still satisfies what its consumers actually use, without a full shared environment. For OpenAPI-first teams, schema-based checks and breaking-change diffing in CI catch many of the same issues.
    Testing async and time
    Use fake timers or an injected clock for expiry logic, and test queue handlers by calling them directly plus one integration path through the real queue. Test retries and idempotency explicitly by delivering the same message twice.

    Do

    • Test against the same database engine and version as production.
    • Build test data with small factory functions instead of large shared fixtures.
    • Run the full integration suite on every pull request and keep it under a few minutes.
    • Add a regression test for every production bug before fixing it.

    Watch out for

    • Mocking the ORM so thoroughly that tests verify the mock instead of the query.
    • Tests that depend on execution order or shared mutable data and fail randomly in CI.

    Checkpoint project: Confidence suite

    Add unit tests for pricing logic, Testcontainers-based integration tests for the orders repository and API, and a Pact or OpenAPI diff check between the service and its generated client. Done when the suite runs in CI under five minutes, catches a deliberately introduced breaking change, and reproduces the oversell race from stage 05.

    • VitestVitest
    • Testcontainers
    • Pact

    Quick check

    0/4 answered · 0 correct

    Get at least 3 of 4 right to mark this stage as passed.

    1. Question 1 of 4IntermediateWhy is heavily mocking the ORM in repository tests a weak approach?
    2. Question 2 of 4IntermediateWhy are integration tests against real PostgreSQL via Testcontainers better than in-memory SQLite?
    3. Question 3 of 4IntermediateHow do you test that the OrderPaid event consumer is idempotent?
    4. Question 4 of 4IntermediateYou need to test token expiry after 15 minutes. What is the right approach?

Advanced09–10

  1. 09Stage 09, 4 weeksObservability and API securityMake a service debuggable in production with logs, traces and metrics, and harden it against the OWASP API Top 10.
    I grouped these because both are about what happens after deploy, when you are not watching. The first time I tried to debug a production issue from unstructured console.log lines, I understood why both matter.

    What to learn

    Structured logging
    Log JSON with Pino (nestjs-pino integrates it), include a request or trace ID on every line, and use levels consistently. Never log secrets, tokens or personal data; redact by path in the logger config.
    OpenTelemetry traces and metrics
    OpenTelemetry is the vendor-neutral standard for traces, metrics and logs; the Node SDK with auto-instrumentation covers HTTP, PostgreSQL, Redis and more. Export via OTLP to a Collector, then to any backend such as Grafana, Jaeger, Honeycomb or Datadog, and track RED metrics (rate, errors, duration) per route.
    SLOs and alerting
    Define service level objectives on user-facing latency and error rate, and alert on error budget burn rather than on every CPU spike. Dashboards answer known questions; traces with high-cardinality attributes answer the unknown ones.
    OWASP API Security Top 10
    The 2023 edition is still the current API list: broken object level authorization (BOLA), broken authentication, broken object property level authorization, unrestricted resource consumption, broken function level authorization, unrestricted access to sensitive business flows, SSRF, misconfiguration, improper inventory management and unsafe consumption of APIs. BOLA tops the list for a reason, so test it on every endpoint.
    Hardening the service
    Enforce body size limits, timeouts and rate limits, block SSRF by allowlisting outbound hosts, and set security headers with Helmet. Scan dependencies and container images in CI, pin lockfiles, and treat npm supply chain attacks as a real risk.

    Do

    • Propagate one trace ID from the edge through HTTP calls, queues and logs.
    • Redact sensitive fields in the logger, not at each call site.
    • Add an authorization test for every new endpoint that tries another user's resource ID.
    • Run dependency and image scanning on every build and fail on critical findings.

    Watch out for

    • Logging full request bodies with passwords or tokens into a shared log platform.
    • Exposing internal fields such as role or isAdmin to mass assignment through DTOs.

    Checkpoint project: Production-ready telemetry

    Instrument the service with OpenTelemetry and Pino, export to a local Collector with Grafana or Jaeger, and define one latency SLO with an alert. Done when a single order request is visible as one trace across API, database and queue worker, and a security checklist mapped to the OWASP API Top 10 passes with tests.

    Quick check

    0/4 answered · 0 correct

    Get at least 3 of 4 right to mark this stage as passed.

    1. Question 1 of 4AdvancedWhat is BOLA (broken object level authorization), the top item in the OWASP API Security Top 10?
    2. Question 2 of 4AdvancedWhy redact sensitive fields in the logger config (for example Pino redact paths) instead of at each log call site?
    3. Question 3 of 4AdvancedA link preview endpoint fetches a user-supplied URL. An attacker sends http://169.254.169.254/latest/meta-data/. What is the right fix?
    4. Question 4 of 4AdvancedThe team suffers from alert fatigue. Which alert should stay for the checkout service?
  2. 10Stage 10, 6 weeksScaling, distributed systems and deployScale a service horizontally, reason about consistency and failure, and deploy it safely with containers and health checks.
    This is where I am still learning the most, mostly by reading and building side projects rather than running large systems. What I would tell myself: most scaling problems are solved by a better query, an index or a queue long before you need microservices.

    What to learn

    Consistency and failure models
    Understand linearizability, read-your-writes and eventual consistency, and that replicas lag so a read after write may be stale. Timeouts, retries with exponential backoff and jitter, and circuit breakers keep one slow dependency from taking down the whole system.
    Rate limiting and load shedding
    Token bucket or sliding window counters in Redis protect shared resources per user, key or tenant, and return 429 with Retry-After. Shed load early when queues or latency grow instead of letting everything time out together.
    Event-driven architecture
    Events decouple services, but you trade simple transactions for sagas, idempotent consumers and eventual consistency. Start with a modular monolith and clear module boundaries; extract services only when team or scaling boundaries demand it.
    Containers and deploy pipelines
    Multi-stage Docker builds with a slim Node base, a non-root user and only production dependencies; run migrations as a separate step before rolling out. Platforms range from Kubernetes to managed services like Cloud Run, Fly.io or Railway; pick the simplest one that meets your needs.
    Health, rollout and configuration
    Expose liveness and readiness endpoints separately, handle SIGTERM with a drain period, and roll out with rolling, blue-green or canary deploys behind feature flags. Follow twelve-factor config through environment variables and a secret manager, never baked into images.

    Do

    • Measure first with load tests (k6 or autocannon) and fix the slowest query before adding infrastructure.
    • Keep services stateless so any instance can serve any request.
    • Set a timeout on every outbound call, including database and Redis clients.
    • Make deploys reversible: backward-compatible migrations and a one-command rollback.

    Watch out for

    • Splitting into microservices early and ending up with a distributed monolith that must deploy together.
    • Retrying immediately without backoff and turning a small outage into a retry storm.
    • Using one health endpoint for both liveness and readiness, causing restarts during dependency blips.

    Checkpoint project: Ship it and break it

    Containerize the service, deploy two or more instances behind a load balancer on a managed platform or local Kubernetes, and add per-tenant rate limiting. Done when a k6 test shows stable p95 latency at target load, killing an instance drops no requests, and a bad release can be rolled back in one command.

    Quick check

    0/4 answered · 0 correct

    Get at least 3 of 4 right to mark this stage as passed.

    1. Question 1 of 4AdvancedWhy should the liveness probe not check the database connection?
    2. Question 2 of 4AdvancedWhy do retries need exponential backoff with jitter?
    3. Question 3 of 4AdvancedAn API key exceeds its requests-per-minute limit. What is the right response?
    4. Question 4 of 4AdvancedA user updates their profile (written to the primary), and the next page reads from a read replica and still shows old data. What is the right fix?

What matters right now, and where things are heading in the next few years.

Now

  • TypeScript everywhere, with lighter runtimes

    Node 24 LTS runs TypeScript via type stripping, Bun and Deno have matured, and ORMs like Prisma 7 dropped native engines for pure TypeScript. The practical result is simpler tooling, but frameworks that depend on decorator metadata like NestJS still need a real compile step.

  • PostgreSQL as the default database

    PostgreSQL 18 added asynchronous I/O and a built-in uuidv7(), and its extensions (pgvector, PostGIS, full text) let one database cover workloads that used to need several. Knowing Postgres deeply is now a core backend skill, not a DBA specialty.

  • Contracts and standards over custom glue

    OpenAPI 3.2, RFC 9457 Problem Details, OAuth 2.1 practices with RFC 9700 and OpenTelemetry give shared vocabulary across teams and tools. Generating clients, docs and tests from contracts is now expected rather than a nice extra.

  • Authorization and API abuse as the main risk

    The OWASP API Top 10 still lists broken object level authorization first, and business-flow abuse by bots keeps rising. Fine-grained authorization, rate limiting and passkeys are moving from advanced topics to baseline.

Next

  • APIs consumed by AI agents

    More traffic will come from LLM agents calling APIs through tool protocols like MCP, which makes clear schemas, good error messages, idempotency and scoped tokens even more important. Prepare by writing precise OpenAPI descriptions and designing safe, permission-scoped operations.

  • Durable execution and workflow engines

    Tools like Temporal and other durable workflow engines handle retries, timers and long-running processes that teams used to build from queues and cron. Learn the outbox, saga and idempotency patterns first so these engines feel like a natural next step.

  • Policy-as-code authorization

    Relationship and policy based engines such as OpenFGA, SpiceDB, Cedar and OPA are becoming common as apps add sharing, multi-tenancy and agent permissions. Practice separating authorization rules from handlers and testing them as data.

  • Observability by default and platform engineering

    OpenTelemetry is becoming the default instrumentation layer, and internal developer platforms abstract much of Kubernetes away. Backend engineers will be expected to own SLOs and production behavior of their services, so get comfortable reading traces and defining alerts.

By the end of this map you can

  • You can design a REST API with OpenAPI, consistent errors, idempotent writes and cursor pagination, and explain when GraphQL or tRPC fits better.
  • You can structure a NestJS service with clean modules, guards, pipes, interceptors and validated configuration.
  • You can model a PostgreSQL schema, read query plans, pick isolation levels and ship zero-downtime migrations through Prisma or Drizzle.
  • You can implement session or OIDC login with passkeys and enforce object-level, tenant-aware authorization.
  • You can move slow work to idempotent BullMQ jobs and publish events reliably with a transactional outbox.
  • You can test services with Testcontainers and contract tests, and trace a request across API, database and workers with OpenTelemetry.
  • You can containerize, deploy and scale a stateless service with health checks, rate limiting and safe rollbacks.

Related articles

All articles

Backend Engineer interview questions

See all 40

Other roles

Roadmap

Frontend Engineer

My main role

Building web interfaces that stay fast, accessible and maintainable as products grow

10 stages, 51 topics, ~34 weeks

Build iOS and Android apps that feel smooth, work offline and ship safely

10 stages, 50 topics, ~34 weeks

Find risk before users do and turn quality into something the team can measure

10 stages, 50 topics, ~36 weeks

Builds infrastructure, pipelines and platforms so teams ship fast, safely and reliably

10 stages, 56 topics, ~42 weeks

Build products on top of LLMs: APIs, RAG, agents, evals and running it all in production

10 stages, 58 topics, ~31 weeks