Skip to content

The 7 layers of a predictable API

Keyboard: ← → to move, F for full screen, O for overview.

Tan waving

Muhammet Şafak — Presentations

The 7 layers of a predictable API

An API that never surprises its client is the consistency of seven small decisions.

Muhammet Şafak

Tan pointing

Flow

Four stops

  1. Resource and status codeLayers 1 and 2
  2. List endpointsLayer 3: pagination, filtering, sorting
  3. Response and errorLayers 4 and 5
  4. Version and contractLayers 6 and 7

Seven layers

Every layer is a consistency decision

Resource
The URL is noun-based and plural; the HTTP method carries the action.
Status
Success is 2xx, client error is 4xx, server error is 5xx.
List
page, per_page, sort, order: the same names on every endpoint.
Envelope
Every response has the same skeleton: success, data, message, errors.
Error
Errors come back as error.code, error.message, error.details.
Version
A breaking change calls for a new version.
OpenAPI
The contract is written before the code.

Layer 1 · Resource and method

The method carries the action, not the URL

The method carries the action, not the URL
MethodMeaningExample
GETReadGET /users/42
POSTCreatePOST /users
PUTReplace entirelyPUT /users/42
PATCHUpdate partiallyPATCH /users/42
DELETEDeleteDELETE /users/42

Layer 2 · Status code

The status code tells the outcome

Returning 200 OK for every error and putting "error": true in the body works, but it goes against the REST principle.
ClassCodesMeaning
2xx200 · 201 · 204Succeeded; created; no content
4xx400 · 401 · 403 · 404 · 422Bad data, identity, permission, not found, validation
5xx500Unexpected error on the server side

Layer 3 · Pagination

The identity of a page

  • 2current_pagelast_page: 15
  • 20per_pageUpper limit: min($adet, 100)
  • 21–40from – toRecords on this page
  • 287totalTotal records

Example from the source · OFFSET

Deeper pages, costlier OFFSET.

To reach page ten thousand, the database has to read through and skip 200,000 records; one solution is cursor pagination.

Layer 4 · Response envelope

Every response has the same skeleton

The client reads the same four fields on every endpoint: success, data, message, errors.

response.json
{"success": true,"data": {},"message": null,"errors": null}

Layer 4 · Response envelope

Lists use the same envelope

A paginated list comes back as data.items and data.meta; the envelope does not change.

response.json
{"success": true,"data": {  "items": [],  "meta": {    "current_page": 1,    "per_page": 15,    "total": 243,    "last_page": 17  }},"message": null,"errors": null}

Layer 5 · Error contract

An error is a contract too

code is fixed for machines, message can change for humans, details carries extra context.

error.json
{"error": {  "code": "VALIDATION_FAILED",  "message": "Gönderilen veriler doğrulanamadı.",  "details": [    {      "field": "email",      "message": "Geçerli bir e-posta adresi giriniz."    },    {      "field": "phone",      "message": "Telefon numarası zorunludur."    }  ]}}

Layers 4 and 5

Envelope and error rules

  • Error with 200Returning 200 + "error": true for errors works but goes against REST; caching and logging break.
  • FormatError codes in uppercase with underscores: QUOTA_EXCEEDED.
  • CodesNew codes can be added, old codes should not be removed.
  • ScaleIn a small service with a single client, a status code and a short message are enough.

Layer 6 · Versioning

Three strategies, URI preferred

The preferred one is URI versioning: readability, separate documentation, separate route groups.
StrategyExampleTrade-off
URIGET /api/v1/usersClearly visible in logs and proxies; multiplies URLs
HeaderX-API-Version: 2URLs stay clean; a missing header creates ambiguity
QueryGET /api/users?version=2Practical at the start; proxies and CDNs may not see it

Layer 6 · Versioning

At a basic level: which change calls for a new version?

Compatible

  • Adding a new field to the response.
  • Adding an optional parameter.
  • Enriching error messages.

Breaking

  • Removing or renaming an existing field.
  • Adding a required parameter.
  • Changing the response structure or the status code.

Deprecation

Give at least 6–12 months' notice.

For an API open to external clients, a deprecation period of at least 6-12 months is reasonable.

Layer 6 · RFC 8594 (2019) · RFC 9745 (2025)

Announce a version's lifespan with a header

RFC 8594: May 2019, Informational. RFC 9745: March 2025.
HeaderDocumentWhat it says
Deprecation: @1780272000RFC 9745When it was deprecated; the value is a date, true is invalid
Sunset: Sat, 31 Oct 2026 23:59:59 GMTRFC 8594When it will stop responding
Link: <…/docs/migrate-v2>; rel="deprecation"RFC 9745The address of the migration guide

Layer 7 · Contract-first

Contract first, code later

  1. Step 1: ContractOpenAPI 3.0.3
  2. Step 2: Parallel workbackend · UI · test
  3. Step 3: MockPrism
  4. Step 4: Validationcontract testing
  5. Step 5: Updatecontract or code

Layer 7 · OpenAPI

The contract is written before the code

The schema excerpt of the example contract. The /orders endpoint restricts the status filter to pending, completed, cancelled.

openapi.yaml
components:schemas:  Order:    type: object    required: [id, status, total]    properties:      id:        type: integer      status:        type: string      total:        type: number

Layer 7

When does contract-first pay off?

  • Small projectOne developer, small project: contract-first can be overkill.
  • Many consumersWeb, mobile, third parties: it definitely saves time.
  • Long lifeIn an API that will live for years, the contract is a decision record.
Tan smiling

Thank you

muhammetsafak.com.tr

Seven layers, seven promises of consistency. The details are in the six source posts.

Share, embed, download