REST API Best Practices: A Guide Based On Many Books

Table of contents

REST API Best Practices: A Guide Based On Many Books

How to design a REST API? How many levels to nest resources? Relative or full URLs?

This post compiles REST API design recommendations from books and articles. Sources are at the end.

Check basic features of a REST API for context. I’ll focus on design choices here.

A REST API can return other formats, but I’ll use JSON for examples since it’s common.

Start with JSON response structure.

JSON response structure

No single structure is correct. Pick one and stay consistent across your API.

JSON:API

JSON:API standardizes responses for single and multiple resources.

{
    "products": [{
        "id": 1,
        "title": "title"
    }]
}

Twitter style API

An individual resource response looks like this:

{
  "id":1,
  "title":"title"
}

For multiple resources, Twitter returns an array:

[
  {
    "id":1,
    "title":"title"
  },
  {
    "id":2,
    "title":"title"
  }
]

Facebook style API

Individual resources look the same as Twitter:

{
    "id": 1,
    "title": "title"
}

Multiple resources go in a data array:

{
  "data":[
    {
      "id": 1,
      "title": "title"
    },
    {
      "id": 2,
      "title": "title"
    }
  ]
}

Any of these works as long as you’re consistent and document the structure.

Relative or full URLs in HATEOAS?

HATEOAS lets clients navigate via links. There’s no consensus on relative vs full URLs. Microsoft uses full URLs, which is worth considering.

{"rel":"self",
    "href":"https://adventure-works.com/customers/2"}

Nested objects in responses

When returning related resources (one-to-one, one-to-many, many-to-many), you can either include them or just return IDs.

Return IDs

Return only IDs and let clients fetch related resources separately. Good for large or rarely used data.

{
    "posts": [{
        "id": 1,
        "title": "title",
        "comments": [2,3,4]
    }]
}

Watch out for n+1 queries - fetch related data in batch when possible.

GET /comments/2,3

Return full objects

Include related objects in the response to avoid extra requests. Larger responses but fewer round trips.

{
    "posts":[
      {
        "id":1,
        "title":"title",
        "comments":[
          {
            "id":2,
            "text":"..."
          },
          {
            "id":3,
            "text":"..."
          },
          {
            "id":4,
            "text":"..."
          }
        ]
      }
    ]
  }

For flexibility, allow clients to request embeds:

GET /posts/1?embed=comments

Pagination

Don’t return entire datasets. For large collections, paginate with a fixed page size.

Include in your response:

  • Total items
  • Items per page
  • Total pages
  • Current page
  • Previous page URL (if any)
  • Next page URL (if any)
{
    "data": [
         {}
    ],
    "pagination": {
        "total": 60,
        "items_per_page": 12,
        "current_page": 1,
        "total_pages": 5,
        "next_url": "https://api.example.com/items?page=2",
    }
}

API versioning

APIs change over time. Versioning helps consumers adapt.

Version unless your API is extremely simple and stable, or changes only by adding new endpoints without modifying existing ones. When in doubt, version it.

Options (not all strictly RESTful):

URL

Most common. Simple, but hard to update for clients storing URLs and harder to split across servers. Not strictly REST.

http://example.com/api/v1/

Domain

Use a versioned subdomain. Similar drawbacks. Not strictly REST.

http://apiv1.example.com

URL or body parameters

Keep the same URL, change a parameter. Not strictly REST.

http://example.com/resource?version=1
POST /places HTTP/1.1
Host: api.example.com
Content-Type: application/json

{
    "version": "1.0"
}

HTTP headers

Keep URLs the same, but caching can be tricky. Add Vary: ApiVersion to avoid cache issues.

GET /resources HTTP/1.1
Host: example.com
ApiVersion: 1.0
Vary: ApiVersion

Content negotiation

Uses Accept header like format negotiation. REST-compliant since resources and representations are separate. Can confuse developers unfamiliar with HTTP.

Accept: application/vnd.github.v3+json

Resource nesting

Deep nesting gets unwieldy. Avoid it:

# Avoid
/clients/99/orders/88/products/77/variants

Limit to one or two levels. For deeper relationships, reference by ID directly:

# Better
/orders/88/products/77

DRF , White House API standards , and Microsoft all recommend keeping URIs simple. Hierarchical URLs can help SEO in some cases, but readability matters more for APIs.

Notifying about API changes

For breaking changes, notify users early and often (email, blog, etc.).

When deprecating an endpoint, return HTTP 410 (Gone) with a message linking to the new endpoint, updated docs, and an explanation if available.

Throttling

Limit requests to prevent abuse, reduce load, and control costs. Add a throttling policy for all users. You can also tie limits to paid plans.

URI characters

Follow RFC 3986 :

  • Unreserved [0-9a-zA-Z - . _ ~]: no encoding
  • Reserved [: / ? # [ ] @ ! $ & ’ ( ) * + , ; =]: encode if used for other than intended purpose
  • Others: encode
  • Unsafe [< > % { } | \ ^ `]: encode

Standards evolve for international characters. See W3C’s IDN and IRI article .

SEO in URLs

If search ranking matters, include descriptive text in URLs:

/posts/post-title ✅
/posts/99-post-title ✅
/posts/99 ❌

I hope this was useful.

Reference sources

Eduardo Zepeda
Eduardo Zepeda
Just call me Ed. developer and Linux (GNU/Linux) preacher. I like boring and predictable frameworks, the more opinionated and less boilerplate the better. I think Django is the best framework for MVPs. I used to believe in the goodness of cryptocurrencies outside of monetary speculation.