5 Steps to a Well-Designed REST API

A practical approach to designing REST APIs

Jonathan Manera
7 min readJul 16, 2022
Photo by Scott Graham on Unsplash

Unless you are an experienced engineer, it could be difficult sometimes to correctly design an API from scratch. In this article, we will explore 5 steps to a well-designed REST API for most use cases.

NOTE: REST terminology is in italics, and each term is described in a glossary at the end of the article in order of appearance.

1. Design the URI around resources

An URI describes the resource where operations will be applied.

To represent resources, we use names in plural

Most of the times, a collection of business entities is provided; hence, the semantic representation of the collection must be plural. The example below shows a collection of entities User and Role.

/users
/roles

To represent a single resource, we use path params in kebab-case

To represent a single resource, we must identify it in a collection through an ID. The resource identifier should be expressed in ASCII kebab-case. The example below shows a User with the identifier user-id.

/users/{user-id}

To represent a subset of resources, we use query params in snake_case

Query params should be used as filter parameters on resources. The resource filter should be expressed in ASCII snake_case. The example below shows all users filtered by their non-unique properties last_name and first_name.

/users?first_name={first-name}&last_name={last-name}

To represent sub-resources, we use path segments

Some resources may contain sub-resources. These sub-resources are represented by path segments or “directories” to the right of the higher-level resource. The example below shows the collection of entities Role linked to a particular User.

/users/{user-id}/roles

2. Choose the VERB based on the intended operation

Verbs describe the operation that will be performed on the server side.

To CREATE a resource, we use the verb POST

In CREATE operations, it is important to verify if there are unique constraints to be validated on the server side in order to avoid resource duplication. This will dictate whether it is an idempotent POST or not.

In an idempotent operation, if N requests are sent to create a single resource, then N resources will be created. But let’s imagine that we want to create an entity User that requires an e-mail address and, as a business rule, e-mail addresses must be unique. If we send the same request twice, the second one will return an error because the user with the given e-mail already exists.

To READ resources, we use the verb GET

GET operations must be “safe”, meaning they only fetch information and do not make changes on the server side.

To MODIFY or update a resource partially, we use the verb PATCH

In PATCH operations, a subset of optional properties is sent within the payload to be updated. Avoid replacing missing properties with null.

To UPDATE a resource (or to create it), we use the verb PUT

Unlike PATCH , in PUT operations the resource is replaced. Check if you have a payload with the entire representation of the resource. If the resource does not exist, it may be necessary to create it.

To DELETE a resource, we use the verb DELETE

In DELETE operations, sometimes the resource must be completely removed from the source (hard delete), but in many other cases, we just want to inactivate it (soft delete). Soft deletes involve updating a column such as is_active , from true to false.

Complete Example:

GET     /users
POST /users
GET /users/{user-id}
PATCH /users/{user-id}
PUT /users/{user-id}
DELETE /users/{user-id}

GET /users/{user-id}/roles
POST /users/{user-id}/roles
GET /users/{user-id}/roles/{role-id}
PATCH /users/{user-id}/roles/{role-id}
PUT /users/{user-id}/roles/{role-id}
DELETE /users/{user-id}/roles/{role-id}

3. Design JSON payloads around business entities

The payload must use a JSON object as a full or partial representation of the business entity.

To represent property names, we may use snake_case

Property names are often expressed in ASCII snake_case. The first character must be a lower-case letter, or in some cases, an underscore.

"first_name": …
"last_name": …

To represent array names, we use plural

Names of arrays should be pluralized to indicate that they contain multiple entities.

"users": […]
"roles": […]

To represent date-time property names, we may use the suffix _at

Date and date-time property names should end with “_at”, to distinguish them from other properties.

"created_at": …
"deleted_at": …

To represent date and time, we use the ISO format

To represent date and date-time values, we use ISO 8601 format.

"birthday": "1889–04–04"
"created_at": "2021–06–28T18:55:19.000z"

To represent boolean property names, we may use the prefix is_

Boolean property names may start with “is_”, to distinguish them from other properties.

"is_active": …
"is_credential_expired": …

To represent boolean, we use true or false

Boolean values should be true or false. Avoid values such as 0 or 1.

"is_active": true
"is_credential_expired": false

To represent enum property values, we use UPPER_SNAKE_CASE

Enumerations should be represented as strings restricted to ASCII UPPER_SNAKE_CASE.

"key_name": "ROLE_USER"
"key_name": "ROLE_ADMIN"

To represent null required properties, we use “null”

Properties marked as required and nullable are represented as null.

{ "example": null }

To represent null non-required properties, we do not include the property

Properties marked as nullable but non-required are represented as an absent property.

{}

To represent empty arrays, we use an empty list

Empty arrays should never be represented as null but as empty lists.

"users": []

Complete Example:

{
"users": [
{
"id": 1,
"email": "john.foo@example.com",
"first_name": "John",
"last_name": "Foo",
"birthday": "1889–04–04",
"created_at": "2021–06–28T18:55:19.000z",
"is_active": true,
"is_credential_expired": false,
"roles": [
{
"id": 1,
"key_name": "ROLE_USER"
}
]
},
{
"id": 2,
"email": "jane.bar@example.com",
"first_name": "Jane",
"last_name": "Bar",
"birthday": "1889–04–04",
"created_at": "2021–06–28T18:55:19.000z",
"is_active": true,
"is_credential_expired": false,
"roles": [
{
"id": 1,
"key_name": "ROLE_USER"
},
{
"id": 2,
"key_name": "ROLE_ADMIN"
}
]
}
]
}

4. Return status codes consistent with their semantics

We use standardized HTTP status codes consistent with their semantics.

Common status codes to represent success

  • 200 Ok — Standard response for success, usually when a body is provided.
  • 201 Created — The business entity was successfully created.
  • 202 Accepted — The request was successful and will be processed any time soon.
  • 204 No Content — The request was successful, but no body was provided.
  • 207 Multi-status — The response contains multiple status information for different operations (usually in batch/bulk processing).

Common status codes to represent client-side errors

  • 400 Bad request — One or more fields failed in the business logic validation.
  • 401 Unauthorized — The client must be authenticated first to perform the operation.
  • 403 Forbidden — The client was authenticated, but does not have permission to perform the operation.
  • 404 Not Found — The resource was not found or does not exist.
  • 405 Method Not Allowed — The operation was not implemented for the resource.
  • 406 Not Acceptable — The content provided is not acceptable according to the Accept header.
  • 408 Request timeout — Server timed out on the operation.
  • 409 Conflict — The operation cannot be completed due to a conflict, such as when there are concurrent and conflicting updates.
  • 415 Unsupported Media Type — The content provided is not supported according to the Content-Type header.
  • 423 Locked — Another operation is being processed on the resource.
  • 429 Too many requests — The request limit on a resource has been exceeded (usually to prevent brute force attacks).

Common status codes to represent server-side errors

  • 500 Internal Server Error — A generic error for an unexpected server error. This type of error is usually caused by unhandled exceptions and may be permanent.
  • 501 Not Implemented — The server cannot fulfill the request (it usually implies future availability).
  • 503 Service Unavailable — the operation is temporarily unavailable. The client should try again.

5. Make your changes compatible

As business requirements change, new resources may be added, relationships between resources may be altered, and the data structure of resources may change. As developers, we have full control over the design of the API, but we do not have that degree of control over how the API is used. So, we must allow client applications to continue to work until they adapt to the new features.

URI versioning

Versioning allows us to indicate the resources that the API exposes to, so client applications can perform operations targeting a specific version of a resource. Based on the Semantic Versioning specification, we divide these changes into two main categories:

  • MINOR changes — Backwards compatible changes.
    • New optional parameter.
    • New response fields.
    • New endpoint.
  • MAJOR changes — Incompatible API changes (breaking changes).
    • New required parameters.
    • Removal of existing parameters.
    • Removal of response properties.
    • Change in parameter name or type.
    • Deprecation of a service.

For example, if optional fields are added to the entity User, the version of the resource could be represented by a URI containing an increment in the MINOR version number.

/v1.0/users
/v1.1/users

Backward compatible changes are the easiest to make because they don’t affect clients, and there is usually no need to maintain older versions.

On the other hand, if mandatory parameters are changed, the version of the resource could be represented by a URI containing an increment in the MAJOR version number.

/v1/users
/v2/users

Glossary

  • Verb/Verbs: HTTP methods.
  • Resource/Resources: A business entity.
  • Idempotent: An operation that has the same intended effect on the server state, regardless of whether it is executed once or multiple times.
  • Payload: The message body of the request, usually in JSON format.
  • Property/Properties: JSON data written as name/value pairs.
  • URI: Uniform Resource Identifier.
  • Sub-resources: Business entities part of a higher-level entity.

Thanks for reading. I hope this was helpful!

--

--

Jonathan Manera

If you wish to make a Java app from scratch, you must first invent the universe.