Course outline

Node.js / Intermediate: application boundaries / Lesson 7

API contracts and validation

Define a small API boundary that distinguishes transport syntax, data validation, and domain rules before changing state.

About 4 min readContent reviewed
On this page

Lesson goals

  • Explain the concepts and trade-offs behind api contracts and validation.
  • Predict and verify both examples, including their outputs and cleanup behavior.
  • Diagnose the three common failures and apply the worked solutions to related inputs.

A contract is more than a route

An API contract defines accepted methods, paths, headers, field types, limits, and response semantics. Decide which unknown fields are rejected and whether missing values differ from null. A valid JSON object is not automatically a valid command. Keep transport decoding separate from validation and from the domain operation. That order makes it possible to reject an invalid request before allocating expensive resources or modifying data. Document examples of both accepted and rejected input so clients do not infer the contract from incidental implementation behavior.

Bound input before retaining it

Network data arrives in chunks whose boundaries do not match JSON tokens. A production reader must count actual bytes as chunks arrive, enforce a maximum, apply a read deadline, and choose a safe policy for unread data. Content-Length is an early hint, not sufficient enforcement against every transfer mode. Decode text only after the byte limit is satisfied, then parse and validate. The first example starts from an already bounded Buffer to demonstrate those later stages; its caller remains responsible for enforcing a limit while receiving the network body.

Validate structure without coercion

The parser accepts a non-array object with one name field. It checks the byte budget before parsing, rejects unknown fields, trims the name, and checks its length. A text field that looks like a number remains text unless conversion is an explicit contract choice. Return a new validated object rather than passing all user properties downstream. This prevents unexpected fields from becoming persistence attributes or authorization flags. Validation messages should identify a usable correction while avoiding disclosure of implementation details, filesystem paths, or credentials.

javascript Complete example: Validate structure without coercion
function parseCreate(bytes) {
  if (bytes.length > 128) throw new Error('413 body too large');
  let value;
  try { value = JSON.parse(bytes.toString('utf8')); }
  catch { throw new Error('400 malformed JSON'); }
  if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('422 expected object');
  if (Object.keys(value).some(key => key !== 'name')) throw new Error('422 unknown field');
  if (typeof value.name !== 'string' || value.name.trim().length < 1 || value.name.trim().length > 40) throw new Error('422 invalid name');
  return { name: value.name.trim() };
}
console.log(JSON.stringify(parseCreate(Buffer.from('{"name":"  Ada  "}'))));
try { parseCreate(Buffer.from('{"name":"Ada","admin":true}')); }
catch (error) { console.log(error.message); }
Expected output
{"name":"Ada"}
422 unknown field

Only the allowed normalized field crosses the boundary. This parser receives a complete bounded buffer; it is not a network body receiver.

Choose stable HTTP semantics

Use 400 for malformed request syntax, 413 for a body above the supported limit, and 415 for an unsupported media type. A project can use 422 for structurally decoded but invalid commands if that distinction is documented consistently. Authentication and authorization have separate meanings, and a not-found policy may deliberately conceal a protected resource. The second example validates a limit query parameter with an exact decimal format and a range. It produces a stable error object instead of allowing parser exceptions to leak into arbitrary server responses.

javascript Complete example: Choose stable HTTP semantics
function pageLimit(target) {
  const url = new URL(target, 'http://local');
  const values = url.searchParams.getAll('limit');
  if (values.length === 0) return { limit: 20 };
  if (values.length !== 1 || !/^\d+$/.test(values[0])) return { error: 'invalid_limit' };
  const limit = Number(values[0]);
  return Number.isInteger(limit) && limit >= 1 && limit <= 100 ? { limit } : { error: 'invalid_limit' };
}
console.log(JSON.stringify(pageLimit('/items?limit=25')));
console.log(JSON.stringify(pageLimit('/items?limit=1&limit=2')));
Expected output
{"limit":25}
{"error":"invalid_limit"}

Repeated parameters are rejected deliberately so different proxy and application interpretations cannot disagree.

Protect state-changing operations

Validation does not replace authorization, transactions, or idempotency. A valid transfer amount can still exceed a balance, and a valid resource identifier can still belong to another user. Perform domain checks close to the state change so concurrent requests cannot invalidate an earlier decision. For retryable create operations, define an idempotency key scoped to the authenticated caller and the intended operation. Store the key and outcome with the change when atomicity matters. A consistent contract helps clients retry safely and gives tests observable behavior to assert.

Common errors and fixes

A numeric string passes an integer schema

Why it happens
Implicit coercion replaced validation.
How to fix it
Require the documented type or explicitly document conversion.
Prevent it next time
Include wrong-type cases in contract tests.

Memory grows before a 413 response

Why it happens
The body was fully buffered before checking size.
How to fix it
Enforce bytes during reception and stop retaining excess data.
Prevent it next time
Test oversized chunked bodies and deadlines.

A client sets an admin property

Why it happens
All decoded fields were copied into a trusted model.
How to fix it
Construct an allowlisted command object.
Prevent it next time
Keep privilege decisions outside user-controlled fields.

Check your understanding

1. Classify malformed JSON, an oversized body, and a well-formed object missing its required name.

Show worked solution

Under this lesson’s contract they become 400, 413, and 422 respectively. Check the byte limit first, parse second, and validate fields third. A single generic 500 would misclassify client-correctable failures and obscure useful monitoring.

2. A valid update contains another user’s resource ID. Which additional check is required?

Show worked solution

Authorize the authenticated actor against the target resource and operation before changing state. Validation only establishes the identifier’s shape. Perform the ownership check in the same transactional decision as the update when concurrent ownership changes are possible.

Key takeaways

  • An API contract defines accepted methods, paths, headers, field types, limits, and response semantics.
  • Network data arrives in chunks whose boundaries do not match JSON tokens.
  • Validation does not replace authorization, transactions, or idempotency.

Official references