APIs should support techniques for reducing bandwidth based on client needs. This holds for APIs that (might) have high payloads and/or are used in high-traffic scenarios like the public Internet and telecommunication networks. Typical examples are APIs used by mobile web app clients with (often) less bandwidth connectivity. (Zalando is a 'Mobile First' company, so be mindful of this point.)
Common techniques include:
-
compression of request and response bodies (see {SHOULD} use
gzip
compression) -
querying field filters to retrieve a subset of resource attributes (see {SHOULD} support partial responses via filtering below)
-
{ETag} and {If-Match}/{If-None-Match} headers to avoid re-fetching of unchanged resources (see [182])
-
{Prefer} header with
return=minimal
orrespond-async
to anticipate reduced processing requirements of clients (see [181]) -
[pagination] for incremental access of larger collections of data items
-
caching of master data items, i.e. resources that change rarely or not at all after creation (see {MUST} document cacheable
GET
,HEAD
, andPOST
endpoints).
Each of these items is described in greater detail below.
A servers and clients should support gzip
content encoding to reduce the data
transported over the network and thereby speed up response times, unless there
is a good reason against this. Good reasons against compression are:
-
The content is already compressed, or
-
The server has not enough resources to support compression.
While gzip
content encoding should be the default, servers must also support
unencoded content for testing. This is ensured by content negotiation using the
{Accept-Encoding} request header (see {RFC-9110}#section-12.5.3[RFC 9110
Section 12.5.3]). Successful compression is signaled via the {Content-Encoding}
response header (see {RFC-9110}#section-8.4[RFC 9110 Section 8.4]). Clients and
servers may support other compression algorithms as compress
, deflate
, or
br
.
To signal server support for compression the API specification should define both, the {Accept-Encoding} request header and the {Content-Encoding} response header. This can be done by simply referencing the standard header definitions as follows (see also the default definition below):
components:
parameters|headers:
Accept-Encoding:
$ref: 'https://opensource.zalando.com/restful-api-guidelines/models/headers-1.0.0.yaml#/Accept-Encoding'
Content-Encoding:
$ref: 'https://opensource.zalando.com/restful-api-guidelines/models/headers-1.0.0.yaml#/Content-Encoding'
link:../includes/accept-encoding.yaml[role=include]
link:../includes/content-encoding.yaml[role=include]
Note: since most server and client frameworks still do not support
gzip
-compression out-of-the-box you need to manually activate it, e.g.
Spring Boot,
or add a dedicated middleware, e.g. gin-gionic/gin.
Depending on your use case and payload size, you can significantly reduce
network bandwidth need by supporting filtering of returned entity fields.
Here, the client can explicitly determine the subset of fields he wants to
receive via the {fields} query parameter. (It is analogue to
GraphQL fields
and simple
queries, and also applied, for instance, for
Google
Cloud API’s partial responses.)
GET http://api.example.org/users/123 HTTP/1.1
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": "cddd5e44-dae0-11e5-8c01-63ed66ab2da5",
"name": "John Doe",
"address": "1600 Pennsylvania Avenue Northwest, Washington, DC, United States",
"birthday": "1984-09-13",
"friends": [ {
"id": "1fb43648-dae1-11e5-aa01-1fbc3abb1cd0",
"name": "Jane Doe",
"address": "1600 Pennsylvania Avenue Northwest, Washington, DC, United States",
"birthday": "1988-04-07"
} ]
}
GET http://api.example.org/users/123?fields=(name,friends(name)) HTTP/1.1
HTTP/1.1 200 OK
Content-Type: application/json
{
"name": "John Doe",
"friends": [ {
"name": "Jane Doe"
} ]
}
The {fields} query parameter determines the fields returned with the response
payload object. For instance, (name)
returns users
root object with only
the name
field, and (name,friends(name))
returns the name
and the nested
friends
object with only its name
field.
OpenAPI doesn’t support you in formally specifying different return object
schemes depending on a parameter. When you define the field parameter, we
recommend to provide the following description: Endpoint supports filtering
of return object fields as described in
[Rule #157](https://opensource.zalando.com/restful-api-guidelines/#157)
The syntax of the query {fields} value is defined by the following BNF grammar.
<fields> ::= [ <negation> ] <fields_struct>
<fields_struct> ::= "(" <field_items> ")"
<field_items> ::= <field> [ "," <field_items> ]
<field> ::= <field_name> | <fields_substruct>
<fields_substruct> ::= <field_name> <fields_struct>
<field_name> ::= <dash_letter_digit> [ <field_name> ]
<dash_letter_digit> ::= <dash> | <letter> | <digit>
<dash> ::= "-" | "_"
<letter> ::= "A" | ... | "Z" | "a" | ... | "z"
<digit> ::= "0" | ... | "9"
<negation> ::= "!"
Note: Following the principle of least astonishment, you should not define the {fields} query parameter using a default value, as the result is counter-intuitive and very likely not anticipated by the consumer.
Embedding related resources (also know as Resource expansion) is a great way to reduce the number of requests. In cases where clients know upfront that they need some related resources they can instruct the server to prefetch that data eagerly. Whether this is optimized on the server, e.g. a database join, or done in a generic way, e.g. an HTTP proxy that transparently embeds resources, is up to the implementation.
See [137] for naming, e.g. "embed" for steering of embedded resource expansion. Please use the BNF grammar, as already defined above for filtering, when it comes to an embedding query syntax.
Embedding a sub-resource can possibly look like this where an order resource has its order items as sub-resource (/order/{orderId}/items):
GET /order/123?embed=(items) HTTP/1.1
{
"id": "123",
"_embedded": {
"items": [
{
"position": 1,
"sku": "1234-ABCD-7890",
"price": {
"amount": 71.99,
"currency": "EUR"
}
}
]
}
}
Caching has to take many aspects into account, e.g. general cacheability of response information, our guideline to protect endpoints using SSL and OAuth authorization, resource update and invalidation rules, existence of multiple consumer instances. Caching is in best case complex, e.g. with respect to consistency, in worst case inefficient.
As a consequence, client side as well as transparent web caching should be avoided, unless the service supports and requires it to protect itself, e.g. in case of a heavily used and therefore rate limited master data service, i.e. data items that rarely or not at all change after creation.
As default, servers and clients should always set the {Cache-Control} header to {Cache-Control-no-store} and assume the same setting, if no {Cache-Control} header is provided.
Note: There is no need to document this default setting. However, please make sure that your framework is attaching these header values by default, or ensure this manually, e.g. using the best practice of Spring Security as shown below. Any setup deviating from this default must be sufficiently documented.
Cache-Control: no-cache, no-store, must-revalidate, max-age=0
If your service really requires to support caching, please observe the following rules:
-
Document all [cacheable] {GET}, {HEAD}, and {POST} endpoints by declaring the support of {Cache-Control}, {Vary}, and {ETag} headers in response. Note: you must not define the {Expires} header to prevent redundant and ambiguous definition of cache lifetime. A sensible default documentation of these headers is given below.
-
Take care to specify the ability to support caching by defining the right caching boundaries, i.e. time-to-live and cache constraints, by providing sensible values for {Cache-Control} and {Vary} in your service. We will explain best practices below.
-
Provide efficient methods to warm up and update caches (see ).
Usually, you can reuse the standard {Cache-Control}, {Vary}, and {ETag} response header definitions provided by the guideline as follows:
components:
parameters|headers:
Cache-Control:
$ref: 'https://opensource.zalando.com/restful-api-guidelines/models/headers-1.0.0.yaml#/Cache-Control'
Vary:
$ref: 'https://opensource.zalando.com/restful-api-guidelines/models/headers-1.0.0.yaml#/Vary'
ETag:
$ref: 'https://opensource.zalando.com/restful-api-guidelines/models/headers-1.0.0.yaml#/ETag'
See also [182].
To make best use of caching in micro service environments you need to provide efficient methods to warm up and update caches, e.g. as follows:
-
In general, you should support
ETag
Together WithIf-Match
/If-None-Match
Header on all [cacheable] endpoints. -
For larger data items you should support {HEAD} requests or more a bit more efficient {GET} requests with {If-None-Match} header to check for updates.
-
For small data sets you should provide a get full collection {GET} endpoint that supports an {ETag} for the collection in combination with a {HEAD} or {GET} requests with {If-None-Match} to check for updates.
-
For medium sized data sets provide a get full collection {GET} endpoint that supports an {ETag} for the collection in combination with [pagination] and {entity-tag} filtering {GET} requests for limiting the response to changes since the provided {entity-tag}. Note: this is not supported by generic client and proxy caches on HTTP layer.
Hint: For proper cache support, you must return {304} without content on a
failed {HEAD} or {GET} request with If-None-Match: <entity-tag>
instead of {412}. For {ETag} see also [182].
The default setting for {Cache-Control} should contain the private
directive
for endpoints with standard OAuth authorization, as well as the
must-revalidate
directive to ensure, that the client does not use stale cache
entries. Last, the max-age
directive should be set to a value between a few
seconds (max-age=60
) and a few hours (max-age=86400
) depending on the change
rate of your master data and your requirements to keep clients consistent.
Cache-Control: private, must-revalidate, max-age=300
The default setting for {Vary} is harder to determine correctly. It depends on the API endpoint, e.g. whether it supports compression, accepts different media types, or requires other request specific headers. To support correct caching you have to carefully choose the value. However, a good first default may be:
Vary: accept, accept-encoding
Anyhow, this is only relevant, if you encourage clients to install generic HTTP layer client and proxy caches.
Generic client and proxy caching on HTTP level is hard to configure. Therefore, we strongly recommend to attach the (possibly distributed) cache directly to the service (or gateway) layer of your application. This relieves the service from interpreting the {vary} header and greatly simplifies the usage patterns of the {Cache-Control} and {ETag} headers. Moreover, is highly efficient with respect to cache performance and overhead, and allows to support more advanced cache update and warm up patterns.
Anyhow, please carefully read {RFC-9111}[RFC 9111] before adding any client or proxy cache.