REST API Guidelines
Introduction
Our digital products we produce is centred around decoupled services that is provided via RESTful APIs with JSON payloads. Every team that produce software to the digital platform own, deploy and operate their APIs. This guide provide guidance for how to design high-quality, long-lasting APIs that fits in to the overall platform strategy.
We adopt an “API first” mindset, where we treat APIs as products. This helps us to facilitates a service ecosystem, which can be evolved more easily and be used to experiment quickly with new business ideas by recombining core capabilities. It´s also easy to implement cross platform and can generate client libraries with ease with tools.
APIs should be stable even if we replace complete service implementation including its underlying technology stack.
We focus on:
- API first mindset
- REST all the things
- Design for cloud native
- Have a “SaaS mindset”
We adhere to “Postels Law” and stipulates:
We are liberal in what we accept, but conservative in what we send
We encourage developers to define APIs first, before coding its implementation, using a standard specification language, this will help to get early review feedback from peers and client developers. We do this via the Open API specification where we only use U.S. English as preferred language.
Teams are responsible for using these guidelines during API development and are encouraged to contribute to this guideline via pull requests.
Hostname location and URLs
Humans should be able to easily read and construct URLs. This facilitates exploration and interaction with our platform without a well-supported client library.
All public API endpoints shall use https and use the service name in the URL, like https://api.[service].truesec.app for production workloads if they are part of the overall platform. Services that can generate URLs longer than 2,083 characters shall make accommodations for the clients they wish to support.
Consistent with our privacy policy, clients should not transmit personally identifiable information (PII) parameters in the URLs.
Resource orientation
REST is centered around business (data) entities exposed as resources that are identified via URIs and can be modified via standardized CRUD-like http methods.
- Use nouns as names
- Use pluralize names
- Use URL friendly resource names
- Use as domain-specific resource names as possible
- Use UUID as identifiers
- Never nest resource more then two levels deep
- Never end with a trailing slash
- Keep URLs verb-free
- Identify resources and sub-resources via path segments
Avoid introducing dependencies between the web API and the underlying data store. For example, if your data is stored in a relational database, the web API doesn’t need to expose each table as a collection of resources. Instead, think of the web API as an abstraction of the database. Instead introduce a mapping layer between the database and the web API. This keep the client applications isolated from changes to the underlying database scheme.
Instance isolation
In most API/services we do have separation of instances (be it on multiple workspaces or tenant), we always use UUID as separator for this.
Examples
https://api.truesec.app/tenants/38631565-d3b2-4a03-b177-f76c032a2c08
https://api.truesec.app/workspaces/de5c23f2-5fdd-4371-b92c-2ff0b50abf8e/incidents/aeb2d251-34ae-4fb1-9a4a-1dba63acc1c4HTTP Methods
The table below lists the http methods used by our services:
| Method | Description | Response Status Code |
|---|---|---|
| PUT | Create/Replace the whole resource | 200-OK (if nothing is changed), 201-Created |
| POST | Create new resource (ID set by service) | 201-Created with URL of created resource |
| POST | Action | 200-OK, 204-No Content (only when nothing returned in response body), 202-Accepted (only when long running task) |
| GET | Read (i.e. list) a resource collection | 200-OK |
| GET | Read the resource | 200-OK |
| DELETE | Remove the resource | 204-No Content; avoid 404-Not Found |
HTTP Headers
All header values shall follow the syntax rules set forth in the specification where the header field is defined. Many HTTP headers are defined in RFC7231, however a complete list of approved headers can be found in the IANA Header Registry.
The table below lists the headers used by our APIs
| Header Key | Applies to | Example |
|---|---|---|
| authorization | Request | Bearer eyJ0…Xd6j |
| traceparent | Request | (see Distributed Tracing) |
| tracecontext | Request | (see Distributed Tracing) |
| accept | Request | application/json |
| If-Match | Request | ”67ab43” or * (no quotes) (see Conditional Requests) |
| If-None-Match | Request | ”67ab43” or * (no quotes) (see Conditional Requests) |
| If-Modified-Since | Request | Sun, 06 Nov 1994 08:49:37 GMT (see Conditional Requests) |
| If-Unmodified-Since | Request | Sun, 06 Nov 1994 08:49:37 GMT (see Conditional Requests) |
| api-version | Request | 1 (see Versioning) |
| date | Both | Sun, 06 Nov 1994 08:49:37 GMT (see RFC7231, Section 7.1.1.2 ) |
| content-type | Both | application/json |
| content-length | Both | 1024 |
| ETag | Response | ”67ab43” (see Conditional Requests) |
| last-modified | Response | Sun, 06 Nov 1994 08:49:37 GMT |
| retry-after | Response | 180 (see RFC 7231, Section 7.1.3 ) |
Content types
Use JSON (RFC 7159) to represent structured (resource) data passed with HTTP requests and responses as body payload. The JSON payload shall use a JSON object as top-level data structure (if possible) to allow for future extension. This also applies to collection resources, where you ad-hoc would use an array. We use UTF-8 encoding and valid unicode strings. JSON property names shall be camelCased.
Non-JSON media types may be supported, if you stick to a business object specific standard format for the payload data. This could be image data format (JPG, PNG, GIF), document format (PDF, DOC, ODF, PPT), or archive format (TAR, ZIP).
Generic structured data interchange formats other than JSON (e.g. XML, CSV) may be provided, but only additionally to JSON as default format using content negotiation, an only for specific use cases where clients may not interpret the payload structure! We strongly advice to not use any other format than JSON as inputs.
Data formats
Primitive values shall be serialized to JSON following the rules of RFC8259 .
Important note for 64bit integers: JavaScript will silently truncate integers larger than Number.MAX_SAFE_INTEGER (2^53-1) or numbers smaller than Number.MIN_SAFE_INTEGER (-2^53+1). If the service is expected to return integer values outside the range of safe values, strongly consider returning the value as a string in order to maximize interoperability and avoid data loss.
| Type | Format | Specification | Example |
|---|---|---|---|
| integer | int32 | 4 byte signed integer between -231 and 231-1 | 7721071004 |
| integer | int64 | 8 byte signed integer between -263 and 263-1 | 772107100456824 |
| integer | bigint | arbitrarily large signed integer number | 77210710045682438959 |
| number | int32 | binary32 single precision decimal number — see IEEE 754-2008/ISO 60559:2011 | 3.1415927 |
| number | double | binary64 double precision decimal number — see IEEE 754-2008/ISO 60559:2011 | 3.141592653589793 |
| number | decimal | arbitrarily precise signed decimal number | 3.141592653589793238462643383279 |
| string | date-time | RFC 3339 internet profile — subset of ISO 8601 | ”2019-07-30T06:43:40.252Z” |
Query options
| Parameter name | Type | Description | Example |
|---|---|---|---|
name | string | a property on the resource type that selects the resources to be returned | https://api.truesec.app/tenants?name=test |
orderBy | string | a property value on the model that is supported | https://api.truesec.app/tenants?orderBy=name |
pageNumber | integer | an offset into the collection of the first resource to be returned | https://api.truesec.app/tenants?pageNumber=1 |
pageSize | integer | the maximum number of resources to include in a single response | https://api.truesec.app/tenants?pageSize=50 |
NOTE
Filtering, Sorting and Pagination operations can all be performed against a given collection. When these operations are performed together, the evaluation order shall be:
- Filtering. This includes all range expressions performed as an AND operation.
- Sorting. The potentially filtered list is sorted according to the sort criteria.
- Pagination. The materialized paginated view is presented over the filtered, sorted list. This applies to both server-driven pagination and client-driven pagination.
Pagination
Operations that return a collection of resources shall consider pagination. There are hard limits to the payload size of HTTP responses, and when the size of a collection or the resources themselves can grow arbitrarily large there is the risk of exceeding this limit if the operation does not support pagination. Later adding support for pagination is a breaking change so it should be supported in the initial design of the service if there is any possibility that it will eventually be needed! Services should almost always support server-driven paging and may optionally support client-driven paging.
The service determines how many items to include in the response and may choose a different number for different collections and even for different pages of the same collection. An operation may allow the client to specify a maximum number of items in a response with an optional pageSize parameter. Operations that support pageSize should return no more than the value specified in pageSize but may return fewer.
Clients shall be resilient to collection data being either paged or non-paged for any given request.
Example of a paged result data
{
"items": [
{
"id": "87acb32b-199d-4e48-a1ee-8216bd59e1c3",
"title": "Incident 1",
"description": "This is a incident 1",
"status": "WAITING",
"createdAt": "2022-08-17T07:40:51.3898292Z",
"updatedAt": "2022-08-17T07:40:51.3898296Z"
},
{
"id": "9e87b440-42b2-4694-9d2c-56876ba31611",
"title": "Incident 2",
"description": "This is a incident 2",
"status": "NEW",
"createdAt": "2022-08-17T07:40:51.3900369Z",
"updatedAt": "2022-08-17T07:40:51.3900371Z"
}
],
"pageNumber": 1,
"pageSize": 50,
"totalItems": 2,
"totalPages": 1
}Versioning
For using explicit version (try to avoid), the consumer shall specify this explicit with an api-version header with format 1.0. We always default to the latest version.
Changes that impact the backwards compatibility of an API are a breaking change.
Use a new version number to signal that support for existing clients will be deprecated in the future. When introducing a new version, services shall provide a clear upgrade path for existing clients and develop a plan for deprecation.
Online documentation of versioned services shall indicate the current support status of each previous API version and provide a path to the latest version.
Partial responses
A resource may contain large binary fields, such as files or images. To overcome problems caused by unreliable network and to improve response times, consider enabling binary resources to be retrieved in chunks. Also consider implementing HTTP HEAD requests for these resources. A HEAD request is similar to a GET request, except that it only returns the HTTP headers that describe the resource.
Long running actions
If the need to process long running actions arise in the api, one should accept the trigger via a POST request that returns the action code 202-Accepted that include a location header where a status monitoring resource reside.
For all long-running actions, the client can than run a GET request on the status monitor resource to obtain the current status of the operation.
In the event that the long running task creates a new resource on completion, the status monitor resource GET request can result in a 303 status code, with location header set to the new resource created.
Example of a monitoring resource
HTTP/1.1 200 OK
Content-Type: application/json
{
"status":"In progress",
"link": {
"rel":"cancel",
"method":"delete",
"href":"/api/status/0721aa2b-61a8-4835-948e-25fcdb4d9792"
}
}Example of a monitoring resource status that show where to locate a new resource created
HTTP/1.1 303 See Other
Location: /api/tenants/40a26375-919d-472b-b1c3-1ac937dc10d9Error handling
For non-success conditions, developers should be able to write one piece of code that handles errors consistently across different REST API services. This allows building of simple and reliable infrastructure to handle exceptions as a separate flow from successful responses.
The error response shall be a single JSON object. The value shall be a JSON object. We conform to RFC 7231 and send a status code of 400. For problem details we use RFC 7807 .
Example of an 400 error object for invalid input parameters
{
"type": "https://example.net/validation-error",
"title": "Your request parameters didn't validate.",
"invalid-params": [
{
"name": "age",
"reason": "shall be a positive integer"
},
{
"name": "color",
"reason": "shall be 'green', 'red' or 'blue'"
}
]
}Secure endpoints
Every API endpoint shall be protected and armed with authentication and authorization. The majority of our APIs are protected using JWT tokens provided by the platform (both external and internal). You should use the http typed Bearer Authentication security scheme.
Assigned permissions and scopes
APIs shall define permissions to protect their resources. Thus, at least one permission shall be assigned to each API endpoint. On public APIs we use somewhat opaque tokens that mostly express whom the user is and then exchange the token against our access control system with claims where every action shall be expressed.
Example of claim
| Application | Resource | Operation | Example |
|---|---|---|---|
| soc | incidents | read | soc.incidents.read |
| admin | members | write | admin.members.write |
CORS
API compliant with our guidelines shall support CORS (Cross Origin Resource Sharing). Web developers usually don’t need to do anything special to take advantage of CORS. All of the handshake steps happen invisibly as part of the standard XMLHttpRequest calls they make.
Add an Access-Control-Max-Age pref response header containing the number of seconds for which this preflight response is valid.
Distributed Tracing
Distributed tracing allows the consumer to trace their code from frontend to backend and between services. The distributed tracing library creates spans (units of unique work) to facilitate tracing. Each span is in a parent-child relationship. As you go deeper into the hierarchy of code, you create more spans. We conform to OpenTelemetry for tracing.
Conditional Requests
An ETag should also be used to reflect the create, update, and delete policies of our services. We implement an “optimistic concurrency” strategy, where the incoming state of the resource is first compared against what currently resides in the service.
| Operation | Header | Value | ETag check | Return code | Response |
|---|---|---|---|---|---|
| PATCH / PUT | If-None-Match | * | check for any version of the resource (’*’ is a wildcard used to match anything), if none are found, create the resource. | 200-OK or 201-Created | Response header shall include the new ETag value. Response body SHOULD include the serialized value of the resource (typically JSON). |
| PATCH / PUT | If-None-Match | * | check for any version of the resource, if one is found, fail the operation | 412-Precondition Failed | Response body SHOULD return the serialized value of the resource (typically JSON) that was passed along with the request. |
| PATCH / PUT | If-Match | value of ETag | value of If-Match equals the latest ETag value on the server, confirming that the version of the resource is the most current | 200-OK or 201-Created | Response header shall include the new ETag value. Response body SHOULD include the serialized value of the resource (typically JSON). |
| PATCH / PUT | If-Match | value of ETag | value of If-Match header DOES NOT equal the latest ETag value on the server, indicating a change has ocurred since after the client fetched the resource | 412-Precondition Failed | Response body SHOULD return the serialized value of the resource (typically JSON) that was passed along with the request. |
| DELETE | If-Match | value of ETag | value matches the latest value on the server | 204-No Content | Response body SHOULD be empty. |
| DELETE | If-Match | value of ETag | value does NOT match the latest value on the server | 412-Preconditioned Failed | Response body SHOULD be empty. |