← Back to blog Khalil Drissi

REST API design guidelines

Listen to article
0:00

An API is a promise. Once a client depends on it, every quirk you shipped becomes permanent, because someone out there wrote code against that quirk. I have maintained APIs for years and the ones that aged well all shared the same trait: they were boring and predictable. Here is how I get there.

Model resources as nouns, use verbs from HTTP

The URL should name a thing. The method says what you are doing to it. I see endpoints like /getUser and /createOrderNow constantly, and they fight the whole point of HTTP. A clean design uses plural nouns for collections and lets the method carry the action.

GET    /orders          list orders
POST   /orders          create an order
GET    /orders/42       fetch one order
PATCH  /orders/42       update some fields
DELETE /orders/42       remove it

GET    /orders/42/items nested resource

PATCH for partial updates and PUT for full replacement is a distinction worth keeping. Most real updates touch a few fields, so PATCH is what I reach for, and PUT becomes the rare case where the client genuinely owns the entire representation.

Use status codes the way clients expect

Return the status code that matches reality. A 200 on a failed request because “we put the error in the body” breaks every generic client and monitoring tool that reads the status line. The set I use covers almost everything:

Make errors machine-readable

An error body should help the calling code react, not just print a string. I return a stable machine code alongside a human message, so clients can branch on the code without parsing prose that I might reword later.

{
  "error": {
    "code": "insufficient_funds",
    "message": "The card was declined.",
    "field": "payment_method"
  }
}

Consistency here matters more than cleverness. Every error in the API should have the same shape, so a client writes one error handler instead of ten. This is the same instinct I bring to logging, which I wrote about in logging and observability best practices: structure beats prose when something else has to read it.

Plan for versioning before you need it

You will need to make a breaking change eventually. Decide how before you ship v1. I put the version in the path, /v1/orders, because it is visible, cacheable, and trivial to route. Header-based versioning is more elegant on paper and more annoying in practice when someone is debugging with curl. Whatever you pick, the rule is that you never break an existing version. Additive changes like new optional fields are fine. Removing a field or changing its type is a new version.

Paginate and filter from day one

Any collection that can grow will grow, and a GET that returns ten thousand rows will eventually time out and take a database with it. I add pagination to every list endpoint at the start, even when the data is tiny, because retrofitting it later is a breaking change. Cursor-based pagination handles large, shifting datasets better than offset, since offset drifts when rows are inserted mid-scroll.

Be strict about what you accept, generous about what you return

Validate input hard at the boundary and reject anything malformed with a clear 400 or 422. The closer to the edge you catch bad data, the less it can corrupt downstream, which ties straight back to the constraints I rely on in database schema best practices. On the output side, keep responses stable and predictable so clients can trust the shape. An API that is strict at the door and consistent on the way out is one people enjoy building against, and that good will is what gets your platform adopted.

Comments
Leave a comment