API Design: How to Design APIs Like a Pro — REST, GraphQL, gRPC, OpenAPI, and Best Practices

Most APIs are not designed — they are accumulated. A team ships an endpoint for the mobile app, another for the partner integration, a third for the internal dashboard. Six months later, the API surface is a patchwork of inconsistent naming, ad-hoc pagination, and error formats that no two services agree on. The cost is not aesthetic: every new consumer spends days reverse-engineering conventions, every change risks breaking someone, and every incident takes longer to debug because the contract was never written down.

API Design is the discipline of making those contracts explicit before code is written. It is closer to interface design than to programming: you decide what is exposed, what is hidden, how failures are communicated, and how the API will evolve without breaking its users. The protocol — REST, GraphQL, gRPC — is a implementation detail; the design decisions are what determine whether the API is a pleasure or a tax.

Why API design is harder than it looks

The naive view: define endpoints, pick JSON, done. The practitioner's view: every endpoint is a promise you will have to keep for years. Changing a field name is easy in code and expensive in contracts. Adding a required parameter breaks every existing client. Removing a field silently breaks parsers that expected it.

The core difficulty is that APIs are consumed by software you do not control. Unlike a UI, you cannot watch users struggle; you only see 400 errors and support tickets. Good design anticipates misuse: it makes the correct call obvious and the incorrect call fail loudly and early.

A second difficulty is that protocols are not interchangeable. REST, GraphQL, and gRPC solve different problems and impose different costs. Choosing wrong means fighting the tool: over-fetching with REST, N+1 queries with GraphQL, or browser incompatibility with gRPC. The choice should follow from the access patterns, not from what the team already knows.

A walkthrough: designing an orders API from scratch

Imagine a marketplace that needs an API for orders. The first instinct is GET /orders returning everything. That works until the order table has 2 million rows and the mobile client times out.

A practitioner starts with the contract, not the code. Using OpenAPI, they define the resource, the fields, and the error schema before writing a handler. The design questions come in order:

  1. Resource modeling. Is an order a single resource or a collection of line items? The answer determines the URL structure: /orders/{id} with embedded items, or /orders/{id}/items as a sub-resource. Embedding is simpler for clients but bloats responses; sub-resources are more flexible but require extra round trips.

  2. Pagination. Offset pagination (?offset=0&limit=50) is easy but degrades on large tables: the database still scans skipped rows. Cursor pagination (?after=abc123) is stable and fast but cannot jump to page 7. For an orders list sorted by creation date, cursor pagination is usually the right trade-off — clients rarely need random access, and the performance difference at scale is significant.

  3. Filtering and sorting. Allowing arbitrary filters (?status=shipped&created_after=2026-01-01) is powerful but invites expensive queries. A common compromise: expose a fixed set of indexed filters and document them in the OpenAPI spec. Unbounded filtering is a denial-of-service vector.

  4. Error handling. A consistent error envelope — for example, { "error": { "code": "ORDER_NOT_FOUND", "message": "..." } } — lets clients branch on code rather than parsing prose. HTTP status codes set the category (404 for missing, 409 for conflict, 422 for validation), but the machine-readable code carries the detail.

  5. Versioning. Adding a field is backward-compatible; renaming or removing one is not. The safest default is additive change with a version in the path (/v1/orders) or in a header. Path versioning is blunt but visible; header versioning is cleaner but easier to forget.

The non-obvious insight: the most expensive design decision is what you leave out. Every field you expose becomes a commitment. Exposing internal identifiers, database timestamps, or implementation-specific statuses creates coupling that is hard to undo. A good API exposes the minimum that satisfies the use case, and hides the rest behind a stable abstraction.

Key distinction REST is a style, not a protocol. The constraints that matter — statelessness, uniform interface, cacheability — are what make an API predictable. JSON over HTTP without those constraints is just RPC with extra steps.

Where the design breaks: four failure scenarios

Even a well-designed API meets conditions that invalidate its assumptions. These are not beginner mistakes; they are structural failures of the scheme itself.

1. The client base outgrows the versioning strategy

What goes wrong: you ship v1, then v2 with a breaking change. Six months later, a long-tail of clients still calls v1, and maintaining two versions doubles the test surface and the bug surface. Eventually v1 is deprecated, but a partner's integration breaks because they missed the email.

How to spot it early: track usage per version and per client. If v1 traffic is not declining, the migration path is unclear or the deprecation notice was too quiet.

What to do if it is too late: freeze v1 (security fixes only), publish a migration guide with concrete diffs, and set a hard sunset date with repeated communication. In the EU, the NIS2 directive and sector regulations may impose breach-notification duties if a deprecated endpoint causes a security incident — document the timeline.

2. GraphQL introspection becomes an attack surface

What goes wrong: you expose a GraphQL endpoint with introspection enabled and no query depth limit. A malicious client sends a deeply nested query that fans out into thousands of database calls, and the API goes down.

How to spot it early: monitor query depth and complexity. If p99 latency spikes without a corresponding traffic increase, suspect expensive queries.

What to do if it is too late: disable introspection in production, add depth and complexity limits, and use persisted queries. These are standard defenses, but they must be in place before the incident, not after.

3. The OpenAPI spec drifts from the implementation

What goes wrong: the spec is written once, then the code evolves and the spec is not updated. Consumers generate clients from the spec and get runtime errors because a field was renamed or a status code changed.

How to spot it early: contract tests in CI that compare the spec to actual responses. If the spec is not tested, it is documentation, not a contract.

What to do if it is too late: treat the spec as the source of truth and generate server stubs from it, or use a linter that validates responses against the spec on every build.

4. gRPC meets the browser

What goes wrong: you choose gRPC for internal services and then need to expose the same API to a web frontend. Browsers do not speak HTTP/2 trailers natively for gRPC; you need gRPC-Web and a proxy, which adds latency and operational complexity.

How to spot it early: map your consumers before choosing a protocol. If a browser is in the picture, gRPC may still work via gRPC-Web, but the cost is real.

What to do if it is too late: add a REST or GraphQL gateway in front of the gRPC services. This is a common pattern, but it means maintaining two contracts.

Choosing a protocol: a trade-off table

Criterion REST GraphQL gRPC
Best for Public APIs, CRUD, caching Flexible clients, mobile, aggregation Internal service-to-service, low latency
Over-fetching Common Avoided by design Avoided by schema
Caching HTTP caching works well Harder (POST, persisted queries) Limited (HTTP/2 streams)
Browser support Native Native Needs gRPC-Web proxy
Schema OpenAPI (optional but recommended) Built-in (SDL) Built-in (protobuf)
Learning curve Low Medium Medium-high

No protocol wins on every axis. A pragmatic architecture often uses gRPC internally and REST or GraphQL at the edge. The design work — resource modeling, error handling, versioning — is largely the same across all three.

Learning paths and their real costs

Self-study is the default. The materials are abundant: the OpenAPI Specification, the GraphQL spec, the gRPC docs, and many blog posts. The cost is time and the absence of feedback. You can read about cursor pagination in twenty minutes, but you will not know whether your implementation is correct until a client hits an edge case — often in production.

Practice on a real project is the fastest teacher, but it is also the most expensive: mistakes become incidents. A middle path is to design an API for a known domain (a bookstore, a ticketing system) and have someone review the contract. That review is the missing piece in most self-study: nobody tells you that your error schema is inconsistent or that your pagination will not scale.

Structured courses compress the feedback loop. They are not free, and they are not a substitute for building something. But they can surface the decision points — versioning, pagination, security — in the right order, with examples of what breaks when you get them wrong. For a topic where the cost of a bad decision is measured in years of maintenance, that structure is worth considering. The API Design course on asibiont.com follows this approach: personalized lessons adapted to your level, covering REST, GraphQL, gRPC, OpenAPI, versioning, and security (OAuth, API keys) with a focus on trade-offs rather than recipes.

What to check first Before committing to a protocol, list your consumers: browser, mobile, internal services, partners. The consumer mix, not team preference, should drive the choice.

Who this path suits — and who it does not

This path suits developers who already ship code and have felt the pain of an API that grew without a plan. It suits tech leads who need to set conventions for a team. It suits backend engineers moving toward platform or integration work, where the contract is the product.

It does not suit complete beginners. API design assumes you understand HTTP, JSON, and at least one backend language; without that, the trade-offs are abstract. It also does not suit those looking for a quick certification — the value is in the decisions you learn to make, not in a credential. And if your project is a throwaway prototype, the rigor may be overkill; ship it, learn, and design properly when the API has consumers you cannot coordinate with.

One more honest caveat: no course replaces the experience of maintaining an API in production. The best combination is structured learning for the frameworks and a real project for the judgment. See also , and Real-Time Systems: Myths, Realities, and the Engineering Behind Live Apps.

If you want a structured path through the design decisions — resource modeling, pagination, error handling, versioning, protocol choice, and security — the API Design course walks through them with examples and gives you a place to check your reasoning. Start with the course page and see whether the scope matches where you are. See also , and Unfair Contract Terms: How to Challenge Them Before You Pay.

← All posts

Ask AI about this article

Ask a question about this article — ASI Biont will answer based on its text.