Last Minute Notes

Backend Fundamentals

Beginner backend engineering: the life of an HTTP request, methods, status codes, headers, cookies and CORS, a runnable HTTP server, layered application structure, configuration and the twelve-factor app, data access, validation and errors, logging, and concurrency models.

📘 Backend Fundamentals

A backend is the part of a product that runs on servers: it receives requests, applies rules, talks to databases and other services, and returns responses. This page builds the mental model that every framework sits on: how a request travels, what HTTP means, how to structure code, how to configure it, and how it handles many requests at once. The runnable example uses only Node.js built-in modules, so you can see the mechanics without a framework.

Table of Contents

  1. The Life of a Request
  2. HTTP Essentials
  3. Cookies and CORS
  4. A Server From Scratch
  5. Application Structure
  6. Configuration and the Twelve-Factor App
  7. Data Access
  8. Validation and Errors
  9. Logging and Health Checks
  10. Concurrency Models
  11. Beginner Questions

1. The Life of a Request

What happens when a browser calls https://api.example.com/users/42?

StepWhat it involves
DNSNames to addresses, cached by resolvers and the OS
ConnectionTCP handshake (or QUIC for HTTP/3), then TLS to encrypt and authenticate the server
RequestA method, a path, headers, and optionally a body
EdgeA CDN, a load balancer and often an API gateway terminate TLS, route, rate limit, and cache
ApplicationRouting, authentication, validation, business logic, database calls
ResponseA status code, headers and a body
ReuseConnections stay open (keep-alive, HTTP/2 multiplexing) so the next request skips the handshakes

More on the infrastructure side in Building Blocks.


2. HTTP Essentials

HTTP is a text-based request and response protocol. A request looks like this:

POST /users HTTP/1.1
Host: api.example.com
Content-Type: application/json
Authorization: Bearer eyJhbGciOi...
Accept: application/json

{"name": "Ana"}

and the response:

HTTP/1.1 201 Created
Content-Type: application/json
Location: /users/43

{"id": 43, "name": "Ana"}

2.1 Methods

MethodMeaningSafe (no side effects)Idempotent (repeat = same effect)Body
GETRead a resourceYesYesNo
HEADLike GET without the bodyYesYesNo
POSTCreate, or run an actionNoNoYes
PUTReplace a resource entirelyNoYesYes
PATCHPartially modifyNoNot guaranteedYes
DELETERemoveNoYesUsually no
OPTIONSAsk what is allowed (CORS preflight)YesYesNo

Safe and idempotent matter because clients, proxies and load balancers retry requests. A retried POST can create two orders unless you add an idempotency key, see API Design.

2.2 Status Codes

RangeMeaningCommon codes
1xxInformational101 Switching Protocols (WebSocket)
2xxSuccess200 OK, 201 Created (with Location), 202 Accepted (async work), 204 No Content
3xxRedirect or cache301 Moved Permanently, 302 and 307 Temporary Redirect, 304 Not Modified
4xxClient error400 Bad Request, 401 Unauthorized (not authenticated), 403 Forbidden (authenticated but not allowed), 404 Not Found, 405 Method Not Allowed, 409 Conflict, 412 Precondition Failed, 415 Unsupported Media Type, 422 Unprocessable Content (valid syntax, invalid data), 429 Too Many Requests
5xxServer error500 Internal Server Error, 502 Bad Gateway, 503 Service Unavailable, 504 Gateway Timeout

The distinction that matters: 4xx means the caller should change the request, 5xx means the server failed and a retry might work. Clients and monitoring treat them differently, so do not return 200 with an error inside, and do not return 500 for bad input.

2.3 Headers That Matter

HeaderDirectionPurpose
Content-TypeBothFormat of the body (application/json, multipart/form-data)
AcceptRequestFormats the client can handle (content negotiation)
AuthorizationRequestCredentials: Bearer <token>, Basic ...
Cache-ControlBothCaching rules: max-age, no-store, private, public, s-maxage
ETag, If-None-MatchResponse, requestValidators for conditional requests, a match returns 304 with no body
LocationResponseURL of a created resource or redirect target
Set-Cookie, CookieResponse, requestCookies
Retry-AfterResponseWhen to retry after 429 or 503
X-Request-Id or traceparentBothCorrelate logs and traces across services
Content-EncodingResponsegzip, br, zstd compression
User-Agent, Host, OriginRequestWho is calling, which site, which origin

2.4 URLs

https://api.example.com:443/v1/users/42/orders?status=paid&limit=20#section
\___/   \_____________/ \_/ \__________________/ \_______________/ \_____/
scheme      host       port        path                 query        fragment (client only)
  • Path identifies the resource (/users/42/orders). Query string filters, sorts, paginates. Body carries the data being created or changed.
  • URLs identify nouns. The method is the verb: POST /orders, not POST /createOrder.

2.5 HTTP Versions

VersionKey points
HTTP/1.1One request at a time per connection, plain text, keep-alive
HTTP/2Multiplexed streams on one TCP connection, header compression, server push (rarely used)
HTTP/3Runs over QUIC on UDP, no TCP head-of-line blocking, faster connection setup and migration, supported by major CDNs and browsers

3. Cookies and CORS

3.1 Cookies

A cookie is a small value the server asks the browser to store and send back on every request to that site. It is the usual carrier of a session id.

Set-Cookie: sid=abc123; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=3600
AttributeEffect
HttpOnlyJavaScript cannot read it, which limits theft through XSS
SecureSent only over HTTPS
SameSite=Lax/Strict/NoneControls cross-site sending, the main defense against CSRF. Lax is the modern default
Domain, PathScope of the cookie
Max-Age / ExpiresLifetime, absent means a session cookie
__Host- prefixEnforces Secure, Path=/ and no Domain, hardening the cookie

Details of sessions vs tokens in Authentication and Security.

3.2 CORS

Browsers enforce the same-origin policy: a page from https://app.example.com cannot read responses from https://api.example.com unless the API opts in. An origin is scheme, host and port.

Cross-Origin Resource Sharing (CORS) is that opt-in, done with headers.

  • A preflight OPTIONS request is sent first for "non-simple" requests (methods other than GET, HEAD, POST, custom headers such as Authorization, JSON content type).
  • The server answers with Access-Control-Allow-Origin, -Methods, -Headers, and optionally -Credentials and -Max-Age (to cache the preflight).
  • With credentials (cookies), Allow-Origin must be one specific origin, not *.
  • CORS is a browser rule, not server security. curl and other servers ignore it. It only stops other websites' scripts from reading responses in a user's browser.
  • Common mistake: reflecting any Origin back with credentials allowed, which defeats the protection. Use an allowlist.

4. A Server From Scratch

Frameworks hide this, so build a tiny one with only Node's built-in http module: routing, JSON parsing, status codes, headers and error handling. The server starts on a free port, calls itself with fetch, and shuts down.

// runnable
const http = require('node:http');

const users = new Map([[1, { id: 1, name: 'Ana' }]]);
let nextId = 2;

function send(res, status, body, headers = {}) {
  const payload = body === undefined ? '' : JSON.stringify(body);
  res.writeHead(status, {
    'Content-Type': 'application/json',
    'Content-Length': Buffer.byteLength(payload),
    ...headers,
  });
  res.end(payload);
}

async function readJson(req) {
  let raw = '';
  for await (const chunk of req) raw += chunk;         // the body arrives as a stream of chunks
  if (!raw) return {};
  try { return JSON.parse(raw); }
  catch { throw Object.assign(new Error('invalid JSON'), { status: 400 }); }
}

const server = http.createServer(async (req, res) => {
  const url = new URL(req.url, 'http://localhost');
  try {
    if (req.method === 'GET' && url.pathname === '/healthz') return send(res, 200, { status: 'ok' });
    if (req.method === 'GET' && url.pathname === '/users') return send(res, 200, [...users.values()]);

    const match = url.pathname.match(/^\/users\/(\d+)$/);
    if (match && req.method === 'GET') {
      const user = users.get(Number(match[1]));
      return user ? send(res, 200, user) : send(res, 404, { error: 'user not found' });
    }
    if (req.method === 'POST' && url.pathname === '/users') {
      const body = await readJson(req);
      if (typeof body.name !== 'string' || !body.name.trim()) return send(res, 422, { error: 'name is required' });
      const user = { id: nextId++, name: body.name.trim() };
      users.set(user.id, user);
      return send(res, 201, user, { Location: `/users/${user.id}` });
    }
    return send(res, 404, { error: 'no such route' });
  } catch (e) {
    // client mistakes carry a status, anything else is a 500 and must not leak internals
    return send(res, e.status || 500, { error: e.status ? e.message : 'internal error' });
  }
});

server.listen(0, async () => {
  const base = `http://127.0.0.1:${server.address().port}`;
  const call = async (label, path, init) => {
    const r = await fetch(base + path, init);
    console.log(label.padEnd(24), r.status, (r.headers.get('location') || '').padEnd(9), await r.text());
  };
  await call('GET /users/1', '/users/1');
  await call('GET /users/99', '/users/99');
  await call('POST /users (valid)', '/users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{"name":" Ben "}' });
  await call('POST /users (no name)', '/users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' });
  await call('POST /users (bad JSON)', '/users', { method: 'POST', body: '{oops' });
  await call('GET /nope', '/nope');
  await call('GET /users', '/users');
  server.close();
});

What to notice:

  • Routing is just matching method and path. Frameworks add parameters, middleware and ergonomics.
  • The body is a stream, read chunk by chunk, so large uploads do not have to sit in memory.
  • 201 plus Location on create, 404 for a missing resource, 422 for a well-formed request with invalid data, 400 for malformed JSON, 500 with a generic message for bugs.
  • A single try/catch at the top is the seed of a central error handler.

5. Application Structure

Split code by responsibility so each layer can change and be tested independently.

LayerResponsibilityShould not
ControllerTranslate HTTP to a method call and back. Validate input shape. Choose status codesContain business rules or SQL
ServiceBusiness rules, transaction boundaries, coordinate repositories and clientsKnow about HTTP (req, res)
RepositoryPersist and load data behind an interfaceContain business decisions
DTO (data transfer object)The shape of data crossing the API boundaryExpose database entities directly (leaks columns, breaks on schema change)
MiddlewareCross-cutting concerns applied to many routesContain route-specific logic

A typical folder layout, organized by feature:

src/
  users/
    users.controller.ts     routes and HTTP mapping
    users.service.ts        business logic
    users.repository.ts     data access
    users.dto.ts            request and response shapes, validation schemas
    users.test.ts
  orders/ ...
  common/
    errors.ts  logger.ts  auth.middleware.ts  config.ts
  main.ts                   composition root: wire dependencies, start the server

Organize by feature (a folder per domain area) rather than by type (all controllers together), so related code changes together. Pass dependencies in (constructor injection), and wire them once in the composition root, see Architecture and Testing.


6. Configuration and the Twelve-Factor App

The twelve-factor app is a set of practices for building services that deploy cleanly to cloud platforms.

FactorPractice
CodebaseOne repository per app, many deploys from it
DependenciesDeclare explicitly (package.json, pom.xml) with a lockfile, never rely on system packages
ConfigStore config in the environment, not in code
Backing servicesTreat databases, queues, caches as attached resources reached by URL
Build, release, runStrictly separate: build an artifact once, combine with config, run
ProcessesStateless, share nothing, keep state in a database or cache
Port bindingExport the service by listening on a port
ConcurrencyScale out by running more processes
DisposabilityFast startup and graceful shutdown, robust to sudden death
Dev and prod parityKeep environments similar
LogsWrite to stdout as an event stream, the platform collects them
Admin processesRun one-off tasks (migrations) as separate processes

Configuration guidance:

  • Environment variables for anything that differs by environment: database URL, API keys, feature flags, log level. Validate them at startup and fail fast if missing.
  • Never commit secrets. Use a secret manager and inject at runtime, see Deployment.
  • Build once, deploy many: the same artifact runs in staging and production, only the environment differs.
  • Precedence is usually defaults, then config file, then environment, then command-line flags.
  • Node.js 20.6+ can load a .env file with node --env-file=.env app.js. Spring Boot uses application.yml with profiles and env overrides.

7. Data Access

ApproachExamplesTrade-offs
Raw driver and SQLpg, mysql2, JDBCFull control and performance, you write mapping code
Query builderKnex, jOOQ, KyselyComposable and type-safe SQL, close to the database
ORMPrisma, TypeORM, Drizzle (Node), Hibernate and JPA, Spring Data (Java), SQLAlchemy, Django ORMProductivity and mapping, risk of N+1 queries, hidden SQL, leaky abstractions

Rules that apply to all of them:

  • Always use parameterized queries (WHERE id = $1), never string concatenation, see Security.
  • Use a connection pool and size it sensibly, see Databases: connection pooling.
  • Keep transactions short and inside the service layer, and do not call external services while holding one.
  • Watch the SQL your ORM generates (enable query logging in development), and eager load to avoid N+1.
  • Migrations as versioned files applied in CI or at deploy, with backward-compatible steps, see Databases: migrations.
  • Set timeouts on queries and acquisition of connections.
  • Return DTOs, not entities, and select only needed columns.

8. Validation and Errors

8.1 Validate at the Boundary

Never trust input: clients, other services and even your own frontend can send anything.

  • Validate shape (types, required fields), format (email, UUID), ranges and lengths, and business rules (start before end).
  • Use a schema library: Zod, Joi, Ajv (JSON Schema) in Node, Bean Validation (@NotBlank, @Size, @Valid) in Java.
  • Allowlist fields when binding request bodies to objects, so a caller cannot set isAdmin: true (mass assignment).
  • Reject early with a clear, machine-readable message.
  • Also validate on the way out when serializing, so you never leak internal fields.

8.2 Error Handling

KindExampleResponseLog level
ValidationMissing name400 or 422 with field errorsdebug or info
AuthenticationBad token401info
AuthorizationNot your resource403 (or 404 to hide existence)warn
Not found, conflictUnknown id, duplicate email404, 409info
Dependency failureDatabase or payment provider down502, 503 or 504, retry latererror
BugNull pointer500 with a generic body and an error iderror with stack trace

Principles:

  • One central error handler maps exceptions to responses, so controllers stay clean.
  • Never leak stack traces, SQL or internal hostnames to clients. Log details server-side with a correlation id, and return the id.
  • Use a consistent error body. The standard is Problem Details for HTTP APIs (RFC 9457): type, title, status, detail, instance, see API Design.
  • Distinguish expected errors (client mistakes, business rule violations) from unexpected ones (bugs, outages).
  • Do not swallow exceptions. Either handle them meaningfully or let them propagate to the central handler.

9. Logging and Health Checks

Logging

  • Write structured logs (JSON) to stdout: timestamp, level, message, request id, user id, route, duration, and error fields.
  • Use levels consistently: error for failures needing action, warn for suspicious, info for business events, debug for diagnostics (off in production).
  • Add a correlation id per request (accept X-Request-Id or generate one) and include it in every log line and outgoing call.
  • Do not log secrets or personal data (passwords, tokens, card numbers). Redact.
  • Use a real logger (pino, Winston in Node, SLF4J with Logback in Java), not console.log in production code.

Health endpoints

EndpointMeaningUsed by
Liveness (/healthz)The process is alive and not stuck. If it fails, restart itKubernetes liveness probe
Readiness (/readyz)Ready to serve traffic: dependencies reachable, warmed up. If it fails, stop routing to itLoad balancers, Kubernetes readiness probe

Keep liveness shallow (do not check the database, or a database outage restarts every instance in a loop). Make readiness reflect whether this instance can serve. More in Deployment and Runtime and the observability notes in Reliability and Operations.


10. Concurrency Models

How does one server handle thousands of simultaneous requests?

ModelHowExamplesStrengthsWatch out
Thread per requestA pool of OS threads, each handles one request at a timeTraditional Java servlet containers (Tomcat), Rails with Puma, DjangoSimple blocking codeThreads are heavy (memory, context switches), the pool size caps concurrency
Event loop, async I/OOne thread runs callbacks and awaits I/O without blockingNode.js, Python asyncio, NginxHandles many idle connections cheaplyCPU-heavy work blocks everyone, need discipline
Virtual threadsCheap user-space threads, blocking style code, the runtime multiplexes them on few OS threadsJava 21+ virtual threads, Go goroutinesSimple code with high concurrencyPinning, thread-local usage, still bounded by downstream resources
Actors / reactive streamsMessage passing or non-blocking pipelinesAkka, Vert.x, Spring WebFlux, ReactorHigh throughputSteeper learning curve, harder debugging
Multi-processSeveral processes behind a balancerNode cluster, Gunicorn workers, PM2Uses all cores, isolationShared state must live outside

The takeaway: most backend time is spent waiting on databases and other services. Async I/O and virtual threads let a small number of threads serve many waiting requests, and the limit becomes the database and downstream services, not the web server. Node.js and Java specifics are in Node.js and Spring.


11. Beginner Questions

Q1. What is the difference between GET and POST? GET reads data, is safe and idempotent, and can be cached, so parameters go in the URL. POST sends data in the body to create or trigger something, is not idempotent, and is generally not cached.

Q2. What does idempotent mean, and which methods are? Repeating the request has the same effect as doing it once. GET, PUT, DELETE (and HEAD, OPTIONS) are idempotent, POST is not.

Q3. 401 vs 403? 401 means the caller is not authenticated (missing or invalid credentials). 403 means authenticated but not permitted.

Q4. 400 vs 422? 400 for a malformed request (bad JSON syntax). 422 for a well-formed request whose data fails validation. Many APIs use 400 for both, be consistent.

Q5. What is CORS and why does it exist? A browser mechanism that lets a server declare which other origins may read its responses, relaxing the same-origin policy. It protects users from other sites reading their authenticated responses, and it is not a server-side security control.

Q6. Cookie vs token authentication, briefly? A cookie holds a session id the browser sends automatically, so protect against CSRF. A token (JWT) is sent explicitly in the Authorization header, so it is not sent automatically but needs safe storage. Details in Authentication and Security.

Q7. Why layer controller, service and repository? Separation of concerns: HTTP handling, business rules and persistence change for different reasons, and layering makes each testable and replaceable.

Q8. What is the twelve-factor principle for configuration? Store config in the environment, not in code, so the same build runs in every environment and secrets stay out of the repository.

Q9. Liveness vs readiness? Liveness asks "should I restart this process?", readiness asks "should I send it traffic now?".

Q10. Why do many servers use an event loop or async I/O? Requests mostly wait on I/O, and non-blocking I/O lets one thread manage thousands of waiting connections without a thread each.

Next: Node.js and Spring Boot.

On this page