The purpose of this document is to establish a shared vocabulary and a pragmatic (yet robust) approach to design an HTTP API using JSON (JavaScript Object Notation) as a data-interchange format.
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.
Clients can interact with entities in two shapes:
- A single object:
/objects/{id} - A collection of objects:
/objects
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:
{
"id": 1,
"name": "Max"
}
Valid forms of defining an object:
{
"entity": "users",
"id": 1,
"name": "Max"
}
{
"entity": "dogs",
"id": 1,
"name": "Max"
}
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)
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:
{
"id": 1,
"name": "Max"
"owner_id": 1
}
Valid form of defining a relationship:
{
"entity": "dogs",
"id": 1,
"name": "Max"
"owner": {
"entity": "users",
"id": 1
}
}
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
{
"entity": "dogs",
"id": 1,
"name": "Max"
"owner": {
"entity": "users",
"id": 1
}
}
GET /dogs/1/owner redirects to GET /users/1
{
"entity": "users",
"id": 1,
"name": "Max",
"age": 12,
"citizen_id": "123.456.789-00"
"phone_number": "91234-5678"
}
Since relationships are represented as nested objects, they are expandable.
GET /dogs/1?expand=owner
{
"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.
Entity endpoints are separated by grammar semantics:
- A
nounendpoint is used for transferring entity state representations (/objects/{id}) - A
verbendpoint is used for performing actions (/objects/{id}/do)
An example for a user_files entity with the following schema:
id: uuid
name: string
extension: string
url: string
POST /user_files will create a new object by transfering a representation:
{
"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:
{
"entity": "user_files",
"id": "13728a84-e1dc-4de6-8f88-f0ba574907ad",
"name": "gopher"
"extension": ".png"
"url": "https://storage.com/blobs/gopher.png"
}
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:
- Create a
carsobject
POST /cars
{
"model": "Mazda RX-7",
"year": 1978
}
Possible response:
{
"entity": "cars",
"id": 1,
"model": "Mazda RX-7",
"year": 1978,
"photo": null
}
- 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).
A collection in simply an array of entity objects.
Collections support the following operations:
- Pagination
- Sorting
- Filtering
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.
[
{
"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.
[
{
"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 order can be either ASCENDING or DESCENDING.
Sort nations by name in ASCENDING order: GET /nations?sort=name
[
{
"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)
[
{
"entity": "nations",
"id": 160,
"name": "Zimbabwe",
"continent": "Africa"
},
{
"entity": "nations",
"id": 45,
"name": "Zambia",
"continent": "Africa"
},
{
"entity": "nations",
"id": 37,
"name": "Yugoslavia",
"continent": "Europe"
}
]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"
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 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)
[
{
"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.
HTTP methods follows the specifications of RFC 9110. 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 |
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 |
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 objectdata: only present iferrorsis absent. Either an object or an array of objectspagination: only present ifdatais an arrayerrors: only present ifdatais 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.
This object represents request metadata. Contains two defaults fields (status and message) and any other API-specific fields.
Example (default fields; mostly for debugging)
{
"meta": {
"status": 404,
"message": "Could not find any user with provided ID."
}
}More concrete examples
{
"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."
}
}Primary result of your request. The resulting data after requested was processed.
GET /nations/8
{
"data": {
"entity": "nations",
"id": 8,
"name": "China",
"continent": "Asia"
}
}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 sizecurrent_page: Current page numberpage_size: Page size controls how many objects each page will returnobjects_count: Count of the number of objects existing in this collection
GET /nations?page_number=3&page_size=3
{
"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
}
}Array of error objects detailing client-side errors. An error object consists of three fields:
code: application-specific error codetitle: a short, human-readable title of the error caused by the clientdetail(optional): error message providing details about the error in the current request context
{
"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."
}
]
}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)