Razvoj

Ditch the Docs: Architecting APIs for Human Readability

Zaboravite dokumentaciju: Oblikovanje API-ja za ljudsku čitljivost

An API that needs a separate document to explain every request is carrying too much hidden meaning. The usual response is to write better documentation. Sometimes that is necessary. More often, the better move is to reduce the amount of interpretation the API demands.

Human-readable APIs are not APIs with clever endpoint names. They are systems whose intent survives the journey from route to controller, validation, database write, response, logs, and error handling. A developer should be able to infer the normal path without decoding conventions that exist only in someone else’s head.

That does not make documentation obsolete. Authentication, business context, guarantees, migrations, limits, and unusual workflows still need explanation. But documentation should clarify the edges of an API, not compensate for an unreadable center.

Make the common path obvious

Start with the resource and the operation. An endpoint such as POST /orders says more than POST /createOrder, because the HTTP method already supplies the verb. A payload with customer_id, items, and shipping_address communicates a stable domain model more clearly than a collection of abbreviated fields and action flags.

Consistency matters more than personal preference. If an API uses plural nouns for collections, nested objects for related data, ISO-style date strings, and snake_case field names, apply those decisions everywhere. A consumer can then learn the system once instead of relearning it endpoint by endpoint.

POST /orders

{
  "customer_id": "cus_4821",
  "items": [
    {
      "product_id": "prod_keyboard",
      "quantity": 2
    }
  ],
  "shipping_address": {
    "line1": "12 Market Street",
    "city": "Bristol",
    "postal_code": "BS1 1AA",
    "country_code": "GB"
  }
}

This request does not answer every business question, but it makes the basic contract legible. The caller can see which concepts exist, how they relate, and what data belongs together.

Let names reveal intent, not implementation

Names are part of the user interface. Avoid exposing database-shaped language when it does not match the domain. A public field called status_id may be convenient internally, but status: "pending_payment" is usually more meaningful to a client. The database can retain foreign keys; the API can present the state a human actually needs to understand.

The same principle applies in PHP. A method named transitionOrderToPaid() tells a reader more than updateOrderStatus(). The former carries a business rule. The latter leaves readers asking which statuses are valid, who may choose them, and whether payment has truly been confirmed.

final class OrderService
{
    public function markPaid(Order $order, PaymentReference $payment): void
    {
        if (! $order->canBePaid()) {
            throw new OrderCannotBePaid($order->id());
        }

        $order->markPaid($payment);
        $this->orders->save($order);
    }
}

This is not verbosity for its own sake. It makes invalid states harder to overlook during code review and reduces the temptation to expose a generic “change anything” endpoint.

Design errors as part of the conversation

Most API frustration happens when the happy path ends. A 400 with “Invalid input” shifts detective work onto the caller. A useful error identifies the failed field, explains the violated rule, and preserves a predictable shape.

{
  "error": {
    "code": "validation_failed",
    "message": "The request contains invalid fields.",
    "details": [
      {
        "field": "items[0].quantity",
        "message": "Quantity must be at least 1."
      }
    ]
  }
}

Keep error codes stable enough for machines and messages clear enough for people. Do not force clients to parse prose, and do not return raw database or framework exceptions. Internal details can leak implementation choices, security information, and confusion. Log the diagnostic context on the server; return the actionable contract to the client.

Choose status codes carefully

Status codes should support the meaning already present in the response. Use successful responses for completed operations, validation responses when submitted data cannot be accepted, authentication responses when identity is missing or invalid, authorization responses when a known identity lacks access, and conflict responses when a request collides with the current state. The goal is not ceremonial REST purity. It is giving clients enough signal to make the next correct decision.

Keep reads and writes unsurprising

A readable API respects expectations about side effects. A GET should not silently trigger a workflow, a retryable create operation should not accidentally produce duplicate records, and a field called total should not sometimes mean pre-tax and sometimes mean final payable amount.

For write operations that may be retried after a network failure, use an explicit idempotency strategy when duplicate creation would be harmful. The important architectural decision is to define what “the same request” means and to store enough state to return a consistent result. Naming the mechanism clearly, such as an Idempotency-Key request header, makes its purpose discoverable before a client reads a long integration guide.

Likewise, pagination should say what it is doing. A response that includes next_cursor makes continuation visible. An endpoint that accepts limit should define a sensible maximum and return a clear validation error if the caller exceeds it. Hidden defaults are sometimes necessary, but hidden behavior should never be surprising.

Build the contract through every layer

Readable external design falls apart when the implementation speaks a different language. If a controller receives an order request, passes an unstructured array into a service, and lets a repository decide business rules, the API may look polished while remaining difficult to change safely.

Give each layer a narrow, recognizable job:

  • Controllers translate HTTP requests and responses.
  • Request validators reject malformed or incomplete input early.
  • Application services coordinate a named business operation.
  • Domain objects protect meaningful state transitions and invariants.
  • Repositories handle persistence without becoming a second business layer.

This separation is especially valuable in PHP applications where framework conveniences can make it easy to blur responsibilities. Convenience is useful until it hides a rule that future maintainers need to find. Put rules where their names and tests make them visible.

Use examples as a design test

Before publishing an endpoint, write one ordinary request, one successful response, and one likely failure response. If those examples require lengthy commentary to make sense, reconsider the contract. Perhaps two concepts have been compressed into one field. Perhaps an action should be a resource. Perhaps the endpoint exposes an internal workflow instead of a client-facing capability.

Examples are not merely documentation artifacts. They are a low-cost architecture review. They reveal ambiguity before it becomes client code, database migrations, support tickets, and compatibility commitments.

The goal is less interpretation

“Ditch the docs” is deliberately provocative. Good APIs still deserve reference material and thoughtful onboarding. But the strongest documentation is an API that explains its everyday behavior through its routes, names, payloads, errors, and boundaries.

Every unnecessary mystery becomes a future dependency on tribal knowledge. Remove enough of those mysteries, and the documentation becomes what it should be: a guide to the important decisions, not a decoder ring for the basics.

Portret autora bloga

Mihajlo

Ja sam Mihajlo — programer vođen znatiželjom, disciplinom i stalnom željom da stvorim nešto smisleno. Dijelim uvide, tutorijale i besplatne usluge kako bih pomogao drugima da pojednostave svoj rad i rastu u svijetu softvera i umjetne inteligencije koji se neprestano razvija.