API Design Guide

Tohid haghighi
8 min readFeb 6, 2023

--

API Design is the first and the most important part of an API’s Lifecycle. A well-designed API is easy, and intuitive to use.

For someone who is just starting their journey in API design, this API Design Guide collection will work as a reference to help them design simple, consistent and easy-to-use REST APIs.

GET Fetch all cats

{{url}}/cats
 Example Request:

curl --location -g --request GET '{{url}}/cats'

Example Response

{
"data": [
{
"id": 1,
"name": "Taco",
"breed": "Abyssinian",
"owner": {
"id": 1,
"name": "John Doe"
}
},
{
"id": 2,
"name": "Snowball",
"breed": "Birman",
"owner": {
"id": 12,
"name": "Stuart Little"
}
}
]
}

GET Fetch a particular cat

{{url}}/cats/:id
Example Request:

curl --location -g --request GET '{{url}}/cats/1'

Example Response:

{
"data": {
"id": 2,
"name": "Snowball",
"breed": "Birman",
"owner": {
"id": 12,
"name": "Stuart Little"
}
}
}

HTTP methods

Operations MUST use the proper HTTP methods whenever possible, and operation idempotency MUST be respected. HTTP methods are frequently referred to as the HTTP verbs. The terms are synonymous in this context, however, the HTTP specification uses the term method.

Below is a list of methods that REST services should support. Not all resources will support all methods, but all resources using the methods below MUST conform to their usage.

HTTP methods

POST Add new cat

{{url}}/cats
Example Request:

curl --location -g --request POST '{{url}}/cats' \
--header 'Content-Type: application/json' \
--data-raw '{
"name": "Taco",
"breed": "Abyssinian",
"owner": 1
}'

Example Response:

{
"data": {
"id": 4,
"name": "Taco",
"breed": "Abyssinian",
"owner": {
"id": 1,
"name": "John Doe"
}
}
}

PUT Update cat details

{{url}}/cats/:id
Example Request:

curl --location -g --request PUT '{{url}}/cats/1' \
--header 'Content-Type: application/json' \
--data-raw '{
"name": "Fury",
"breed": "Abyssinian",
"owner": 1
}'

Example Response:

{
"data": {
"id": 1,
"name": "Fury",
"breed": "Abyssinian",
"owner": {
"id": 1,
"name": "John Doe"
}
}
}

DEL Delete cat details

{{url}}/cats/:id
Example Request:

curl --location -g --request DELETE '{{url}}/cats/1'

Query parameters

It’s best to keep the base resource URLs as lean as possible. Complex result filters, sorting requirements and advanced searching (when restricted to a single type of resource) can all be easily implemented as query parameters on top of the base URL.

In some cases, you can also use query params to populate or filter out information from a response. Also whatever query parameters you decide to use, keep it consistent across your endpoints.

Paginatation

It is almost never a good idea to return all resources of your database at once. Consequently, you should provide a pagination mechanism. A really simple approach is to use the parameters offset and limit, which are well-known from databases.

GET Fetch cats

{{url}}/cats?offset=1&limit=10
Example requests:

curl --location -g --request GET '{{url}}/cats?offset=1&limit=3'

Example response:

{
"data": [
{
"id": 1,
"name": "Taco",
"breed": "Abyssinian",
"owner": {
"id": 1,
"name": "John Doe"
}
},
{
"id": 2,
"name": "Snowball",
"breed": "Birman",
"owner": {
"id": 12,
"name": "Stuart Little"
}
},
{
"id": 3,
"name": "Fury",
"breed": "Birman",
"owner": {
"id": 12,
"name": "Stuart Little"
}
}
],
"pagination": {
"offset": 1,
"limit": 3,
"total": 123
}
}

Searching

When full text search is used as a mechanism of retrieving resource instances for a specific type of resource, it can be exposed on the API as a query parameter on the resource’s endpoint. Let’s say q. Search queries should be passed straight to the search engine and API output should be in the same format as a normal list result.

GET Search cats with name

{{url}}/cats?q={{query}}
Example request:

curl --location -g --request GET '{{url}}/cats?q=snow'

Example response:

{
"data": [
{
"id": 1,
"name": "Snowy",
"breed": "Abyssinian",
"owner": {
"id": 1,
"name": "John Doe"
}
},
{
"id": 2,
"name": "Snowball",
"breed": "Birman",
"owner": {
"id": 12,
"name": "Stuart Little"
}
}
]
}

Filtering

Use a unique query parameter for each field that implements filtering. For example, when requesting a list of tickets from the /cats endpoint, you may want to limit these to only those with color white. This could be accomplished with a request like GET /cats?breed=birman. Here, the breed is a query parameter that implements a filter

GET Filter cats by breed

{{url}}/cats?breed={{breedName}}
Example request:

curl --location -g --request GET '{{url}}/cats?breed=birman'

Example response:

{
"data": [
{
"id": 1,
"name": "Sully",
"breed": "Birman",
"owner": {
"id": 1,
"name": "John Doe"
}
},
{
"id": 2,
"name": "Snowball",
"breed": "Birman",
"owner": {
"id": 12,
"name": "Stuart Little"
}
}
]
}

Sorting

A generic parameter sort can be used to describe sorting rules. Accommodate complex sorting requirements by letting the sort parameter take in a list of comma separated fields, each with a possible unary negative to imply descending sort order.

GET Fetch cats, sorted by name (ascending)

{{url}}/cats?sort=name
Example request:

curl --location -g --request GET '{{url}}/cats?sort=name'

Example response:

{
"data": [
{
"id": 5,
"name": "Amy",
"breed": "Birman",
"owner": {
"id": 12,
"name": "Stuart Little"
}
},
{
"id": 2,
"name": "Snowball",
"breed": "Birman",
"owner": {
"id": 12,
"name": "Stuart Little"
}
},
{
"id": 1,
"name": "Taco",
"breed": "Abyssinian",
"owner": {
"id": 1,
"name": "John Doe"
}
}
]
}

GET Fetch cats, sorted by name (descending)

{{url}}/cats?sort=-name
Example request:

curl --location -g --request GET '{{url}}/cats?sort=-name'

Example response:

{
"data": [
{
"id": 1,
"name": "Taco",
"breed": "Abyssinian",
"owner": {
"id": 1,
"name": "John Doe"
}
},
{
"id": 2,
"name": "Snowball",
"breed": "Birman",
"owner": {
"id": 12,
"name": "Stuart Little"
}
},
{
"id": 5,
"name": "Amy",
"breed": "Birman",
"owner": {
"id": 12,
"name": "Stuart Little"
}
}
]
}

Status codes

When the client raises a request to the server through an API, the client should know the feedback, whether it failed, passed or the request was wrong. HTTP status codes are a bunch of standardized codes which has various explanations in various scenarios. The server should always return the right status code.

  • 200 OK — Response to a successful GET, PUT, PATCH or DELETE. Can also be used for a POST that doesn’t result in a creation.
  • 201 Created — Response to a POST that results in a creation. Should be combined with a Location header pointing to the location of the new resource
  • 204 No Content — Response to a successful request that won’t be returning a body (like a DELETE request)
  • 304 Not Modified — Used when HTTP caching headers are in play
  • 400 Bad Request — The request is malformed, such as if the body does not parse
  • 401 Unauthorized — When no or invalid authentication details are provided. Also useful to trigger an auth popup if the API is used from a browser
  • 403 Forbidden — When authentication succeeded but authenticated user doesn’t have access to the resource
  • 404 Not Found — When a non-existent resource is requested
  • 405 Method Not Allowed — When an HTTP method is being requested that isn’t allowed for the authenticated user
  • 410 Gone — Indicates that the resource at this end point is no longer available. Useful as a blanket response for old API versions
  • 415 Unsupported Media Type — If incorrect content type was provided as part of the request
  • 422 Unprocessable Entity — Used for validation errors
  • 429 Too Many Requests — When a request is rejected due to rate limiting
  • 500 Internal Server Error — This is either a system or application error, and generally indicates that although the client appeared to provide a correct request, something unexpected has gone wrong on the server
  • 503 Service Unavailable — The server is unable to handle the request for a service due to temporary maintenance

Versioning

Always version your API. Versioning helps you iterate faster and prevents invalid requests from hitting updated endpoints. It also helps smooth over any major API version transitions as you can continue to offer old API versions for a period of time.

There are mixed opinions around whether an API version should be included in the URL or in a header. Academically speaking, it should probably be in a header. However, the version needs to be in the URL to ensure browser explorability of the resources across versions (remember the API requirements specified at the top of this post?).

An API is never going to be completely stable. Change is inevitable. What’s important is how that change is managed. Well documented and announced multi-month deprecation schedules can be an acceptable practice for many APIs. It comes down to what is reasonable given the industry and possible consumers of the API.

GET Fetch all cats (v2)

{{url}}/v2/cats
Example request:

curl --location -g --request GET '{{url}}/v2/cats'

Example response:

{
"data": [
{
"id": 1,
"name": "Taco",
"characteristics": {
"breed": "Abyssinian",
"color": "black"
},
"owner": {
"id": 1,
"name": "John Doe"
}
},
{
"id": 2,
"name": "Snowball",
"characteristics": {
"breed": "Birman",
"color": "white"
},
"owner": {
"id": 12,
"name": "Stuart Little"
}
}
]
}

Errors

Just like an HTML error page shows a useful error message to a visitor, an API should provide a useful error message in a known consumable format. The representation of an error should be no different than the representation of any resource, just with its own set of fields.

The API should always return sensible HTTP status codes. API errors typically break down into 2 types: 400 series status codes for client issues & 500 series status codes for server issues. At a minimum, the API should standardize that all 400 series errors come with consumable JSON error representation. If possible (i.e. if load balancers & reverse proxies can create custom error bodies), this should extend to 500 series status codes.

A JSON error body should provide a few things for the developer — a useful error message, a unique error code (that can be looked up for more details in the docs) and possibly a detailed description. JSON output representation for something like this would look like:

{
"code" : 1234,
"message" : "Something bad happened :(",
"description" : "More details about the error here"
}

Validation errors for PUT, PATCH and POST requests will need a field breakdown. This is best modeled by using a fixed top-level error code for validation failures and providing the detailed errors in an additional errors field, like so:

{
"code" : 1024,
"message" : "Validation Failed",
"errors" : [
{
"code" : 5432,
"field" : "first_name",
"message" : "First name cannot have fancy characters"
},
{
"code" : 5622,
"field" : "password",
"message" : "Password cannot be blank"
}
]
}

POST Add new cat

{{url}}/cats
Example request:

curl --location -g --request POST '{{url}}/cats' \
--header 'Content-Type: application/json' \
--data-raw '{
"breed": "Abyssinian",
"owner": 1
}'

Example response:

{
"code": 123,
"name": "VALIDATION_FAILED",
"errors": [
{
"field": "name",
"message": "Name cannot be undefined or NULL."
}
]
}

--

--

Tohid haghighi

Full-Stack Developer | C# | .NET Core | Vuejs | TDD | Javascript