09.07.2026 • 6 min read

DesignFlix 24: Learning API Design, One Feature at a Time

Ever wondered what it takes to get good at API design?

You can build an endpoint in an afternoon and still have plenty to learn when someone else starts using it. What happens when they send the wrong input, request another person's order, or retry a request that already succeeded? Those questions are where a working endpoint starts becoming a useful API.

Let's turn the roadmap into a project: a small bookshop API. We'll add features as the shop needs them, so each new concept has a reason to exist.

Start with one request you can explain

An API gives one piece of software a defined way to communicate with another. In our shop, the website asks for books and the backend returns the catalogue data.

Start with a request such as this:

GET /books?limit=20 HTTP/1.1
Host: localhost:8000
Accept: application/json

Before writing more routes, explain this one. GET requests a representation of the books. The path identifies the collection, the query controls the requested page size, and the header says which response format the client accepts.

Then inspect the actual response: its status, headers, and body. If you can follow that exchange in your browser's network panel or an API client, you've already moved beyond treating an endpoint as a function with a URL.

The original API learning roadmap

Learn HTTP while building the shop

Now add an order-creation endpoint. This gives us a practical reason to learn the difference between reading data and asking the server to create something.

POST /orders HTTP/1.1
Host: localhost:8000
Content-Type: application/json

{"book_id": 17, "quantity": 2}

For this illustrative shop, a successful creation can return 201 Created and identify the new order. Invalid input should produce an error the client can understand. Returning 200 with a vague "something went wrong" string leaves the caller guessing.

Learn the methods and status codes through requests like these. Then add headers, cookies, caching, and the differences between HTTP versions. The HTTP semantics standard gives the definitions when you need to check a detail.

Decide who can use each part

The shop might have a public API for outside developers, a partner API for selected publishers, and a private API used by internal tools. Those access models help us think about the intended audience. They don't remove the need to authenticate and authorize particular requests.

Suppose a signed-in customer requests /orders/101. Authentication tells us who is making the request. We still need to check whether that customer may read order 101.

Try changing the number to an order belonging to a second test account. If its delivery address appears, the problem isn't solved by having a valid login token. The endpoint is missing an ownership check. OWASP describes this class of problem as broken object-level authorization.

Sessions, bearer tokens, JWTs, and OAuth address different parts of authentication and access. Learn the purpose of each before choosing a library. In particular, a JWT is a token format, while OAuth is a framework for delegated authorization; they aren't interchangeable login buttons.

Make the API easy for someone else to use

A useful API needs a contract another developer can follow. For the order endpoint, document the required fields, allowed quantities, authentication, success response, and errors.

A small error body might be:

{
  "error": "invalid_quantity",
  "message": "Quantity must be at least 1."
}

The stable error code helps the client decide what to do. The message explains the problem to a person. Both belong to the API's behaviour, so test and document them together.

OpenAPI can describe that contract in a standard format. Tools such as Swagger UI or ReDoc can then present it. For practice, ask someone to make a successful request using only your documentation. Anything you have to explain separately is useful feedback.

Add pagination before the catalogue gets large

Returning the entire catalogue works nicely when there are eight books. It gets less pleasant when there are thousands and every visitor receives all of them.

Add a page size and a way to ask for the next page. Choose a stable ordering, and explain what happens if books are added while a reader is moving through the results. Offset and cursor approaches have different trade-offs; the important first step is making the behaviour predictable.

Now look at the database query. Does it have an index that supports the filter and order? Pagination limits the response size, but an inefficient query can still do a lot of work before producing that small response.

Make retries safe for order creation

Here's a more expensive problem. The customer places an order, the server saves it, and the response gets lost. The customer retries. Have we just created two orders?

An idempotency key can identify those attempts as the same intended operation. The server needs to record and enforce that relationship, including concurrent retries. A header called Idempotency-Key doesn't achieve anything if the implementation ignores it.

If the customer never received the response, how would they know whether the first request succeeded?

Design the retry behaviour before trusting the happy path. It is much easier to decide how repeated requests work now than to explain a duplicate order afterwards.

Choose another API style when the project needs it

REST, GraphQL, gRPC, SOAP, and WebSockets appear on the roadmap because applications have different communication needs. You don't need to build the shop five times on day one.

A resource-oriented HTTP API is a reasonable starting point. Later, compare GraphQL when clients need different combinations of data, gRPC for suitable service-to-service calls, or WebSockets when a feature needs ongoing two-way communication. Keep each experiment tied to a requirement you can explain.

The same goes for frameworks. Use one you know, whether that's Django, FastAPI, Spring Boot, or a Node.js framework. Understanding the request, permissions, and failure behaviour will transfer when you change tools.

Measure, then add the next layer

Once people use the shop, measure request times, errors, database work, and response sizes. Those measurements help you decide whether caching, indexing, rate limiting, or more capacity will make a difference.

Gateways and load balancers become useful as traffic and services grow. Background jobs help when work doesn't need to finish inside the request. Webhooks can notify another system when something happens, while polling asks it to check repeatedly. Each adds behaviour you should be able to test, observe, and explain.

What should you have at the end?

You should have a small API that another developer can use without sitting beside you. It should handle ordinary requests, bad input, access checks, pagination, retries, and documented errors.

That's a solid foundation for the rest of the roadmap. Keep extending the shop with one real requirement at a time. When you can explain why a feature exists, show it working, and demonstrate how it fails, you're learning API design rather than collecting framework names.

Harry Potter scene

Honestly, read the docs.

Harry Potter · Tenor · Adapted dialogue

Try it yourself

Your order endpoint saves an order, but the client times out before receiving the response. It retries with the same idempotency key. What should happen, and what concurrent case should you test?

A possible solution

The retry should resolve to the same intended order rather than create a second one. The server must associate the key with the caller and request, and enforce that association while recording the result. Test two requests with the same key arriving at the same time, not only a retry after the first request finishes. Also reject or otherwise handle reuse of the key with a different payload according to your documented contract.

Share this article with your friends

Know someone who would enjoy this? Send it their way.

WhatsAppLinkedInXEmail