Map the API

Define request and response contracts

Specify allowed methods, parameters, bodies, content types, responses, and errors so unknown input and accidental data exposure are visible.

A clear contract gives both clients and the server a boundary. Open-ended objects create room for accidental authority: any field the server accepts is a field an attacker will try.

Specify the request side

Describe required fields, limits, formats, allowed values, and response shapes. Reject unsupported methods and content types. Here is a request schema for creating an invoice:

{
  "type": "object",
  "required": ["customerId", "items"],
  "additionalProperties": false,
  "properties": {
    "customerId": { "type": "string", "pattern": "^cus_[a-z0-9]{12}$" },
    "items": { "type": "array", "minItems": 1, "maxItems": 100 },
    "notes": { "type": "string", "maxLength": 500 }
  }
}

additionalProperties: false is doing real security work here. Any field outside the contract gets rejected instead of silently flowing into your database layer.

Publish the schema — OpenAPI works well — so clients, server validation, and contract tests all share one source of truth instead of three diverging opinions.

Verify the rejection actually happens:

curl -i -X POST https://api.example.com/invoices \
  -H "Content-Type: application/json" \
  -d '{"customerId":"cus_ab12cd34ef56","items":[{"sku":"PLAN-PRO"}],"isPaid":true}'
# HTTP/1.1 400 Bad Request
# {"error":"unknown field: isPaid"}

Select response fields deliberately

Select response fields deliberately rather than serializing database models. An invoice response may accidentally expose internalNote when code serializes the database row. A strict response schema catches that leak without forcing every internal field into the public contract.

function invoiceResponse(row) {
  return {
    id: row.id,
    customerId: row.customer_id,
    total: row.total,
    status: row.status,
  }
}

New columns added by a migration never reach clients until someone adds them here on purpose.

Test the whole response

Contract tests should inspect the complete response, not only required fields. Assert that the set of returned keys equals the approved set, not just that the required ones exist.

This catches new database columns that appear after a migration and were never approved for API clients. The test fails the day the column ships, not the day a customer notices the leak in your JSON.

Write request and response schemas for creating one invoice. Add an unknown input field and an internal database field, then show both are rejected or absent from the response.

Lesson completed

Take this course offline

Get every free book, course edition, and software download.

Get the download library →