← Blog
engineeringarchitecturebackendapi

API Design Is UX

24 March 2026

API Design Is UX

You wouldn't ship a UI where buttons have unpredictable labels, forms give no feedback when they fail, and the navigation works differently on every page. But developers ship APIs like this all the time — and then wonder why integrations are slow, support requests pile up, and nobody uses the SDK.

An API is a product. The developer calling it is your user. Everything you know about UX applies.

The caller is a user

When a developer hits your endpoint, they're not looking at code — they're looking at an interface. They have a mental model of what it should do. They read the name of the route, form an expectation, and send a request. If the response surprises them, they've had a bad experience, the same way a user clicking a button that does something unexpected has had a bad experience.

The difference is that bad UI fails at the moment of interaction. Bad API design fails at the moment of integration — which might be days into a project — and then keeps failing every time someone new picks up the codebase, reads the client code, and tries to understand what it does.

Bad UX is a problem once. Bad DX is a problem forever.

Naming is the first impression

The name of an endpoint is the first thing a developer reads. It sets the expectation that everything else has to meet.

POST /users/create is redundant — POST already implies creation. GET /user when you're returning a list is a lie. POST /do-the-thing is a joke, but I've seen it in production. These aren't nitpicks. A misnamed route means the caller has to hold two things in their head: what the name says and what the route actually does. That's cognitive load you're adding to every integration, forever.

The rule is simple: names should say exactly what happens. POST /users creates a user. GET /users/:id returns one. DELETE /users/:id removes it. If you can't name a route without ambiguity, the route is probably doing too many things.

Errors are communication

Nothing tells a developer more about the quality of an API than what happens when something goes wrong.

A 500 Internal Server Error with an empty body says: we didn't think about this case. A 400 Bad Request with the message "Bad Request" says: we thought about it, but not about you. A 400 with "The field 'email' is required and was not provided" says: we designed this for someone to actually use.

Error responses are the most important part of an API to get right, because they're what the caller reads when things don't work — which is exactly when they need clarity the most. The error message should tell them what went wrong, why it went wrong, and ideally what to do about it. Everything else is friction.

HTTP status codes matter too. A 200 that returns { "success": false } is actively harmful — it breaks every client that routes on status. Use the codes as intended. 401 means unauthenticated. 403 means unauthorized. 404 means not found. 422 means the input was understood but invalid. These aren't suggestions.

What good looks like

An OpenAPI spec is a useful lens because it forces you to articulate every decision: the route name, the required fields, the optional ones, the possible responses. Here's what a well-designed endpoint looks like written out:

paths:
  /users:
    post:
      summary: Create a user
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, name]
              properties:
                email:
                  type: string
                  format: email
                name:
                  type: string
                role:
                  type: string
                  enum: [member, admin]
                  default: member
      responses:
        "201":
          description: User created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/User"
        "400":
          description: Validation error — a required field is missing or invalid
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "409":
          description: A user with this email already exists
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

components:
  schemas:
    Error:
      type: object
      properties:
        error:
          type: object
          properties:
            message:
              type: string
            field:
              type: string

A few things worth noting: POST /users, not POST /users/create. 201 on success, not 200. Two distinct error codes — 400 for bad input, 409 for a conflict — each with a human-readable description. role is optional with a default, so callers don't need to think about it unless they care. And the error shape is the same on every response, so the client can handle it once.

None of this is clever. It's just consistent, specific, and honest about what can go wrong.

Sensible defaults

A well-designed API works out of the box with the minimum required input. Optional parameters should be optional. Pagination should default to something reasonable. Sorting should default to something predictable.

If a caller has to pass seven parameters just to make a basic request, you've made a decision on their behalf: their use case doesn't matter unless they've read the entire documentation first. That's the API equivalent of a form with thirty required fields.

Defaults are a design decision. They say: this is what we expect most callers to want. Get them right and the common case requires almost no configuration. Get them wrong and every caller writes the same boilerplate to override them.

Consistency is the primary virtue

The single most important quality in an API isn't cleverness or feature completeness. It's consistency.

If you paginate one endpoint with page and per_page, paginate all of them that way. If you return dates in ISO 8601 on one route, return them in ISO 8601 everywhere. If your error shape is { "error": { "message": "..." } }, don't return { "message": "..." } on a different route because you wrote it on a different day.

Inconsistency forces callers to handle each endpoint as a special case. It makes the client code fragile and the codebase hard to navigate. It suggests that nobody was thinking about the API as a whole — just shipping endpoints one at a time without a unifying perspective.

A consistent API can have gaps. It can be missing features. Callers will work around gaps. They can't work around inconsistency without encoding it permanently into every client they write.

The API you publish is the API you maintain

Every endpoint you ship is a promise. Callers will build on it. They'll write code that depends on the exact shape of the response, the exact semantics of the status codes, the exact names of the fields. Changing any of that later is a breaking change — a cost you'll pay in versioning, migration guides, and deprecation cycles.

This is why the design deserves real thought before the first response ever leaves the server. Not because APIs are hard to write, but because they're hard to change. The implementation can be refactored freely. The contract is permanent until you're willing to break it.

Design it for the person calling it. They'll be calling it for a long time.