FullStack Course LogoFullStack Course
Module: Full Stack
Full Stack·150·16 MIN READ

150: Contract-First HTTP APIs, Validation, Errors, and Compatibility

TOPICS COVERED: Contract-First HTTP APIs, Validation, Errors, and Compatibility

Learning outcomes

By the end of this lesson, you can:

  • explain and apply resource and action modeling in a realistic implementation;
  • explain and apply request validation in a realistic implementation;
  • explain and apply response contracts in a realistic implementation;
  • explain and apply pagination and filtering in a realistic implementation;
  • explain and apply idempotency and retries in a realistic implementation.

Prerequisites and retrieval

This lesson assumes the earlier 01–06 foundation and the preceding lessons in this module. Before reading, retrieve one concrete example from an earlier project where one of these concerns appeared. Perhaps a list endpoint returned unstable pages, a client had to guess whether an error was retryable, or a duplicate submission created two records. That example gives the terminology somewhere useful to attach.

The goal is not to memorize a collection of API labels. The goal is to make a defensible decision inside a production full-stack web application with a React client, a Node.js API, persistent storage, authentication, observability, and deployment concerns. Keep asking what a caller can send, what the server promises in return, and how either side will recognize a failure.

Terminology

  • Resource and action modeling: Choose resource-oriented URLs for stable domain nouns and explicit action endpoints when the operation does not fit CRUD semantics.
  • Request validation: Treat path parameters, query strings, headers, cookies, and bodies as untrusted. Validation turns an unknown request into a typed, bounded command that the domain layer can safely consider.
  • Response contracts: Return stable status codes and payload shapes. A response contract includes more than the success body: it also describes errors, pagination metadata, and fields clients may safely depend on.
  • Pagination and filtering: Use bounded page sizes and deterministic ordering. Filtering narrows the result set; pagination controls how much of that set is returned in one response.
  • Idempotency and retries: Retries are normal in distributed systems. Idempotency means that repeating the same operation under the defined conditions does not create additional effects. Treat it as a precise engineering concept, not merely vocabulary.
  • Compatibility and versioning: Prefer additive evolution, tolerant readers, and deprecation windows. A compatible change lets existing clients continue to interpret the contract correctly.

Mental model

Treat Contract-First HTTP APIs, Validation, Errors, and Compatibility as a design problem with observable inputs, outputs, invariants, and failure modes. A full-stack team moves faster when request and response contracts are explicit, versionable, validated, and testable rather than inferred from controller code. The controller may be the place where a request is handled, but it should not be the only place where the contract exists in someone's head.

A strong implementation makes assumptions visible, narrows uncertainty at boundaries, and leaves enough evidence—tests, types, constraints, metrics, or diagrams—to prove why the design is safe. For example, a schema can establish that limit is an integer within a maximum, while a database uniqueness constraint can establish that an idempotency key cannot be claimed twice for conflicting work. Those mechanisms solve different problems and should not be confused.

A useful interview and production sequence is:

text
requirement -> constraints -> model -> implementation -> failure analysis -> verification

Do not jump from a requirement directly to a library call. First state what must remain true. Then choose the mechanism that enforces it. A useful invariant might be that an order identifier is never exposed as belonging to another account, that a page has a stable order, or that a retried create request does not produce a second order. Once the invariant is explicit, the route shape, validation rule, storage constraint, and test become easier to evaluate.

Deep dive

1. Resource and action modeling

Start with the domain operation, not with a favorite URL pattern. Stable domain nouns usually map naturally to resources: GET /orders lists orders, GET /orders/:id reads one, and POST /orders creates one. PATCH /orders/:id can update an order when the operation is a partial resource change, and DELETE /orders/:id can request deletion when deletion is actually part of the domain.

Some operations are not ordinary CRUD changes. Capturing a payment, cancelling an order, or publishing a report may have a distinct business meaning, permissions model, audit requirement, or state transition. An explicit action endpoint such as POST /orders/:id/cancel can make that meaning visible instead of hiding it behind a generic update. The important question is not whether one style is universally more RESTful. It is whether the chosen contract exposes the operation and its invariant clearly enough for clients, reviewers, and monitoring to reason about it.

Decision rule: Use resource and action modeling deliberately when it makes the contract or invariant easier to prove. If it only reduces typing while hiding an assumption, prefer the more explicit design. Record what the endpoint does, which states it accepts, whether it is repeatable, and which actor is authorized to invoke it. A route that looks tidy but obscures a state transition will usually create confusion in validation and error handling later.

2. Request validation

Treat path parameters, query strings, headers, cookies, and bodies as untrusted. The fact that a browser normally sends a numeric-looking value does not make a path parameter a number, and the fact that a TypeScript type describes a request does not validate the JSON that arrived at runtime. Validate type, shape, ranges, enum membership, cross-field rules, and size limits before domain execution.

Validation belongs at the boundary, but not every rule belongs in the same schema. A malformed UUID, an unsupported sort value, or an oversized body can be rejected while parsing the request. A rule such as “the shipping address is required for a physical order” may require domain knowledge. Authorization is also separate: a structurally valid order request can still be forbidden for the current user. Keep validation, authentication, authorization, and business invariants distinct even though they can all result in a failed request.

Decision rule: Use request validation deliberately when it makes the contract or invariant easier to prove. If it only reduces typing while hiding an assumption, prefer the more explicit design. Reject unknown or unsafe input predictably, apply size and range bounds, and return an error that identifies the invalid field without echoing secrets. Never rely on client-side validation as the server's security or data-integrity boundary.

3. Response contracts

Return stable status codes and payload shapes. Clients should not have to parse an English error message to decide whether to show a form error, refresh credentials, stop retrying, or report a server problem. Distinguish validation errors, authentication failures, authorization failures, not-found, conflict, rate limits, and server faults so clients can react deterministically.

The distinction is useful in practice: malformed input is commonly a 400-class client correction, missing authentication is commonly 401, insufficient permission is commonly 403, a missing resource is commonly 404, and a state or uniqueness collision is commonly 409. The exact contract must be documented and applied consistently. A server fault should not expose a stack trace or database details to the client; it should provide a safe error identifier while the server logs the diagnostic context.

Decision rule: Use response contracts deliberately when it makes the contract or invariant easier to prove. If it only reduces typing while hiding an assumption, prefer the more explicit design. Define whether fields may be absent or null, how lists carry pagination metadata, and how errors are represented. Add fields in a way tolerant clients can ignore, and avoid silently changing the meaning or type of an existing field.

4. Pagination and filtering

Use bounded page sizes and deterministic ordering. A list endpoint that accepts an unbounded limit can consume excessive memory or database work, while an endpoint without an explicit order can return different records between requests even when the data has not changed. Filtering narrows the query; it does not replace pagination or authorization checks.

Offset pagination, such as page=3&limit=25, is simple and easy to inspect. It can drift under writes: if new rows are inserted near the beginning while a user moves to the next page, records can be repeated or skipped. Cursor or keyset pagination uses a stable ordered key, often a timestamp plus a unique identifier, and generally behaves better for large or changing collections. It requires a more deliberate cursor contract and a compatible database index, so it is not automatically the right choice for every small list.

Decision rule: Use pagination and filtering deliberately when it makes the contract or invariant easier to prove. If it only reduces typing while hiding an assumption, prefer the more explicit design. Define a maximum page size, validate filter and sort values against an allowlist, choose a tie-breaker for equal sort keys, and document whether the result is a snapshot or may change between requests. Inspect the query plan when the collection grows; a page-shaped response does not guarantee page-shaped database work.

5. Idempotency and retries

Retries are normal in distributed systems. A client may lose the response after the server commits, a proxy may time out, or a user may submit the same form twice. For create/payment-like operations, use an idempotency key or domain uniqueness constraint so a repeated request does not create duplicate effects. This is especially important when the operation has an external side effect, not just when the HTTP client happens to retry.

An idempotency key needs a defined scope and lifetime. The server must associate the key with the relevant actor and operation, record enough information to return the original result, and decide what happens when the same key is reused with a different request body. A uniqueness constraint can protect a domain fact, but it may not be enough to replay the original response. Conversely, an application-level key without durable storage or a transaction boundary can still race under concurrent requests. A retry policy also needs a boundary: retrying a safe read is different from blindly retrying an unknown outcome of a non-idempotent operation.

Decision rule: Use idempotency and retries deliberately when it makes the contract or invariant easier to prove. If it only reduces typing while hiding an assumption, prefer the more explicit design. Specify which failures are retryable, how clients identify a logical operation, what response a duplicate receives, and how concurrent claims are serialized. Do not claim that every POST is safe to retry merely because the server can accept an idempotency header.

6. Compatibility and versioning

Prefer additive evolution, tolerant readers, and deprecation windows. Adding an optional response field is usually easier for existing clients than renaming a field, changing its type, removing an enum value, or changing the meaning of a status code. A tolerant reader can ignore fields it does not understand, but it cannot safely handle every semantic change, so compatibility still needs explicit review and tests.

Version only when incompatible semantics cannot be evolved safely within the existing contract. Before adding /v2, first ask whether a new optional field, a new endpoint, or a staged deprecation can solve the requirement. If a version is necessary, define how clients discover it, how long the old version remains supported, and how telemetry identifies remaining consumers. Compatibility is a product and operations decision as well as a routing decision.

Decision rule: Use compatibility and versioning deliberately when it makes the contract or invariant easier to prove. If it only reduces typing while hiding an assumption, prefer the more explicit design. Treat schemas, error formats, pagination cursors, and authentication behavior as part of the contract, not just the path prefix.

Worked example

Consider a production full-stack web application with a React client, a Node.js API, persistent storage, authentication, observability, and deployment concerns. Start by writing the requirement in one sentence, then list the input and output contracts and identify which concept owns each failure mode. The important move is separation: parsing or validation belongs at the boundary; domain rules belong in the domain/service layer; persistence rules belong in the database or repository; presentation rules belong in the client. Mixing these concerns makes a happy-path demo look shorter, but it makes edge cases much harder to reason about and test.

ts
export async function handleRequest(input: unknown) {
  const command = parseCommand(input);
  const result = await service.execute(command);
  return toHttpResponse(result);
}

This small function is a boundary map, not a complete application. parseCommand should establish the runtime shape and bounds of the input. service.execute should apply domain rules and coordinate persistence or other dependencies. toHttpResponse should translate the result into the documented status code and payload rather than leaking an internal exception. Authentication context and authorization decisions must still be enforced on the server; a React client cannot be treated as an authority.

Walk the example with at least four cases: the normal path, an empty or missing value, a duplicate/retry/concurrent path where relevant, and a dependency failure. For each case, state which layer detects the problem and what the caller observes. For a validation failure, the boundary should reject the input before a repository call. For a duplicate, the service or database should enforce the invariant rather than relying on a client check. For a dependency failure, the API should return a safe, documented error and emit enough structured telemetry to investigate it. This is the level of explanation expected in a senior code review or technical interview.

Production perspective

Production correctness is broader than “the code works on my machine.” Ask how the design behaves during deploys, retries, partial failure, stale clients, concurrent requests, malformed data, schema changes, and high cardinality. Prefer explicit contracts, bounded resource usage, structured errors, and measurable behavior. Optimize only after you can identify the bottleneck or risk with evidence.

When the topic involves an external dependency, define a timeout and cancellation strategy. A request that waits forever can exhaust server capacity, and a retry without a deadline can multiply that pressure. When it involves persistence, define transaction and consistency expectations, including what happens if two requests observe the same old state. When it involves user-visible state, define loading, empty, error, stale, and success states so the client does not mistake an empty result for a failed request.

When it involves security, assume the client can be modified and the network input is untrusted. Do not place credentials or sensitive data in URLs merely because query strings are convenient; URLs can be logged or retained in history. HTTPS protects data in transit and authenticates the server when certificate validation succeeds, but it does not make an authorized user trustworthy or make an API's business rules correct. Observe status distributions, latency, retries, validation failures, and dependency errors without turning unbounded user input into unbounded metric cardinality.

Guided lab

Design list/create/update/delete endpoints for an order resource. Define request schemas, response schemas, error codes, pagination, one idempotent create path, and contract tests that run without a browser. Include authentication and authorization assumptions in the contract: for example, identify which caller can see an order and which caller can update or delete it. The goal is to make the boundary testable independently of React while still considering the behavior the client will need.

Complete the lab with this discipline:

  1. Write the requirement and two non-requirements. A non-requirement keeps the design from silently expanding into unrelated work.
  2. List input, output, and error contracts before implementation. Include path, query, headers, and body inputs, not only the JSON body.
  3. Implement the smallest correct vertical slice. Make the normal path observable before adding abstractions.
  4. Add at least one invalid-input test and one edge-case test. Include a case such as a duplicate idempotency key, a conflicting update, an empty page, or a maximum page size.
  5. Instrument or inspect the behavior instead of guessing. Use contract-test output, logs, response status, and a query plan where applicable.
  6. Refactor one hidden assumption into an explicit type, constraint, function, or configuration.
  7. Explain one alternative design and why you did not choose it. For example, compare offset and cursor pagination or an action endpoint and a generic update.
  8. Record a short “what would break at 10× scale?” note. Name the resource, query, dependency, or operational signal that would become the first concern.

Edge cases and failure modes

  • Resource and action modeling: test absence, malformed input, duplicates, ordering/concurrency where applicable, and behavior at the smallest and largest credible sizes. Also test invalid state transitions, such as cancelling an order that is already shipped, when the operation has that rule.
  • Request validation: test absence, malformed input, duplicates, ordering/concurrency where applicable, and behavior at the smallest and largest credible sizes. Check every input location and verify that invalid requests do not reach domain or persistence code.
  • Response contracts: test absence, malformed input, duplicates, ordering/concurrency where applicable, and behavior at the smallest and largest credible sizes. Assert status codes and meaningful shape, including safe errors rather than only successful JSON.
  • Pagination and filtering: test absence, malformed input, duplicates, ordering/concurrency where applicable, and behavior at the smallest and largest credible sizes. Verify deterministic ordering, maximum limits, allowed filters, and cursor or offset behavior while records are inserted or removed.
  • Idempotency and retries: test absence, malformed input, duplicates, ordering/concurrency where applicable, and behavior at the smallest and largest credible sizes. Send the same key concurrently, retry after an unknown response, and reuse a key with different input to verify the documented behavior.

Common mistakes and debugging

  • Solving the example instead of the requirement: a copied pattern can be syntactically correct but architecturally wrong.
  • Hiding uncertainty with assertions, broad exception handlers, permissive schemas, or “temporary” any values. These can move the failure away from the boundary without making the input safe.
  • Testing only the happy path and therefore discovering contracts only after integration.
  • Optimizing before measuring, or selecting a scalable mechanism without a scale requirement.
  • Letting client-side behavior stand in for server-side authorization, validation, or persistence guarantees.

For debugging, reproduce the smallest failing case, inspect the actual value or execution plan, trace the boundary where the invariant first becomes false, and fix the owning layer rather than adding a downstream patch. Start at a concrete boundary: source or build, browser or DOM, Network or HTTP, server or route, database or query, then deployment or configuration. A 404 response means the request reached an HTTP server that reported no matching resource; a DNS failure happens earlier and should be investigated at a different boundary. Likewise, a client displaying an error does not tell you whether parsing, authorization, the route, the database, or a dependency failed. Inspect the request, response, logs, and correlation information before choosing the fix.

Interview questions

  1. What problem does Resource and action modeling solve, and what trade-off or failure mode would make you choose a different approach?
  2. What problem does Request validation solve, and what trade-off or failure mode would make you choose a different approach?
  3. What problem does Response contracts solve, and what trade-off or failure mode would make you choose a different approach?
  4. What problem does Pagination and filtering solve, and what trade-off or failure mode would make you choose a different approach?
  5. What problem does Idempotency and retries solve, and what trade-off or failure mode would make you choose a different approach?

Checkpoint

Without notes, explain Contract-First HTTP APIs, Validation, Errors, and Compatibility to another developer in five minutes. Your explanation must include one invariant, one edge case, one production failure mode, and one alternative design. Then implement a small example without copying the lesson code. Be able to say which layer owns each decision and what evidence you would inspect when the behavior differs from the contract.

Mastery checklist

  • I can define the core terms precisely.
  • I can choose a design from requirements instead of from habit.
  • I can implement and test the normal path and edge cases.
  • I can explain the runtime, storage, or complexity cost.
  • I can identify which layer owns validation, errors, and recovery.
  • I can compare at least two reasonable alternatives.
  • I can explain how the design changes at larger scale or stricter reliability.

References

Reader page: /fullstack/lesson/150/contract-first-http-apis-validation-errors-and-compatibility