Summary: We help developers learn and grow by keeping them up with what matters. 👉 www.faun.dev

FAUN.dev() 🐾

FAUN.dev() 🐾

We help developers learn and grow by keeping them up with what matters. 👉 www.faun.dev

)

REST API BEST PRACTICES

Introduction

Rest API Best Practices Standard Document helps to decide how our microservice should be designed. I have documented the standards which will help to build a microservice in a proper way.

URI Format

The full URI format will be

{base-path}/{area}/{version}/entity1/{entity1}/{entity2} where:

  • base-path is {dns-name}/{microservice-name}
  • area such as api or management to indicate the general area, most cases just use api
  • version is the current version of the micro-service area
  • entity1 is the entity1 as supplied by the gateway
  • entity2 is the main entity of the service

Entity names should be:

  • plural and nouns (employees, customers, orders,contacts, etc).
  • use dashes for compound names eg:- customers-orders

If a sub entity is a primary entity of the service then the entity name can be dropped.

For example, order/statuses instead of *order/order-statuses*.

If the sub entity is not a primary entity of the service the full sub entity name should be used.

For example: customers / *orders/order-statuses*.

If the endpoint does not segregate by customers then the customers/{customer-id} part of the URI is dropped to become:

{base-path}/{area}/{version}/{entity}

Example for order service:

Field naming

  • All Field Names will be camelCase.
  • Field Names should NOT contain spaces or underscores
  • Compound Field Name should repeat the object name unless they are specifically for the entity itself.
  • status can be used instead of orderStatus if it is on the order object.
  • Field Names are case sensitive when possible. orderstatus is not the same as order Status.
  • Do not have fields that differ only in case (order status and OrderStatus).
  • Do not use abbreviations unless it is an industry common term.
  • Always lowercase the abbreviation if it is the full name, and camelCase if part of a compound field name.
  • /id or /statusId NOT /iD or /statusID
  • Field references should always end in ID unless they are returning an actual object.
# Get Order Response Example Get /order-service/v1/orders/12 { "id": "12", "status": { "id" : "657", "name" : "Open" }, "contactId" : "9" }
  • Fields should be consistently named between the various operations (POST, PATCH, GET). For example you should not use statusId for POST and status/id for GET
  • You can however have some fields that are not honored or returned in various circumstances. For example you can choose to not honor status/name for a POST but return it for a GET
# POST Order Request Example Get /order-service/v1/orders{ "status": { "id" : "77" }, "contactId" : "87" }

Response Details

  • If a field is null do not return it in the response
  • If a field is empty (for example an empty string) It should be treated as NULL
  • The exception to this is if there is a difference in meaning between NULL and an empty value. For example a cost of 0.00 could be difference than an unset cost of NULL
  • If a field is an array with no values you should return an empty array instead of not returning the field
  • Datetimes should use ISO-standard representation

HTTP Methods and HTTP Status Codes

Common status codes and applicable to all the HTTP verbs https://en.wikipedia.org/wiki/List_of_HTTP_status_codes. Out of which, there are few though which can be used in application in some cases are as follows -

Status Code Additional info 503(Service Unavailable)Use this stats code to state if circuit is open(if the application is using the circuit breaker)409(Conflict)If the operation can not be performed because of any dependency. For example, if trying to delete a resource which cannot be deleted unless all reference of the resource are deleted explicitly.401(Unauthorized)Received a request but the authentication token is not valid to perform the request, typically in case of CUD operation.

GET

Used to retrieve resource representation/information only.
It should be read only and should not modify representation/information and hence considered as safe method. GET request should not have anything in request body.

Request: Headers

Response: A representation of the resource or collection at the request URI

Status Code Reason

200 (Ok)

Resource was successfully retrieved. If you’re trying to get a list of resources and there are none, then it would still be 200.
The array you normally return should still be returned, but it will have a length of 0.

400 (Bad Request)

An invalid query string value was passed in, such as an incorrect format or a limit over the max allowed by the server. Alternatively if a request is missing a mandatory query string or header to be a valid request.

404 (Not Found)

The requested resource does not exist. Only applies for getting 1 resource, like getting it by its ID, or if some ID in the path is definitely not known.
For example, if you get an incorrect ID that your service doesn’t validate, that’s fine, it shouldn’t cause a 404.

POST

POST methods are used to create a new resource. It is neither a safe nor an idempotent. Invoking two identical POST requests may result in two different resources containing the same information (except resource ids).
Request: A representation of a resource.

Response: body of the created resource and/or a URI locating the new resource

Note: POST should never return a 404 response status code. No query string should be allowed in URL unless explicitly mentioned elsewhere in this document.

Status Code Reason

201 (Created)If the resource was successfully created409 (Conflict)If the resource already exists

Bulk Post

A POST request only but allowing to create multiple resources of same type in one request. Such APIs must be with /batch suffix and the request body should be the array of resource.

Request: An array of resource. Below is an example

[
  {
    "name": "Raj",
    "taxId": 10,
    "mainDetail": {
        "number": "12345"
    }
  },
  {
    "name": "Jack",
    "taxId": 10,
    "mainDetail": {
        "number": "3457"
    }
  }
]

Response: An array of response object having information(and status code) of each resource of the request body

Below is the response body format and an example of partial success. The response contain two object wherein one is success and other is failed with 400 status code

[{
    "status": 200,
    "result": {
        //result
    }
}, {
    "status": 400,
    "errors": [{
        //errors
    }]
}]

You can very well include the Id or any identified as per the API need which can help understand the mapping of each object in array of response body to the request payload

Note: A microservice should come up with and implement a limit on the number of allowed resources per request

Note: If all of the individual resource requests returned the same status code, the request as a whole should return that status code.

Status Code Reason

201 (Created)

If all the requests were successful 207 (Multi Status)Contain a number of separate response codes, depending on how many sub-requests were made

400 (Bad Request)

Is the request payload is invalid

Note: A microservice may need some additional GET, POST endpoints to support functionalities like ping, renew expiry time of something, or anything related to remote procedure call. Although they don’t fall under RESTful architecture, they can be supported and the endpoints for such requests should be different than the resource endpoint.

PUT

PUT is not a safe but an idempotent request. Making the same PUT call again and again, does not change the state of the resource on the server.
It creates a new resource or modifies the existing one. If a URL supports PUT and POST both request then the payload for a PUT request should exactly be same as POST. One can very well choose to only support PUT request rather a PUT and POST both. If the nature of a URL is such that the client is unaware if the resource exist or not but still want to update or create the resource, then the URL should just support PUT request.

Return the modified resource in success response (for status code 200) If there is a strong use case or else it should just return the status code and no response body.

Request: A representation of a resource.
Response: Generally status of the update but should we return modified resource (or created) also?

Note: No query string be allowed in URL

Status Code Reason

200 (Ok)

If the resource was successfully updated.201 (Created)In cases when a new resource was created successfully.204 (No Content)If the resource was successfully updated and no representation is returned.404 (Not Found)If the client attempts to update a resource that does not exist.

PATCH

The PATCH method requests that a set of changes described in the request entity be applied to the resource identified by the Request-URI.

Request: A “patch document” representing a set of changes to be applied to the specified resource. Unlike PUT, PATCH is not an idempotent request. So if a PATCH is issued for a resource which does not exist on Server, it should return a 404 rather creating that resource.

Use JSON-PATCH with the format as below and as per the RFC standard https://tools.ietf.org/html/rfc6902 and the content type of the request should be Content-Type: application/json-patch+json

[

{ "op": "test", "path": "/a/b/c", "value": "foo" },
     { "op": "remove", "path": "/a/b/c" },
     { "op": "add", "path": "/a/b/c", "value": [ "foo", "bar" ] },
     { "op": "replace", "path": "/a/b/c", "value": 42 },
     { "op": "move", "from": "/a/b/c", "path": "/a/b/d" },
     { "op": "copy", "from": "/a/b/d", "path": "/a/b/e" }
]

Response: The status of the update.

Note: No query string be allowed in URL. Refer http://jsonpatch.com/ for example on JSON Patch

For the simplicity, we will only support operation “replace” to replace the value of any existing field in the resource. If applicable and required, you can also support add/remove operation but only for Array like field. No new field to be added or removed using JSON Patch.

If a readonly field is attempted to be patched a 400 error should be thrown

Status Code

Reason

200 (Ok)If the resource was successfully updated.204 (No Content)If the resource was successfully updated and no representation is returned.400 (Bad Request)Malformed patch document provided.404 (Not Found)If the client attempts to update a resource that does not exist.415 (Unsupported Media Type)The client sends a patch document format that the server does not support for the resource identified by the Request-URI.

DELETE

Delete methods are used to delete a resource.

DELETE operations are idempotent. Calling Delete API multiple times changes the status from 200 or 204 to 404

Request: Headers but no body (identified by the Request-URI)

Response: The status of the delete action. It may also include a representation of the deleted resource in the body.

Note: No query string be allowed in URL

Status Code Reason

204 (No Content)

If the resource was successfully delete and no representation is returned in the response body.404 (Not Found)

If the client attempts to delete a resource that does not exist.

409 (Conflict)If the resource deletion is creating a conflict. i.e. resource is used somewhere else like a foreign dependency

Bulk Delete

A POST request allowing for the deletion of multiple resources of the same type in one request. Such APIs must be made with the /batch-delete suffix and the request body should be an array of ids to be deleted.

Request: An array of ids. Below is an example

[
    1,
    2,
    5
]

Response: An array of response object having information(and status code) of each resource of the request body

Below is the response body format and an example of partial success. The response contain two object wherein one is success and other is failed with 400 status code

[{
    "status": 200,
    "result": {
        //result
    }
}, {
    "status": 400,
    "errors": [{
        //errors
    }]
}]

You can very well include the Id or any identified as per the API need which can help understand the mapping of each object in array of response body to the request payload

Note: A microservice should come up with and implement a limit on the number of allowed resources per request

Note: If all of the individual resource requests returned the same status code, the request as a whole should return that status code.

Status Code Reason

200 (Ok)

If all the requests were successfully deleted 207 (Multi Status)Contain a number of separate response codes, depending on how many sub-requests were made400 (Bad Request)Is the request payload is invalid

Versioning

The Major version of the service is part of the url in the format v#, such as v1 etc. This allows a new major version to be deployed without breaking existing service consumers. This also aids in the goal of zero downtime deployment.

A Major version is needed any time a breaking change is introduced with consumers of the existing service. Non breaking changes such as adding additional fields or features do not trigger a Major version increment and should follow https://semver.org/. The current full version of the service can be retrieved by calling the version end point as outlined in the Health and Version section of this document.

If a service accidently pushes a breaking change to an existing version, they must immediately roll back that change since the service has broken its version contract. Services should maintain automation tests that verify they do not accidently break consumers of an existing version with an update.

The only time a breaking change to an existing version with consumers in production is allowed is if there is a critical security issue that has to be patched immediately.

Error Handling Best Practices

  • ALWAYS return relevant HTTP status codes
  • For errors due to client inputs, return a 4xx status code.
  • For errors due to server implementation or current server state, return a 5xx status code.
  • Always include a JSON error representation in the response body if applicable (apart from HEAD request).
  • Errors should have relevant information outside of the message to be machine parseable whenever possible
  • If you have a generic code that applies to multiple fields, there should be another field in the response specifying what field it is applying to
  • Keep the response body descriptive. Along with the error message, describe any actions that the client can take to correct the error if applicable.
  • Do not expose internal technologies. Details such as stack traces or system error messages should NEVER be returned in the error response. In fact, Service should never return anything in the error that we didn’t specifically write or handle. Any error which is not handled should be returned as ‘Unexpected error’.
  • If information to correct or debug the error is available as a separate human-readable document, include a link to that document in the response body (possibly more relevant for public-facing APIs).
  • Error Codes are good way to further categorize the errors and which also enable consumers to handle the error in more meaningful ways.

Response Body

  • Error description object should include:
  • error code — error type identifier (please check the Error Code section below)
  • message — A description with information about the error and how to fix the error, #if applicable.
  • Body should have a list of error description object — Sometimes, we may want to return more than one error for the given request. Hence we should always return a list of errors in response body to avoid complications in such cases.
  • Only message field can be localized by the service if needed. All the keys and error codes should remain as it is.

Error Codes

  • Error code can be a short, human-readable key for the error type (mostly a short alphanumeric value which identifies the error). It should not change from occurrence to occurrence and should be limited to finite set of values.
  • There are some general error codes which can be used across multiple services. e.g. RequiredFieldMissing, FieldInvalid, NoRecordFound, etc. Such error code list is constantly evolving hence it should be a part of some common lib and needs to be documented properly.
  • In case of multiple errors, only the errors with same Http status code can be combined together. e.g. error description cannot have both errors like InvalidField and NoRecordFound in the same response as they represent status 400 & 404 respectively.
  • Sometime, an application may need an error code which is very specific to that application. Hence applications are allowed to use their own error codes apart from the common ones.

Example POST request and error response

# Request
POST /reporting/v1/addresses HTTP/1.1
Host: api.itsupport247.net
Content-Type: application/json
Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==

{"name":"Demo Address in NOIDA ","addressCode":"Demo"}

# Response example 1
HTTP/1.1 400 Bad Request
Content-Type: application/json
Location: https://api.itsupport247.net/v1/addresses/Demo

{ "errors": [{ "code": "RequiredFieldMissing", "field" : "address", "message": "address is a required field.", "info": "https://doccenter.itsupport247.net/addresses/creating" }, { "code": "FieldInvalid", "field" : "addressCode", "message": "addressCode needs to be of UUID format."}] }

# Response example 2
HTTP/1.1 400 Bad Request
Content-Type: application/json
Location: https://api.itsupport247.net/v1/addresses/Demo

{ "errors": [{ "code" : "InvalidField", "field" : "name", "message": "Maximum 10 characters are allowed for name"}] }

Reference Objects

Reference Objects allow nesting of child entities within the main entity Request and Response. This can dramatically cut down on the number of requests that a UI or other consumers have to make.

  • The child Reference Object should just include the minimal number of fields that are most likely to be needed.
  • One to One relationships are easier to include while One to Many have challenges.
  • Judgement has to be used on what child entities should be included and which ones should not. If consumers, especially a UI, are constantly having to make multiple calls every time they call the main entity, that is a sign it should be a Reference Object instead.
  • While there is no hard rule on the amount of nesting to include, after the first 2–3 levels reference objects might affect performance and should not be used.
  • For more complicated/depth levels of descendant entities, the Unified Entity Service/graph layer could be considered to allow those entities only on request.
  • The name of the Reference Object does not have to exactly match the child entity name. For example, the Customer model could have DefaultAddress, ShippingAddress, BillingAddress etc even though all three are the same Address entity.

Let’s assume an example entity Order with two child entities, Customer and Order Status.

The payload would look like this:

{
   "order":{
      "id":12,
      "customerID":8,
      "status":{
         "id":63,
         "name":"Active"
      }
   }
}
  • Since Customer is outside the Order Service domain, it would always be an ID and not a Reference Object.
  • The oredr status Reference Object just contains the minimal amount of fields, not all fields such as createdBy/createdDate etc
  • It is a common pattern is to do a GET on an entity, modify some of the fields, and then do a PUT to update those field. The Endpoints should accept other Reference Object fields such as Name or Description but the non ID fields should be ignored and not attempt to actually update the fields or verified for accuracy.
  • For PATCH operations, an attempt to change a non ID field through a patch to the parent entity should throw an error, as the patch should be applied to the child entity endpoint itself instead.

Filtering

The basic idea behind filtering is to restrict GET calls in order to reduce the amount of results being returned to the subset that you need. Because of this It does not need to be implemented for every endpoint and should only be considered for entities that have these use cases. There are two different implementations you can go with depending on your use cases.

Simple Filtering

For simple filtering use cases where you need to restrict the endpoint in a basic way.

Syntax

You pass in the field you want to filter on as a query string parameter. The basic format should look something like:

.../contacts?firstName=Example

Where firstName is the field being filtered on and “Example” is what it is being compared to.

Details

This format is relatively simple, It is the simple syntax of fieldName=value. You can have multiple query parameters for different fields if you want to filter on multiple fields.

Some additional Notes:

  • Field names are case sensitive. If an invalid casing is passed it will be ignored.
  • The value does not need to be surrounded by anything
  • Timestamps should be done in the standard ISO8601 format
  • Booleans should be passed in as true or false
  • Nested fields can be filtered on using dot notation
  • Only allow filtering on nested fields if you have use cases for it
  • If you want a filter to allow multiple values to be passed in you can pass in a comma separated list
  • The swagger should state this functionality for the query string parameter
  • The field name is still used like normal

Examples

.../orders?summary=Test&closedFlag=false
.../customers?address.country=United States
.../contacts?createdAt=2020-11-01T12:00:00.000Z.../addresses?id=1,2,3

Swagger Documentation

Each supported query string should be documented in the swagger in the YAML format as a query paramater according to the OpenAPI 3.0 spec.

Complex Filtering

For use cases that require more flexibility in filtering, such as conditions other than basic equality checks there is an alternate syntax that can be expanded upon to allow more powerful filtering.

Syntax

Filtering should be passed as a query string parameter ‘filter’. The basic format should look something like:

.../contacts?filter=firstName="Example"

Where firstName is the field being filtered on and “Example” is what it is being compared to.

Details

The syntax is loosely based off of some Google Apis as well as others such as Microsoft Apis. This gives us the ability to support more advanced filtering for endpoints if necessary but at it’s base an endpoint only needs to support the following syntax:

filter=comparison { "AND" comparison }
comparison: fieldName = value

What this means is at the basic level you can chain together equality operators. Some additional Notes:

  • If you are only supporting the above there is no reason you can’t just use simple filtering. But it does allow you to do more
  • You can additionally choose to support “NOT” and “OR” Logical operators
  • You can additionally choose to support the “<=”, “<”, “>=”, “>”, “!=”, “:” comparison operators
  • The “:” operator is a substring operator. It checks to see if the string passed in is a substring of the field
  • At some point it will become necessary to support parentheses for precedence grouping
  • Nested fields should use periods between the nesting.
  • value should be surrounded by double quotes for strings and enums.
  • value does not need to be surrounded by anything for Numbers and booleans
  • Booleans should be passed in as true or false
  • Timestamps should be done in the standard ISO8601 format surrounded by quotes. (Ie: “2020–11–01T12:00:00.000Z”)

Examples

## These examples can all be accomplished using simple filtering
.../orders?filter=summary="Test" AND closedFlag=false
.../customers?filter=address.country="United States"
.../contacts?filter=createdAt="2021-01-01T12:00:00.000Z"## These example are more complex and where you should look at using the complex filtering
# This looks for all contacts with an email of the google.com domain
.../contacts?filter=emailAddress:"@google.com"
# This looks for all active customers outside of the U.S
.../customers?filter=inactiveFlag=false AND NOT address.country="United States" 
# This gets all orders with specific keywords that are in a New or In Progress status
.../orders?filter=(summary:"Critical" OR summary:"Emergency") AND (status.Name="New" OR status.Name="In Progress")

Swagger Documentation

Filters should be documented in the swagger as a query paramater according to the OpenAPI 3.0 spec. The description should include which fields and operators are supported

Example

parameters:
- name: filter
  in: query
  schema:
    type: string
  description: |
    Allow for filtering down the result set
    Fields allowed to be filtered on: name, closedFlag
    Operators allowed: =

Pagination

Pagination should be used to transverse large data sets and get all the results over multiple calls. It does not need to be implemented for every endpoint and should only be considered for entities with the potential to have over one page worth of data.

Forward-Only Pagination

While their are multiple types of pagination the default to be used should be forward-only pagination. It is also known as cursor pagination. It is the most efficient for getting large data sets.

The basic idea behind forward-only paging is:

  1. A consumer makes a call requesting the first x records.
  2. The server servers the request along with stating the last record that was served (usually in the form of a response header pointing to the next page)
  3. The consumer then makes another request using the response header to get the next x records
  4. Repeat steps 2 and 3 until all records have been retrieved.

This is very easy for the server to perform because it can just transform the record left off on into a filter in the query. However it does cause some sorting limitations.

Request Details

Pagination requests are Get requests against a collection that make use of some query string parameters:

  • limit: This is how many records you want to get in each page. If not specified a default should be assumed.
  • cursor: This the last record that was processed. It tells the server where to start from next time. If not passed in it is assumed you are asking for the first page
  • sortBy: This tells the server what field you want to sort by. It is not necessary to implement this and you can instead always sort by id or creation date if desired.

Notes about sorting

If you choose to allow the consumer to specify a sortBy field do note that the field (or set of fields) needs to be unique. Otherwise the server won’t know which record it left off on.

The easiest way to handle this is to make the last sort field the id of the record since those are always unique. Whatever fields you choose to sort by need to be included in the cursor.

Response Details

The response should return the result like normal, limited to the number of records indicated by the limit starting after the passed in cursor sorted by the sortBy field.

Besides that the only thing that should be included is a link header pointing to the next page unless there are no further records.

  • The link header should follow RFC 5988
  • Only the next relation should be included
  • The link should mirror what was initially sent except with the cursor query string added or updated to reflect the last record that was returned.

Example

Scenario: Microservice A needs to get all the orders for a customer from Microservice B. The customer has thousands of customers so they need to page through the data.

# Microservice A Request for first 1000 customers
GET /customer-service/v1/orders?limit=1000 HTTP/1.1
Host: api.customerService.net

# Response # Returns the next page link in the Link header. The cursor equals the id of the last record

HTTP/1.1 200 OK
Content-Type: application/json
Link: </customers?limit=1000&cursor=e8fa6d90-eb97-432e-ba38-04db482d9cef>; rel="next"

[{"id":"abfa6d90-eb97-432e-ba38-04db482d9cef","name":"Customer Sample"},{"id":"acca6d11-ab12-432e-ce33-16hb123d9by3","name":"Customer Example"},... (997 more customers here){"id":"e8fa6d90-eb97-432e-ba38-04db482d9cef","name":"Customer Test"}]

# Microservice A Request then uses the link header to request the next 1000 customers
GET /customer-service/v1/customers?limit=1000&cursor=e8fa6d90-eb97-432e-ba38-04db482d9cef HTTP/1.1
Host: api.customerService.net

# Response # Returns the next page link in the Link header. The cursor equals the id of the last record

HTTP/1.1 200 OK
Content-Type: application/json
Link: </customers?limit=1000&cursor=j8fa61zw-ew9z-432e-ba42-0adb4s2dabc3>; rel="next"

[{"id":"fafa6d90-ls67-d12e-bb12-04ac48219tet","name":"Customer Sample"},{"id":"fec124a9-ab12-432e-c2s3-23hb123ac4yz","name":"Customer Example"},... (997 more customers here){"id":"j8fa61zw-ew9z-432e-ba42-0adb4s2dabc3","name":"Customer Test"}]

# Microservice A Then continues to keep getting the next page until it has gotten all the customers

Projection

Providing a way for clients to choose which fields are returned in the response (representation of resource).
This helps in decreasing response time and payload size.

How to implement:
Use of “fields” query string parameter specify which fields to return in the response. This parameter would accept a comma-separated list of strings denoting the fields desired.

Best Practices

  • Field name used in projection should be case sensitive and appropriate error should be thrown by server in case of case-mismatch.
  • Invalid field name used in projection should be thrown as appropriate error by server.

Default Fields

Default fields is an optional concept. It should be used when resource payload contains large number of fields and not all fields are relevant to the client to help trim down the default payload size

Example

Scenario: Address is an expensive field we don’t want to return unless the consumer explicitly asks for it

Customer Resource fields:
id => default
name => default
phone => default
address => non-default

Here we will just do a normal call

# Request
GET /api/v1/customers# Response (Does not Return address as it's not default)
[ 
  { "id" : 1, "name" : "Customer 1", "phone" : "111-567-1234" }, 
  { "id" : 2, "name" : "Customer 2", "phone" : "222-767-1235" },
  { "id" : 3, "name" : "Customer 3", "phone" : "333-978-1678" } 
]

Here we will ask for specific fields

# Request
GET /api/v1/customers?fields=id,name,address# Response (Returns address as it was explicitly asked for, does not return phone as it was not asked for)
[ 
  { "id" : 1, "name" : "Customer 1", "address" : {"city" : "c1", "country" : "Country1"}}, 
  { "id" : 2, "name" : "Customer 2", "address" : {"city" : "c2", "country" : "Country2"}},
  { "id" : 3, "name" : "Customer 3", "address" : {"city" : "c3", "country" : "Country3"}} 
]

Notes

  • Once a field is marked as default, it MUST not be marked as non-default as this may be a breaking change for client.
  • In the swagger it should be documented which fields will not be returned by default
  • This can optionally be implemented for POST, PATCH and PUT in addition to GET if the smaller response size is desired in those cases

Miscellaneous

Health API

  • The health check API must be a separate REST service call in the microservice component.
  • These calls should be versionless and not have the customer section of the uri
  • The API must return the operational status of the component and its ability to connect to the downstream components that it depends on. e.g. Databases, queues, etc.
  • It should NOT call the /health endpoint of downstream microservices as this can lead to cascading failures
  • It is acceptable to call the /version endpoint of a downstream microservice to check basic connectivity
  • An advanced health check API can be extended to return performance information, such as connection times.
  • The results must be returned as an HTTP status code with JSON data.
  • Health API response should contain following details
  • Generic — ServiceName, Timestamp, Version, Status, And connection specific — ConnectionType, ConnectionStatus, Timestamp, ListenUrl, etc.
  • example -
  • { “timeStampUTC”: “2020–10–29T07:39:47.3224602Z”, “serviceName”: “Inbound Order Service”, “serviceProvider”: “Google Alphabet LLC”, “serviceVersion”: “1.0.0”, “type”: “health”, “status”: “Running”, “lastStartTimeUTC”: “2020–10–29T07:39:39.9232975Z”, “outboundConnectionStatus”: [{ “timeStampUTC”: “2020–10–29T07:39:47.3224608Z”, “type”: “OutboundConnectionStatus”, “name”: “Inbound-Order-Service-Database-Status”, “connectionType”: “Cassandra”, “connectionURLs”: [“cassandra:9042”], “connectionStatus”: “Active” }, { “timeStampUTC”: “2020–10–29T07:39:47.3224608Z”, “type”: “OutboundConnectionStatus”, “name”: “Inbound-Order-Service-SQS-Status”, “connectionType”: “SQS”, “connectionURLs”: [“http://sqs:9324”], “connectionStatus": “Active” }] }

Version API

  • Similar to health, the version API must be a separate REST service call in the microservice component.
  • The results must be returned as an HTTP status code with JSON data.
  • The API should return the following details
  • ServiceName, Timestamp, CurrentVersion, Supported versions, repo link, build number etc.
  • example -
  • { “timeStampUTC”: “2020–10–29T07:59:55.0856569Z”, “serviceName”: “Inbound Order Service”, “serviceProvider”: “Google Alphabet LLC”, “serviceVersion”: “1.0.0”, “type”: “version”, “buildCommitSHA”: “60a421afc030ef8304b4a9498d664423a47fdfc”, “repository”: “https://github.com/saas/inbound-order-service", “supportedAPIVersions”: [“v1”], “buildNumber”: “1.0.0–0” }

Headers

  • HTTP headers can be used to pass any metadata related to request or response. It must contain only metadata and nothing related to fields, parameters, etc.
  • Transaction ID in headers -
  • Header key — ‘X-Request-Id’
  • A transaction ID is used to correlate HTTP requests between a client and server.
  • Hence, every service should read the TransactionID header from request and in case its empty, the service should create a new one. This transaction ID must be forwarded to any further API calls and should be used in logging.
  • Content-type -
  • Service should always return a ‘Content-Type’ in the response header which represents the media type of the response body.

Conclusion

1. Use nouns but no verbs

2. GET method and query parameters should not alter the state

3. Use plural nouns

4. Use sub-resources for relations

5. Use HTTP headers for serialization formats

6. Use HATEOAS

7. Provide filtering, sorting, field selection and paging for collections

8. Version your API

9. Handle Errors with HTTP status codes

10. Allow overriding HTTP method

Wrapping Up

Developers need to spend some time while designing REST APIs, as the API can make a service very easy to use or extremely complex. Additionally, the maturity of the APIs can be easily documented by using the Richardson Maturity Model.

If you’d like to share your thoughts on integrating with REST services connect with me on https://www.linkedin.com/in/kalpit-sharma/

If this post was helpful, please click the clap 👏 button below a few times and follow to show your support for the author 👇

)