Skip to main content

API guidelines

GENERAL API

Accept and respond with JSON

Using JSON payloads is the simplest and common way to provide APIs. JSON are intuitive, simple and widely diffused

Version your API: also if you think you'll never need to write a new API, start versioning them from day 1:

https://myapi.example.com/api/v1/...

Remember that also your controllers should live in a versioned namespace, in order to guarantee consistency: if you add a v2 API, v1 must work unless you deprecate it.

Focus HTTP response codes

Return the right HTTP code on every response. If an error occured, DO NOTrespond with a payload that contains the error and a 200 http code!

You don't really need to use the whole spectrum of available HTTP codes; below the most commonly used:

  • 200 OK: Standard response for successful HTTP requests
  • 400 Bad Request: The server cannot or will not process the request due to an apparent client error (e.g., malformed request syntax, size too large, invalid request message framing, or deceptive request routing)
  • 401 Unauthorized: Similar to 403 Forbidden, but specifically for use when authentication is required and has failed or has not yet been provided
  • 403 Forbidden: The user not having the necessary permissions for a resource or needing an account of some sort, or attempting a prohibited action
  • 404 Not Found: The requested resource could not be found but may be available in the future
  • 500 Internal Server Error: A generic error message, given when an unexpected condition was encountered and no more specific message is suitable

Successful responses

Successful responses should come back in the form of a standard successful HTTP response (200) and directly display their context. Example:

[
{
"_id": "60c1dedda145f5002ba15b61",
"clientName": "John Doe",
"participants": {
"adults": 2,
"children": 0,
"infants": 0
},
"priceLevel": 1,
"startingDate": "2021-07-01T00:00:00.000Z",
"services": [
{
"date": 0,
"sequenceId": 0,
"serviceId": "ATHAPUNAATHARATH",
"extraData": ""
}
],
"TourPlanBookings": [],
"__v": 0
}
]

Unsuccessful responses

Unsuccesful responses should always come back with the right error code (see previous paragraph) and should return the list of errors. Examples:

{
"errors": [
{
"code": "GEN-0001",
"details": "nn unknown error has occurred"
}
]
}
{
"errors": [
{
"code": "GEN-00002",
"details": "email parameter is missing"
},
{
"code": "GEN-00003",
"details": "date parameter is missing"
}
]
}

All error objects should contain a code and details. Code should have the following format <ERROR-CATEGORY/THEME>-<UNIQUE-IDENTIFIER>. So for instance all generic errors should start with GEN- all tourplan specific errors with TP- etc.

REST API

Describe resource functionality with HTTP methods

All resources have a set of methods that can be operated against them to work with the data being exposed by the API.

Method  Description
GET Used to retrieve a representation of a resource.
POST Used to create new new resources and sub-resources
PUT Used to update existing resources
PATCH Used to update existing resources
DELETE Used to delete existing resources

Use plurals for collections

Enpoints that fetch collections must have a plural name: for example if you are fetching a list of posts, the endpoint should be:

https://myapi.example.com/api/v1/posts

Use nesting for showing relationships

Keeping related endpoints together to create a hierarchy is known as API nesting. For example, if a user has any active orders, then nesting the /order after the /users/:id is good way of managing the API:

https://myapi.example.com/api/v1/users (list of users)
https://myapi.example.com/api/v1/users/321 (specific user by using filters)
https://myapi.example.com/api/v1/users/321/order (list of the order of the specific user)

It is recommended to use fewer nesting levels to prevent overcomplicating your application; you can use filtering to reduce the number of nesting levels. Two-level nesting typically keeps the API simpler and gets the job done.

Filtering

URL parameters is the easiest way to add basic filtering to REST APIs. If you have an /items endpoint which are items for sale, you can filter via the property name such as GET /items?status=active or GET /items?status=active&ean=9788845936081. However, this only works for exact matches. Infact filters should be composed of three components:

  • The field name
  • The operator
  • The filter value

With LHS brackets we can have as many operators as we need: lte, gte, eq, ne, like...

The result would look something like this. /items?filter=ean:like:978884,status:ne:presell

Note that LHS brackets can be a little more complicated to implement on the server but provide greater flexibility.

Sorting

Simply use a sort query parameter, allowing the ascending or descending order to be defined via a minus (-) sign, for example: GET /items?sort=-createdAt If you have more than one sorting column, use ; to add more: GET /items?sort=-createdAt,title

Pagination

Page based pagination is the simplest and most common form of paging: limit/page became popular with apps using SQL databases which already have LIMIT and OFFSET as part of the SQL SELECT Syntax. Very little business logic is required to implement Limit/Offset paging (offset is calculated with: offest = page * limit).

Limit/Offset Paging would look like GET /items?limit=2&page=5. This query would return the 20 rows starting with the 100th row.

Do not add pagination in the request and response headers: it's very difficult to understand.

Results must be returned in this way:

{
"results": [
{
"_id": "60f5266c0865db25b40f7890",
"status": "presell",
"ean": "9788845936081",
"title": "RELATIVITA GENERALE ",
"author": "ROVELLI CARLO ",
"createdAt": "2021-07-19T07:14:52.583Z",
"updatedAt": "2021-07-19T07:14:52.583Z",
"__v": 0
},
{
"_id": "60f5266c0865db25b40f7891",
"status": "presell",
"ean": "9788845935978",
"title": "LA MANO ",
"author": "SIMENON GEORGES ",
"createdAt": "2021-07-19T07:14:52.625Z",
"updatedAt": "2021-07-19T07:14:52.625Z",
"__v": 0
}
],
"limit": 2,
"page": 5,
"totalPages": 6,
"totalRecords": 12
}

Documentation

Documentation is one of the important but highly ignored aspects of a REST API structure. The documentation is the first point in the hands of customers to understand the product and critical deciding factor whether to use it or not.

A well compiled documentation should include:

  • API name
  • ACCEPTED METHOD
  • List of query parameters
  • List of body parameters (including knowledge of required ones)
  • A curl example

Consider using SLATE https://github.com/slatedocs/slate as tool for generating API documentation. It's not an autgenerated tool but in this way it guarantees all the needed flexibility.

NOTE: the documentation MUST always keep synchronized with the APIs

GraphQL API

Despite REST that till now doesn't have a standard, GraphQL defines it's own standard and you can find it here: http://spec.graphql.org/

You can also find a schema language cheat sheet here: https://github.com/sogko/graphql-schema-language-cheat-sheet

Finally, check out the official guide: https://graphql.org/learn/

Queries

GraphQL simplifies things for the clients by giving them the control to query for exactly what they want. However, this means the clients can potentially ask for something that is resource intensive by making a lot of database calls on the server. Since clients have the possibility to craft very complex queries, our servers must be ready to handle them properly.

On the client side, to avoid server crashes you must:

On the server side, you can:

  • setup a timeout to defend against large queries
  • configure the server to allow queries with a maximum query depth of 3
  • setup a simple throttle that can be used to stop clients from requesting resources too often

Mutations

  • Using input object type for mutations: it is extremely important to use just one variable for your mutations and use the input object type to simplify the structure of your GraphQL documents:
input LoginMutationInput {
email: String!
password: String!
}

mutation Login($input: LoginMutationInput!) {
login(input: $input) {
token,
user {
id
}
}
}
  • Also, remember to return affected objects as a result of mutations!

Pagination

Always paginate lists of objects from day 1: do not return the entire result set at once.

Paginated results are really important for security reasons and for ability to limit amount of records we would like to retrieve from the server. It is a good practice to structure paginated results as follows:

{
"data": {
"clientMissions": {
"page": 1,
"totalPages": 5,
"limit": 2,
"totalRecords": 12,
"hasNext": true,
"hasPrev": true,
"objects": [
{
"id": "8264",
"status": 1,
"statusLabel": "Accomplished",
"code": "A2-20-006-M",
"begin": "2020-05-01",
"end": "2020-05-08",
"clientApprovalDecision": null,
"isVisibleByClient": true
},
{
"id": "8265",
"status": 1,
"statusLabel": "Accomplished",
"code": "A2-20-007-M",
"begin": "2020-04-26",
"end": "2020-05-01",
"clientApprovalDecision": null,
"isVisibleByClient": true
}
]
}
}
}

Error Handling

A successful GraphQL query is supposed to return a JSON object with a root field called "data". If the request fails or partially fails (e.g. because the user requesting the data doesn’t have the right access permissions), a second root field called "errors" is added to the response

{
"data": { ... },
"errors": [ ... ]
}

Exploring and testing data

The best way to explore and debug your GraphQL APIs is through GraphiQL IDE: https://github.com/graphql/graphiql

You can also use GraphiQL as documentation reference without writing any ad hoc documentation. Alternatively, you can use libraries like this https://github.com/2fd/graphdoc to generate beautiful documentation from the schema.

Other best practices

Always take a look at the official reference to find out tips and best practices https://graphql.org/learn/best-practices/