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:
- Query only the data you need, where you need it!
- Use nested objects to reduce network calls, but avoid going deeper in the response tree: in the large majority of cases, a query such this should be divided into multiple queries that are distributed among the appropriate components
- Use interfaces (https://graphql.org/learn/schema/#union-types) and unions (https://graphql.org/learn/schema/#union-types) for abstraction
- Use fragments to reuse defined types (https://graphql.org/learn/queries/#fragments)
- Cache responses!
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/