Therea are a lot of REST API implementations. And it is always takes a lot of time to decide what to use. For example, should we use unix time or ISO8601? How foreign keys should be handled? How to return errors? etc.
This document just describes practices that works good in production. REST API should be intuitive and understandable without reading tons of docs. Several years ago we started using jsonapi.org but it is rather unstable and every time when I visit jsonapi.org I understand that everything written before is not compatible with newer version.
Moreover, jsonapi.org becomes too complex. We call it jsonapi for Zergs. In our company we prefer to use jsonapi for Terrans :). This document describes it. Everything is written in document is just recomendation no "MUST", use common sense for edge cases.
- All routes should be prefixed with /api/v1.
- Resource names in plural. For example, "users", "orders", "posts" etc.
- Field names in camelCase
- Date and time in ISO8601 UTC. For example, "2015-04-02T14:20Z". Dates without time look like "2015-04-02".
- Enumarable always in upper case. For example, userRole: "ADMIN", "MODERATOR", "CUSTOMER"
- Every return object contains "id" field. Value of "id" is always represented as string. (to avoid problems with numeric types representations)
- All links to related objects are located in "links" section. Every link has collection name ("type" field) and object identifier ("id"). IMPORTANT: links should not conflict with attributes name of base object.
There is a collection "/api/v1/posts" and there are posts in the collection. Each post os idintified by "/api/v1/posts/:id". Think about routes as identifiers, HTTP methods - are available actions.
- GET /api/v1/posts - get posts collection. Can return only subset of fields for each post.
- POST /api/v1/posts - add post to collection. Returns newly created object.
- GET /api/v1/posts/31 - get post with id=31. May return more fields in object than it was in list call. For example, it can be inneficient to return large payload in list.
- PUT /api/v1/posts/31 - replace(add) post with id=31
- PATСH /api/v1/posts/31 - update some fields in post with id=31
- DELETE /api/v1/posts/31 - delete post with id=31
POST /api/v1/posts
{
"data": {
"title": "Babel vs Traceur",
"text": "What is better..."
}
}
HTTP status - 201 Created
{
"status": 1,
"data": {
"id": "2312",
"title": "Babel vs Traceur",
"text": "What is better..."
"links": {
"comments": []
},
"createdAt": "2015-04-02T14:20Z",
"updatedAt": "2015-04-02T14:20Z",
}
}
GET /api/v1/posts
HTTP status - 200 Ok
{
"status": 1,
"data": [{
"id": "2312",
"title": "Babel vs Traceur",
"text": "What is better..."
"links": {
"comments": [
{"type": "comments", "id": 23},
{"type": "comments", "id": 24}
]
},
"createdAt": "2015-04-02T14:20Z",
"updatedAt": "2015-04-02T14:20Z",
}]
}
GET /api/v1/posts/2312
HTTP status - 200 Ok
{
"status": 1,
"data": {
"id": "2312",
"title": "Babel vs Traceur",
"text": "What is better..."
"links": {
"comments": [
{"type": "comments", "id": 23},
{"type": "comments", "id": 24}
]
},
"createdAt": "2015-04-02T14:20Z",
"updatedAt": "2015-04-02T14:20Z",
}
}
PATCH /api/v1/posts/2312
{
"data": {
"title": "Babel vs Traceur!",
}
}
HTTP status - 200 Ok
{
"status": 1,
"data": {
"id": "2312",
"title": "Babel vs Traceur!",
"text": "What is better...",
"links": {
"comments": [
{"type": "comments", "id": 23},
{"type": "comments", "id": 24}
]
},
"createdAt": "2015-04-02T14:20Z",
"updatedAt": "2015-04-02T14:20Z",
}
}
DELETE /api/v1/posts/2312
HTTP status - 200 Ok
{
"status": 1,
}
POST /api/v1/orders
HTTP status - 200 Ok
{
"data": {
"name": "Order electronics",
"deliveryDate": "2015-04-20",
"links": {
"customer": {"type": "customers", "id": "212"},
"payments": [
{"type": "payments", "id": "34"},
{"type": "payments", "id": "35"}
],
},
"orderedProducts": [
{
"quantity": 20,
"links": {
"product":{ "type": "products", "id": "123" }
}
},{
"quantity": 21,
"links": {
"product":{ "type": "products", "id": "124" }
}
}
]
}
}
HTTP status - 200 Ok
{
"status": 1,
"data": {
"id": "2312",
"name": "Заказ техники",
"deliveryDate": "2015-04-20",
"status": "PENDING",
"links": {
"customer": {"type": "customers", "id": "212"},
"payments": [
{"type": "payments", "id": "34"},
{"type": "payments", "id": "35"}
],
},
"orderedProducts": [
{
"quantity": 20,
"links": {
"product":{ "type": "products", "id": "123" }
}
},{
"quantity": 21,
"links": {
"product":{ "type": "products", "id": "124" }
}
}
],
"createdAt": "2015-04-02T14:20Z",
"updatedAt": "2015-04-02T14:20Z",
}
}
Use "include" parameter to load related data. The parameter is optional and can support not all relations but only those that makes sense to support.
Related data should be placed in "linked" section.
GET /api/v1/posts?include=comments
HTTP status - 200 Ok
{
"status": 1,
"data": [{
"id": "2312",
"title": "Babel vs Traceur",
"text": "What is better..."
"links": {
"comments": [
{"type": "comments", "id": "23"},
{"type": "comments", "id": "24"}
]
},
"createdAt": "2015-04-02T14:20Z",
"updatedAt": "2015-04-02T14:20Z",
}],
"linked": {
"comments": [
{
"id": 23,
"text", "пиши есчо",
"links": {
"author": {"type": "users", "id": "13" }
}
},
{
"id": 24,
"text": "отличный пост",
"links": {
"author": {"type": "users", "id": "13" }
}
}
]
}
}
You can pass nested names to include
GET /api/v1/posts?include=comments,comments.author
GET /api/v1/posts?sort=title,-text&limit=20&offset=100&filter={}
Filters are often wery complex, therefoere it is ok path filter as json object.
When error occurs server should return error object with:
- code
- message
- fields with json pointers to fields with errors. Values are error codes.
HTTP status - 200 Ok
{
"status": 0,
"error": {
"code": "FORMAT_ERROR", // Error code
"message": "Check data format", // Message for developer
"fields": {
"title": "REQUIRED", // Error code for field "title"
"text": "REQUIRED", // Error code for field "text"
}
}
}