Pragmatic REST API design practices
Chamoda Pandithage
Nov 24, 2023
pragmatic-rest-api-design-practices

Designing REST APIs is one of the core aspects of platform development today. With emerging microservices architectures and more and more publicly consumed APIs being available for use, designing pragmatic APIs with best practices is more prevalent than ever. In this post, we're exploring pragmatic API design. This is a non-exhaustive list of rules you can consider during API design.

Rule 1: Use plural nouns for collections

This is just an arbitrary convention, but it's well-established to consistently stick to a common convention.

# BAD
GET /user/{user_id}

# GOOD
GET /users
GET /users/{user_id}

Rule 2: Don't add unnecessary path segments

A common mistake is trying to build your relational model into your URL structure. {item_id} is globally unique so there's no reason for {user_id} to exist in the path.

# BAD
GET /users/{user_id}/items/{item_id}

# GOOD
GET /items/{item_id}

However, there can be situations that compound URLs make sense. When keys are not globally unique, it makes sense to use path segments.

# When {option_id} is not globally unique
GET /configs/{config_id}/options/{option_id}

Rule 3: Don't add .json or any other extensions to end of URL

Content type should be determined from headers (Accept, Accept-Charset, Accept-Encoding, Accept-Language) to negotiate representations.

Rule 4: Don't return arrays as top-level responses

The top-level response from an endpoint should always be an object, never an array. The reason is it's very hard to make backward-compatible changes when you return an array. Objects let you make additive changes to the endpoint easily.

# BAD
GET /items
[{...item1}, {...item2}]

# GOOD
GET /items
{"data": [{...item1}, { ...item2}]}

Rule 5: Use strings for all identifiers

Always use strings as object identifiers, even when your internal representation is numeric. Strings are incredibly flexible and that can be helpful in future migrations. It also makes it easier for clients using typed languages to always use strings without thinking about which type to use.

# BAD
GET /items/{item_id}
{"id": 123}

# GOOD
GET /items/{item_id}
{"id": "123"}

Rule 6: Be consistent

Keep field names consistent among objects with similar meanings. For example, if you use a country_name field in one endpoint and use a country field in another endpoint, but both mean the same thing, that's bad.

Rule 7: Use a structured error format

This is more relevant if you are building large-scale REST services. You will save a lot of headaches by establishing a standard error format upfront.

{
  "message": "You do not have permission to access this resource",
  "type": "Unauthorized",
  "types": ["Unauthorized", "Security"],
  "cause": {...recurse for nested any exceptions}
}

Rule 8: Implement idempotence mechanisms to prevent duplicates

Idempotence is the property of an operation such that if you execute it more than once, it doesn't change the result. GET, PUT, DELETE operations are idempotent.

POST is not idempotent. Let's say if you accidentally submit an order multiple times, which results in duplicate orders.

# Submitting for the first time
POST /orders
{"items": [...items]}
201
{"id": 123, ...}

# Submitting again due to network error
POST /orders
{"items": [...items]}
201
{"id": 124, ...}

One way to fix this is by using a client generated Idempotency-Key per unique request and add it as a header of the request. With an appropriate backend implementation, this will protect against duplication.

# Submitting for the first time
POST /orders
Idempotency-Key: 3affc3ac-61db-4ca6-828f-025715550bd
{"items": [...items]}
201 CREATED
{"id": 123, ...}

# Submitting again due to network error
POST /orders
Idempotency-Key: 3affc3ac-61db-4ca6-828f-025715550bd
{"items": [...items]}
409 CONFLICT
{"message": "This is a duplicate"}

Rule 9: Use ISO8601 strings for timestamps

Human readability matters. Always use strings for timestamps, not numbers like Unix timestamps.

Always use ISO8601 for all date and time-related values.

Finally, we've covered a very important set of rules for good REST API design. Most of these rules ensure that API consumers avoid common pitfalls when using APIs. Some rules are only applicable when platforms scale to a certain extent, but they're still worth knowing.