Using a microservice architecture allows development teams to work separately, delivering work faster and focusing on a specific part of the business domain. Working independently means that developers are free to make their own choices. While that’s great, there’s the risk of building the tower of Babel.
Imagine this simple example: team A is developing an address book service, team B is working on an invoice service. The first team is using Java, the second is using .NET. They both follow the default options for their stack. When we put service A and B together, we’ll have an address book API which uses camelCase for the payload of the requests, while the invoice service uses PascalCase. Technically it works, but it looks unprofessional and it will be difficult to integrate with.
We need an API style guide to ensure that the API looks consistent. It’s the same as using a code style guide to ensure that the code looks as if it was written by one person.
In this post, I’m describing the conventions we currently use at work. This is all about a RESTful design, influenced a bit by the fact that we use Swagger and Java / Spring Boot. And just like code style guide, it’s all a bit subjective. It’s more important to follow a specific set of rules rather than arguing about tabs vs spaces, lowercase vs uppercase, etc.
URL Conventions
Path Conventions
The URL is the endpoint to a single resource (HTTP, RESTful) or operation (HTTP, RPC style).
Example of a URL that identifies uniquely a shipment:
- https://api.acme.com/v1/shipping/shipments/42
Example of a URL that maps to an operation:
- https://api.acme.com/v1/auth/availability
We favour RESTful design, but sometimes RPC is more natural.
In any case, the naming conventions for the URLs are:
- no spaces or special characters
- only lowercase
- separate words with hyphens (e.g.
address-book
. Notaddressbook
oraddress_book
).
Query String Parameters Conventions
- no spaces or special characters
- only lowercase
- separate words with underscores (e.g.
per_page
. Notperpage
,perPage
, orper-page
).
Payload Conventions
The body of the request is a JSON object. All properties need to use camelCase, which is the default for Java and JavaScript (.NET by default is using PascalCase).
Example:
{
"firstName": "Tom",
"lastName": "Jones"
}
Enums
Enums can appear in the body of a request, as well as in the query string as parameters. We use FULL_CAPS for enums, because it makes the generated Java code easier to understand.
Examples: ASC
, DESC
, EXPRESS_DELIVERY
.
Swagger Conventions
Operation IDs
All operation identifiers are camelCase e.g. createAddress
.
This matches the convention for Java method names. This is reflected in the generated code but also in the documentation.
Definitions
All definitions (model names) are PascalCase e.g. Shipment
, PriceResult
.
This matches the convention for Java class names and it is reflected in the generated code and documentation.
Documentation
All elements need to have a description. The description will be published in the documentation so it needs to be helpful and in proper English.
Tags
All operations need to be assigned to a tag. Tags have an impact on the documentation but also on the generated client SDKs. If you don’t use a tag, they get grouped in the “Default” tag, which is most likely not what you want.
Example:
paths:
/addresses:
get:
operationId: getAddresses
tags:
- AddressBook
The naming convention for tags is PascalCase.
(Optional) Example values
It is possible to define example values explicitly with the example property:
firstName:
type: string
description: The first name of a person
example: John
This can be useful in the documentation, but it is not required.
Hide the type of identifiers
Sometimes we use numeric identifiers (e.g. entities coming from PostgreSQL). Sometimes we use GUIDs. Sometimes identifiers are strings.
The caller of the API should not care about our identifiers. It should all be just a string. This has some benefits:
- hides the details of our implementation
- allows us to change the underlying storage to even a different ID type without changing the API security through obscurity
Example:
Address:
properties:
id:
type: string
{
"id": "42"
}
REST Conventions
Response Codes
We use the appropriate status codes in the response.
- 2xx indicates a success
- 4xx indicates a client error
- 5xx indicates a server error
More specifically:
- 200 indicates a success
- 201 (CREATED) indicates that a new entity was created. This should be used as the response to a POST operation which created a new entity.
- 400 indicates a validation error
- 401 indicates that the user is not properly authenticated (missing or bad authentication)
- 403 indicates that the user is authenticated but is not authorized to access the resource or perform the requested action
- 404 indicates that the requested resource is not found
- 405 Method not allowed (e.g. GET instead of POST)
- 406 Not acceptable (content type is not accepted)
- 409 (CONFLICT) indicates that an entity already exists
See here for 401 vs 403.
Which response codes should be documented?
You should document only the response codes that add some value to the user of the API. We assume that the user of the API understands the difference between a client error (4xx) and a server error (5xx). It is not our intention to re-document the HTTP protocol.
Example:
200 - User added to role successfully.
Example of redundant documentation:
400 - Invalid request. This can be due to missing or invalid fields.
Errors
We use the same object for reporting errors in all services. It is modelled after Spring’s exception so that we have the same data model for exceptions coming from our code as well as exceptions coming from Spring itself (e.g. validation errors are handled by Spring).
The object is defined in the source code like this:
ErrorInfo:
type: "object"
description: "An error thrown by the API"
properties:
timestamp:
type: "string"
description: "The date-time when the error occurred"
status:
type: "number"
format: "int32"
description: "The status code of the error"
error:
type: "string"
description: "The type of the error"
message:
type: "string"
description: "A message describing the error"
path:
type: "string"
description: "The URL path where the error occurred"
errors:
type: "array"
description: "A collection of details about the errors"
items:
$ref: "#/definitions/ErrorDetail"
ErrorDetail:
type: "object"
description: "Details about an error, usually specific to a field."
properties:
defaultMessage:
type: "string"
description: "A detailed error message"
objectName:
type: "string"
description: "The name of the object that caused the error"
code:
type: "string"
description: "A code for the specific error"
HTTP Verbs
We use the appropriate HTTP verb per operation.
Verb | Typical Use Case |
---|---|
GET | Get a resource |
POST | Create a new resource |
PUT | Update an existing resource |
DELETE | Delete an existing resource |
Typically it is expected that calling a GET operation multiple times does not create side-effects.
CRUD Conventions
Several services define a set of CRUD (create, read, update, delete) operations on a resource. Let’s see an example for the address book:
Operation Id | HTTP Verb | HTTP Path | HTTP Parameters | Payload |
---|---|---|---|---|
getAddresses | GET | /addresses | Searching, Sorting and Pagination | - |
createAddress | POST | /addresses | - | Address |
updateAddress | PUT | /addresses | - | Address |
getAddress | GET | /addresses/{addressId} | - | - |
deleteAddress | DELETE | /addresses/{addressId} | - | - |
Points of interest:
- the HTTP path uses the plural form addresses and not the singular address
- the operation IDs are prefixed with get, create, update and delete
- the HTTP path for a single address is a sub-path under the main path e.g. /addresses/42
Searching, Sorting and Pagination
When getting a list of objects, we typically want to limit the number of results, apply some sorting, and filter for something specifically.
Searching
This refers to free text search on one or more fields.
Each operation needs to document which fields are being taken into account.
We use the q
parameter for this type of search.
Examples:
- /addresses?q=Tom
- /addresses?q=1017
- /users?q=Leo
Sorting
Our conventions are based on GitHub conventions.
- the field to sort by is defined by the
sort
parameter. The valid values for this parameter depend on the object. The value should use camelCase, just like the field names being sorted. In the advanced case of nesting, use a dot to separate fields. - the sorting direction is defined by the
direction
parameter. As this is an enum, the value needs to be in FULL_CAPS. The valid values areASC
andDESC
.
Examples:
- /addresses?sort=firstName
- /addresses?sort=lastName&direction=DESC
- /shipments?sort=sender.company&direction=ASC
The direction parameter should be optional and the default value should be documented per operation.
Pagination
We follow GitHub’s API regarding pagination.
- current page is defined by the
page
parameter - page numbering is one-based, not zero-based
- omitting the page parameter will return the first page
- the page size is defined by the
per_page
parameter - omitting the
per_page
parameter will return a sensible default per operation, which should be documented
Examples:
- /addresses (fetches the first 10 results according to the default sorting rules)
- /addresses?page=2 (fetches the next 10 results)
- /addresses?page=3&per_page=20 (fetches addresses 41-60)