On this page

The ten practices at a glance

  1. Model resources, not actions
  1. Use HTTP methods the way they were meant to be used
  1. Return the right status code
  1. Keep naming consistent
  1. Paginate every collection
  1. Version only on breaking changes
  1. Make writes safe to retry
  1. Return errors a program can read
  1. Publish your rate limits
  1. Generate documentation from the schema

Design for the client that already exists

Frequently asked questions

Final words

Related Reading

API Design Best Practices: 10 Rules for Clean, Scalable APIs

Image
Arslan Ahmad
Ten API design best practices with examples: resource naming, HTTP methods, status codes, pagination, versioning, idempotency, rate limits, and error shapes.
Image

The ten practices at a glance

  1. Model resources, not actions
  1. Use HTTP methods the way they were meant to be used
  1. Return the right status code
  1. Keep naming consistent
  1. Paginate every collection
  1. Version only on breaking changes
  1. Make writes safe to retry
  1. Return errors a program can read
  1. Publish your rate limits
  1. Generate documentation from the schema

Design for the client that already exists

Frequently asked questions

Final words

Related Reading

Good API design comes down to ten rules. Model resources rather than actions. Use HTTP methods and status codes for their real meaning. Keep naming consistent, and paginate every list. Version only on breaking changes, and make writes safe to retry. Return structured errors, publish your rate limits, and generate docs from the schema. The tenth rule sits behind the others: design for the client that already exists.

The rest of this guide takes each one, shows what it looks like in a real request, and names the failure it prevents.

The ten practices at a glance

#PracticeThe failure it prevents
1Model resources, not actionsAn endpoint per feature, with no pattern to learn
2Use HTTP methods correctlyClients cannot tell what is safe to retry
3Return the right status codeErrors that look like successes
4Keep naming consistentEvery endpoint has to be read before it can be used
5Paginate every collectionOne request returns a million rows
6Version only on breaking changesVersion sprawl, or silent client breakage
7Make writes idempotentA retry charges the customer twice
8Return structured errorsClients parse English to decide what to do
9Publish rate limitsClients hammer you and get cut off with no warning
10Generate docs from the schemaDocs that describe last quarter's API

1. Model resources, not actions

Start from the nouns in the system, not the buttons in the app. A resource is a thing worth naming: a customer, an order, a payment.

Good:  POST /orders          Bad:  POST /createOrder
       GET  /orders/42             GET  /fetchOrderById?id=42
       DELETE /orders/42           POST /deleteOrder

The reason is learnability. Once a developer sees two resources, they can guess the third. An endpoint per feature has no pattern, so every call has to be looked up.

Relationships become paths. GET /customers/42/orders reads better than GET /orders?customerId=42 when the orders only exist in the context of that customer.

2. Use HTTP methods the way they were meant to be used

Each method carries a promise, and clients, proxies, and caches rely on it.

MethodPurposeSafe to repeat?
GETRead, never change anythingYes
POSTCreate, or run a non repeatable actionNo
PUTReplace the whole resourceYes
PATCHUpdate part of a resourceUsually not
DELETERemove the resourceYes

A GET that changes data is the worst offender. A browser prefetch or a crawler will call it, and something will change that nobody asked for.

Safe to repeat is the property called idempotent. It means a second identical call leaves the same state as the first.

3. Return the right status code

The status code is the first thing a client branches on. Getting it wrong forces every client to parse the body instead.

  • 200 succeeded, here is the result.
  • 201 created, with a Location header pointing at the new resource.
  • 204 succeeded, nothing to return.
  • 400 the request was malformed.
  • 401 we do not know who you are.
  • 403 we know who you are, and the answer is no.
  • 404 no such resource.
  • 409 a conflict, such as a duplicate or a stale update.
  • 422 the syntax was fine but the values were not.
  • 429 too many requests.
  • 500 we failed.

Never return 200 with an error inside the body. Monitoring, retry logic, and caches all read the status code, and they will all be wrong.

4. Keep naming consistent

Pick one convention and apply it everywhere. The specific choice matters less than the consistency.

  • Plural nouns for collections: /orders, not /order.
  • One case style for fields, usually snake_case or camelCase, never both.
  • The same field name for the same idea across every endpoint. If it is created_at on orders, it is created_at on payments.
  • Dates in one format, ISO 8601 with a timezone.
  • The same shape for every list response.

Inconsistency is a tax paid on every integration. A developer who has to check whether this endpoint says userId or user_id is a developer reading your docs instead of shipping.

5. Paginate every collection

An endpoint that returns a list must have a limit, from the first day. A table with fifty rows becomes a table with five million, and the endpoint that returned everything now takes the database down.

Two approaches, and the choice is about the data, not preference.

Offset pagination takes a page number and a size. It is simple and lets a reader jump to any page. It gets slower the deeper you go, because the database still reads and discards the skipped rows. It also duplicates and skips items when the underlying list changes between requests.

Cursor pagination takes a pointer to the last item seen. Cost stays flat at any depth, and new rows arriving do not disturb the sequence. You give up jumping to an arbitrary page.

GET /orders?limit=20&cursor=eyJpZCI6OTkxfQ

{ "data": [ ... ], "next_cursor": "eyJpZCI6OTcxfQ", "has_more": true }

Use cursor for anything that grows while people read it. Use offset for small, stable lists that need page numbers. Cap the page size and document the cap.

6. Version only on breaking changes

Every new version is a version you maintain. Cutting one for every release gives you five live versions within a year, and a bug fix that has to land in all of them.

A change is breaking when a working client stops working: removing a field, renaming one, changing a type, or making an optional input required. A change is not breaking when it only adds: a new endpoint, a new optional input, a new field in a response.

Put the major version in the path, /v1/orders, because it is visible in logs and a gateway can route on it without parsing headers. Then publish a policy: how long an old version lives, and how clients hear about the end date.

Send the news in the traffic, not only in an email:

Deprecation: true
Sunset: Sat, 01 Nov 2027 00:00:00 GMT

7. Make writes safe to retry

Networks drop responses. A client that got no answer will send the request again, and it has no way to know whether the first one worked.

Accept an idempotency key on every create.

POST /payments
Idempotency-Key: 8f14e45f-ea6a-4c1b-9f2b-2a1d4f3c9b77

The server stores the key alongside the result. If the same key arrives again, it returns the original result rather than doing the work twice. Keep the keys for long enough to cover realistic retries, usually a day.

This is the practice that separates a payment API from a demo. It is also the follow up question in most API design interviews.

8. Return errors a program can read

An error body has two audiences. A stable code for the program, and a sentence for the human debugging it.

{ "error": { "code": "insufficient_funds", "message": "The card has insufficient funds.", "field": "amount", "request_id": "req_01H8XK2N" } }

The code is part of the contract, so it must not change once clients branch on it. The message can be reworded freely. The request id is what a support engineer asks for, so return it on every error and log it.

Use the same envelope on every endpoint. A client should write error handling once.

9. Publish your rate limits

A limit the client cannot see is a limit the client will hit. Return the state on every response.

X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 42
X-RateLimit-Reset: 1723300000

When a caller goes over, return 429 with a Retry-After header. A client that knows when to come back will wait. A client that only gets an error will retry immediately and make things worse.

Set different budgets per endpoint. A read and an expensive report should not share a limit.

10. Generate documentation from the schema

Handwritten docs drift from the code within weeks. Write an OpenAPI or Protobuf schema, treat it as the source of truth, and generate the documentation from it.

That schema then earns its keep three more times. It validates requests at the edge, it generates client libraries, and it powers contract tests that fail the build when a response shape changes.

Documentation that is generated is documentation that is correct. Add the parts a generator cannot produce: a getting started page, an authentication walkthrough, and one worked example per common task.

Design for the client that already exists

One rule sits behind the other ten. An API is used far longer than it takes to write, often by clients you cannot update. A mobile app installed today may still be calling you in three years.

That single fact is why additive changes, versioning policies, and idempotency matter more than elegance. Design so the version you ship can keep working while you build the next one.

Frequently asked questions

What are the most important API design best practices?

Model resources rather than actions, use HTTP methods and status codes correctly, paginate every collection, make writes idempotent, and return structured errors. Those five prevent the failures that are expensive to fix later.

What is the difference between API design principles and best practices?

Principles are the goals: consistency, predictability, and backward compatibility. Best practices are the concrete rules that reach them, such as plural nouns for collections or an idempotency key on writes.

Should I use REST or GraphQL for a new API?

REST is the safer default for a public API, because caching, tooling, and browser support are simpler. GraphQL fits when many different clients need different slices of the same data and the round trips are hurting you.

How do I design an API that will not need a version 2?

Only add, never remove or rename. Make new fields optional with defaults, and ask clients to ignore fields they do not recognise. Most breaking changes come from tightening a rule, so leave room before you need it.

What should an API response look like?

Consistent across endpoints. One envelope for lists with the items and a pagination pointer, one envelope for errors with a stable code, and the same field naming everywhere. Predictability matters more than any particular shape.

How do I test that my API design is good?

Hand the docs to someone who has never seen it and ask them to make three calls. Whatever they get stuck on is the design flaw. Guessability is the real measure.

Final words

An API is a promise you have to keep. The practices above are all versions of one idea. Make the contract obvious. Make it hard to misuse. Make it possible to change without breaking anyone who trusted it.

Preparing for an interview rather than a build? Grokking Modern API Design Interview applies these same practices under interview conditions, with a six step method and 15 designs worked end to end.

API
System Design Fundamentals
System Design Interview

What our users say

Steven Zhang

Just wanted to say thanks for your Grokking the system design interview resource (https://lnkd.in/g4Wii9r7) - it helped me immensely when I was interviewing from Tableau (very little system design exp) and helped me land 18 FAANG+ jobs!

Simon Barker

This is what I love about http://designgurus.io’s Grokking the coding interview course. They teach patterns rather than solutions.

Matzuk

Algorithms can be daunting, but they're less so with the right guide. This course - https://www.designgurus.io/course/grokking-the-coding-interview, is a great starting point. It covers typical problems you might encounter in interviews.

More From Designgurus
Annual Subscription
Get instant access to all current and upcoming courses for one year.

Access to 50+ courses

New content added monthly

Certificate of completion

$31.08

/month

Billed Annually

Recommended Course
Grokking the Object Oriented Design Interview

Grokking the Object Oriented Design Interview

60,674+ students

4.2

Learn how to prepare for object oriented design interviews and practice common object oriented design interview questions. Master low level design interview.

View Course
Join our Newsletter

Get the latest system design articles and interview tips delivered to your inbox.

Read More

Google System Design Secrets: Insider Tips and Strategies for Acing Your Interview

Arslan Ahmad

Arslan Ahmad

Top 25 System Design Interview Questions With Detailed Walkthroughs

Arslan Ahmad

Arslan Ahmad

From UUID to Snowflake: Understanding Database Fragmentation

Arslan Ahmad

Arslan Ahmad

System Design Interview Guide (2026): Framework, Questions & How to Prepare

Arslan Ahmad

Arslan Ahmad

Design Gurus logo
One-Stop Portal For Tech Interviews.
Copyright © 2026 Design Gurus, LLC. All rights reserved.