https://github.com/jkitajima/http_api_design
HTTP API Design Guidelines
https://github.com/jkitajima/http_api_design
api api-design http specification
Last synced: 17 days ago
JSON representation
HTTP API Design Guidelines
- Host: GitHub
- URL: https://github.com/jkitajima/http_api_design
- Owner: jkitajima
- License: cc-by-4.0
- Created: 2024-06-11T19:31:43.000Z (over 1 year ago)
- Default Branch: main
- Last Pushed: 2025-03-24T04:48:07.000Z (11 months ago)
- Last Synced: 2025-11-23T18:04:28.942Z (3 months ago)
- Topics: api, api-design, http, specification
- Homepage:
- Size: 64.5 KB
- Stars: 1
- Watchers: 1
- Forks: 1
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# HTTP API Design Guidelines
The purpose of this document is to establish a shared vocabulary and a pragmatic (yet robust) approach to design an HTTP API using [JSON](https://www.json.org) (JavaScript Object Notation) as a data-interchange format.
## Table of Contents
-
Core Design Principles
- An API is a Relationship of Entities
- Entities have shapes
- ID as a Pair
- Entities relationships are nested
- The ID Pair is a Location
- Nested relationship objects are Expandable
- Entities have grammar
- Single Responsibility and Concern Separation
-
Dealing with Collections
-
Request and Response Cycle
- References
## Core Design Principles
### An API is a Relationship of Entities
An API (Application Programming Interface) is represented as a **relationship** of **entities**.
These entities cooperate together in order to perform actions, manage state and transfer representations of entity data.
### Entities have shapes
Clients can interact with entities in two shapes:
1. A single **object**: `/objects/{id}`
2. A **collection** of objects: `/objects`
### ID as a Pair
An object **uniqueness** is defined as a **pair** of values (entity + id) and not a single value (id).
In other words, this does **not** define an object:
```json
{
"id": 1,
"name": "Max"
}
```
Valid forms of defining an object:
```json
{
"entity": "users",
"id": 1,
"name": "Max"
}
```
```json
{
"entity": "dogs",
"id": 1,
"name": "Max"
}
```
### Relationships
There are 3 types of relationship between entities:
- One-to-One (`1-1`)
- One-to-Many (`1-N`)
- Many-to-Many (`M-N`)
This means that, in every type of relationship, an object has possibly:
- A nested object representing a foreign key relationship ("1-sided" relation)
- A location for representing the collection of objects ("M/N-sided" relation)
### Entities relationships are nested (1-sided relation)
Since the single value of an id does not define an object, entities relationships are nested.
In other words, this does not define a relationship:
```json
{
"id": 1,
"name": "Max"
"owner_id": 1
}
```
Valid form of defining a relationship:
```json
{
"entity": "dogs",
"id": 1,
"name": "Max"
"owner": {
"entity": "users",
"id": 1
}
}
```
### The ID Pair is a Location
Now that the uniqueness of object is a pair consisting of both entity and id values, these two values together defines a location inside the API.
`GET /dogs/1`
```json
{
"entity": "dogs",
"id": 1,
"name": "Max"
"owner": {
"entity": "users",
"id": 1
}
}
```
`GET /dogs/1/owner` redirects to `GET /users/1`
```json
{
"entity": "users",
"id": 1,
"name": "Max",
"age": 12,
"citizen_id": "123.456.789-00"
"phone_number": "91234-5678"
}
```
### Nested relationship objects are Expandable
Since relationships are represented as nested objects, they are **expandable**.
`GET /dogs/1?expand=owner`
```json
{
"entity": "dogs",
"id": 1,
"name": "Max"
"owner": {
"entity": "users",
"id": 1,
"name": "Max",
"age": 12,
"citizen_id": "123.456.789-00"
"phone_number": "91234-5678"
}
}
```
> [!IMPORTANT]
> Relationship objects must either return the identification pair (entity + id) **or** the full object data.
>
> **Do not** return partial representation of data.
### Entities have grammar
Entity endpoints are separated by grammar semantics:
- A `noun` endpoint is used for **transferring entity state representations** (`/objects/{id}`)
- A `verb` endpoint is used for performing **actions** (`/objects/{id}/do`)
An example for a `user_files` entity with the following schema:
```yaml
id: uuid
name: string
extension: string
url: string
```
`POST /user_files` will create a new object by transfering a representation:
```json
{
"name": "gopher"
"extension": ".png"
"url": "https://go.dev/blog/gopher"
}
```
While, for a file upload using `multipart/form-data`, an action should be used: `POST /user_files/upload`. Then, after server-side logic was processed, a response object would be:
```json
{
"entity": "user_files",
"id": "13728a84-e1dc-4de6-8f88-f0ba574907ad",
"name": "gopher"
"extension": ".png"
"url": "https://storage.com/blobs/gopher.png"
}
```
### Single Responsibility and Concern Separation
Ideally, requests have a single responsibility. This principle promotes the modularization of the API, making it composable.
For instance, imagine that the client-side wants to create a `cars` object with a photo. Instead of uploading the photo image and submitting other data in a single call, the flow would be:
1. Create a `cars` object
`POST /cars`
```json
{
"model": "Mazda RX-7",
"year": 1978
}
```
Possible response:
```json
{
"entity": "cars",
"id": 1,
"model": "Mazda RX-7",
"year": 1978,
"photo": null
}
```
2. Upload car image by performing an **action**:
`POST /cars/1/upload_photo` (using `multipart/form-data`)
This way, the logic of uploading the car image is decoupled for its creation, making the API more composable and making server-side logic easier to maintain, debug and reason about.
> [!NOTE]
> If the flow above requires a single call for a specific client (making car metadata and photo upload atomic), a wrapper endpoint can be exposed through a gateway (reverse proxy). This pattern is know as [BFF (Backend for Frontend)](https://learn.microsoft.com/en-us/azure/architecture/patterns/backends-for-frontends).
## Dealing with Collections
A **collection** in simply an array of entity objects.
### Collections have **operations**
Collections support the following operations:
- Pagination
- Sorting
- Filtering
### Pagination
Clients can paginate through a collection using the `page_number` and `page_size` query params (if those values are not sent by the client, server defaults will be used).
`GET /nations?page_number=1&page_size=2`: first page containing two `nations` objects.
```json
[
{
"entity": "nations",
"id": 1,
"name": "Brazil",
"continent": "America"
},
{
"entity": "nations",
"id": 2,
"name": "Uruguay",
"continent": "America"
}
]
```
`GET /nations?page_number=3&page_size=3`: third page containing three `nations` objects.
```json
[
{
"entity": "nations",
"id": 7,
"name": "Japan",
"continent": "Asia"
},
{
"entity": "nations",
"id": 8,
"name": "China",
"continent": "Asia"
},
{
"entity": "nations",
"id": 9,
"name": "South Korea",
"continent": "Asia"
}
]
```
### Sorting
Sorting order can be either `ASCENDING` or `DESCENDING`.
Sort nations by `name` in `ASCENDING` order: `GET /nations?sort=name`
```json
[
{
"entity": "nations",
"id": 122,
"name": "Afghanistan",
"continent": "Asia"
},
{
"entity": "nations",
"id": 83,
"name": "Albania",
"continent": "Europe"
},
{
"entity": "nations",
"id": 57,
"name": "Algeria",
"continent": "Africa"
}
]
```
Sort nations by `name` in `DESCENDING` order: `GET /nations?sort=-name` (hyphen/minus signal in front of the field name)
```json
[
{
"entity": "nations",
"id": 160,
"name": "Zimbabwe",
"continent": "Africa"
},
{
"entity": "nations",
"id": 45,
"name": "Zambia",
"continent": "Africa"
},
{
"entity": "nations",
"id": 37,
"name": "Yugoslavia",
"continent": "Europe"
}
]
```
### Filtering
Clients can filter the collection by entity properties using the `filter` query param. The following table shows available filter operators.
Filter operator | Description | Expression example
-------------------- | --------------------- | -----------------------------------------------------
**Comparison Operators** | |
eq | Equal | city eq "Redmond"
ne | Not equal | city ne "London"
gt | Greater than | price gt 20
ge | Greater than or equal | price ge 10
lt | Less than | price lt 20
le | Less than or equal | price le 100
**Logical Operators** | |
and | Logical and | price le 200 and price gt 3.5
or | Logical or | price le 3.5 or price gt 200
not | Logical negation | not price le 3.5
**Grouping Operators** | |
( ) | Precedence grouping | (priority eq 1 or city eq "Redmond") and price gt 100
Just insert the expression as a value for the `filter` query param:
`GET /nations?filter=continent eq "Europe"`
### Collections represents M/N-sided relations
Recalling, there are 3 types of relationship between entities:
- One-to-One (`1-1`)
- One-to-Many (`1-N`)
- Many-to-Many (`M-N`)
As we've seen, [expandable nested objects](#nested-relationship-objects-are-expandable) represents **1-sided** relations. For **M/N relations** (one-to-many and many-to-many), we use collections.
**One-to-Many (`1-N`)**: an **owner** has a collection of **dogs**.
`GET /users/1/dogs` redirects to `/dogs?filter=owner.id eq 1` (fetch `dogs` collection filtering by the owner)
```json
[
{
"entity": "dogs",
"id": 1,
"name": "Max",
"owner": {
"entity": "users"
"id": 1
}
},
{
"entity": "dogs",
"id": 2,
"name": "Scott",
"owner": {
"entity": "users",
"id": 1
}
}
]
```
**Many-to-Many (`M-N`)**: relationship between `orders` and `products`. An order has a collection of products, and a product has a collection of orders.
`GET /orders/{id}/products`: list products in that order.
`GET /products/{id}/orders`: list the orders that a given product is present.
## Request and Response Cycle
### Request HTTP methods
HTTP methods follows the specifications of [RFC 9110](https://www.rfc-editor.org/rfc/rfc9110.html#name-methods). A summary is presented below.
HTTP method | Common usage
-------------------- | ---------------------
POST | Create a new resource by transferring a representation; perform an unsafe action
GET | Fetch an object or a collection of objects; perform a safe (read-only) action
PATCH | Update an existing object by transferring a partial representation of entity data
PUT | Used for upserts
DELETE | Client requests the deletion of an object
### Response HTTP status codes
Status codes are categorized in four classes:
Status code range | Result
-------------------- | ---------------------
2xx | Success
3xx | Redirection
4xx | Client-side errors
5xx | Server-side exceptions
Most commonly used response status codes are:
Status code | Result
--------------------------- | ---------------------
`200 OK` | Fetched an object/collection; updated (PATCH) an object; performed a safe (read-only) action
`201 Created` | Request processing resulted in the creation of an object; performed an unsafe action
`202 Accepted` | Used for asynchronous requests
`204 No Content` | Request succeeded but returned no data
`400 Bad Request` | Malformed request syntax or invalid request semantics
`401 Unauthorized` | Lacking or invalid authentication credentials
`403 Forbidden` | Server understood the request but refuses to fulfill it. Given credentials does not have enough access level to the specified resource
`404 Not Found` | Server did not find a current representation for the target resource
`500 Internal Server Error` | Server encountered an unexpected condition that prevented it from fulfilling the request
### JSON Response Document
Unless returning a `204 No Content`, requests will return a JSON document. This document have defined fields, making the results predictable and stardandized.
The document have possibly 4 root-level fields:
- `meta`: optional object
- `data`: only present if `errors` is absent. Either an object or an array of objects
- `pagination`: only present if `data` is an array
- `errors`: only present if `data` is absent
If the four root-level fields above are absent (meaning a successful request returning no data), it is a `204 No Content` status code.
#### Meta Object
This object represents request metadata. Contains two defaults fields (`status` and `message`) and any other API-specific fields.
Example (default fields; mostly for debugging)
```json
{
"meta": {
"status": 404,
"message": "Could not find any user with provided ID."
}
}
```
More concrete examples
```json
{
"meta": {
"status": 201,
"message": "Created",
"request_token": "32606149-b8a2-42b0-b507-92d7f7c22465",
"remaining_quota": "Your API request limit is current at 69% of your daily usage quota."
}
}
```
#### Data Object/Array
Primary result of your request. The resulting data after requested was processed.
`GET /nations/8`
```json
{
"data": {
"entity": "nations",
"id": 8,
"name": "China",
"continent": "Asia"
}
}
```
#### Pagination Object
If `data` is an **array**, `pagination` is present. This object consists of four fields:
- `total_pages`: Total number of pages to represent the collection of objects using the current page size
- `current_page`: Current page number
- `page_size`: Page size controls how many objects each page will return
- `objects_count`: Count of the number of objects existing in this collection
`GET /nations?page_number=3&page_size=3`
```json
{
"data": [
{
"entity": "nations",
"id": 7,
"name": "Japan",
"continent": "Asia"
},
{
"entity": "nations",
"id": 8,
"name": "China",
"continent": "Asia"
},
{
"entity": "nations",
"id": 9,
"name": "South Korea",
"continent": "Asia"
}
],
"pagination": {
"total_pages": 4,
"current_page": 3,
"page_size": 3,
"objects_count": 12
}
}
```
#### Errors array
Array of error objects detailing **client-side** errors. An **error object** consists of three fields:
- `code`: application-specific error code
- `title`: a short, human-readable title of the error caused by the client
- `detail` (optional): error message providing details about the error in the current request context
```json
{
"meta": {
"status": 400,
"message": "Failed to upload user profile image."
},
"errors": [
{
"code": 20202,
"title": "Invalid media type",
"detail": "Tried to upload image of content-type 'image/heic' while only 'image/png' is supported."
},
{
"code": 15370,
"title": "File too large",
"detail": "File upload limit is 5 MiB. Tried to upload a 17 MiB file."
}
]
}
```
## References
JSON API
- https://jsonapi.org
- https://jsonapi.org/format/
- https://jsonapi.org/format/#document-structure (Response document structure)
- https://jsonapi.org/recommendations/
- https://jsonapi.org/examples/
Microsoft REST API Guidelines
- https://github.com/microsoft/api-guidelines
- https://github.com/microsoft/api-guidelines/blob/vNext/azure/Guidelines.md
- https://github.com/microsoft/api-guidelines/blob/vNext/azure/Guidelines.md#polymorphic-types (Polymorphic types)
- https://github.com/microsoft/api-guidelines/blob/vNext/azure/Guidelines.md?plain=1#L483 ("kind" field)
- https://github.com/microsoft/api-guidelines/blob/vNext/azure/Guidelines.md#filter-operators (filtering collections)
- https://github.com/microsoft/api-guidelines/blob/vNext/azure/ConsiderationsForServiceDesign.md
- https://github.com/microsoft/api-guidelines/blob/vNext/azure/README.md
Heroku Platform API (interagent)
- https://geemus.gitbooks.io/http-api-design/content/en/
- https://geemus.gitbooks.io/http-api-design/content/en/foundations/separate-concerns.html (Separate Concerns)
- https://geemus.gitbooks.io/http-api-design/content/en/responses/nest-foreign-key-relations.html (Nest foreign key relations)