Design principles behind the Lucca API.
The goal of this document is to facilitate the work and minimize the effort of all API users at Lucca while protecting their investment and encouraging API adoption.
These guidelines lay down the foundation for collaboration, stability, and extensibility.
This API guideline contains:
The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “NOT RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals, as shown here.
In order to validate your OpenAPI specs with the guidelines, you may:
Make sure your OpenAPI specs is v3.x.
The spectral ruleset only supports OAS v3.x, ideally ^3.1.x
Install stoplight/spectral
Install Lucca API guidelines through npm
Lint your oas file with the spectral ruleset of this project
Lucca initially started as a software company that developed a single app. However, due to the founder’s seemingly insatiable appetite, many complementary apps were subsequently developed.
The proliferation of apps posed two significant challenges to the platform architecture:
Consequently, the platform MUST:
Availability is considered more important than consistency. The platform embraces eventual consistency, while being fully aware of the potential cost in terms of cognitive complexity for users and developers.
Simplicity through uniformity is considered more important than performance.
As a result, here are the principles behind the Lucca API design:
All APIs MUST be RESTful and JSON-based (charset = UTF-8). RESTful APIs tend to be less use-case specific and come with less rigid client / server coupling.
Regarding the REST constraints, all APIs MUST at least be at level 2 in Richardson’s maturity model.
Some level 3 (i.e. HATEOAS) features MAY be implemented, but we do not consider it a requirement. In the Lucca API, two features are implemented:
In a nutshell, the Design First principle means:
Defining APIs first outside the code aims at facilitating early review feedback and favoring:
Moreover, defining an API with a standardized specification format also facilitates:
Robustness Principle (Postel’s law)
“Be tolerant of inputs and strict on outputs”. In other words, enforce strict validation on responses, but be more lenient on requests. Have fail-safe defaults.
The degree of leniency may be negotiated with the client through the Prefer
HTTP header.
People are part of the system. Choose interfaces that match the user’s experience, expectations, and mental models. Avoid unexpected side-effects and black-box effects. Conform to standards and enforce consistency and uniformity.
Document the assumptions behind the design so that when the time comes to change it you can more easily figure out what else has to change. Expect not only to modify and replace modules, but also to remodularize as the system and its requirements become better understood.
You won’t get it right the first time, so make it easy to change. Do not try to anticipate future changes, you’ll mostly miss the mark. Take time to refactor existing components whenever the system changes.
Besides, let anyone comment on the design; you need all the help you can get.
Eventually, kill rarely used components: deterioration and corruption accumulate unnoticed.
As a rule of thumb resources should be defined to cover 90% of all its client’s use cases. A useful resource should contain as much information as necessary, but as little as possible. A great way to support the last 10% is to allow clients to specify their needs for more/less information by supporting filtering and embedding.
You SHOULD avoid excessive generality when modeling resources: if it is good for everything, then it is good for nothing.
Check every operation for authenticity (who), integrity (what), and authorization (can).
Document all access rights that may not be immediately understood ; all the more so if this affects data that could be considered sensitive. For example: granting someone the right to create leaves for another employee may reveal this employee’s work-contract dates if errors are thrown when attempting to create leaves outside the contract date range.
Paths SHOULD stick to the plural form of the name of the type of resource when not a singleton resource. In practice,
when the resource is listed in a collection, then the path should be equal to the type
of the collection representation.
Be compliant with the standardized HTTP semantics (refer to RFC-9110).
GET requests are used to read either a single or a collection of resource(s) representation(s).
GET
requests MUST be safe (i.e. they must not change the server state) and thus idempotent.
GET
request SHOULD be cacheable.
GET
requests for individual resources SHOULD return a 404 if the resource does not exist or it is not accessible to the authenticated user.
GET
requests for collection resources MAY return either 200 (if the collection is empty) or 404 (if the collection is missing or inaccessible).
GET
requests SHOULD NOT return a 202, and therefore should not be asynchronous.
GET
requests MUST NOT have a request body payload. Use query parameters, and in the worst case, a POST
request. Do not use headers for this.
GET
requests on collection resources SHOULD provide sufficient filter and pagination mechanisms.
PUT requests are used to update - or create if it does not already exist - entire resources.
The semantic is best described as “please put the enclosed representation at the resource mentioned by the URL, replacing any existing resource”.
PUT
requests MUST have an idempotent behavior.
PUT
requests SHOULD NOT usually be supported on collection resources. Otherwise, please refer to the “batch requests” part of this document.
PUT
requests SHOULD return a 201 (without a Location
header as the resource was created on the URL targeted by the PUT
request) whenever it translates
to the creation of a new resource ; and a 200 whenever it updates an existing one. A 202 MAY be returned to indicate asynchronous processing.
PUT
requests SHOULD return the representation of the updated/created resource whenever it contains server-generated properties.
PUT
requests SHOULD be used for resource creation whenever the client has the control over the URI (and thus, usually, the identifier).
PUT
requests SHOULD support If-Match
and If-None-Match
headers in order to given clients a better control over concurrency and avoid lost updates.
POST requests are usually used to create single resources on a collection resource endpoint, but may also be used to execute commands on single resources endpoint.
In regards to collections, its semantic is best described as “please add the enclosed representation to the collection resource identified by the URL”.
For single resource endpoints, its semantic is to be understood as “please execute the given well specified request on the resource identified by the URL”.
POST
requests do not require to be idempotent, but MAY be implemented in an idempotent way.
POST
requests MAY support batch creations through sending a collection (i.e. an object that list the representations of the items to create). Please refer to the
“batch requests” part of this document to learn more.
POST
requests, when intended for creation and successful, MUST return a 201 as well as the URI of the created resource(s) in the Location
HTTP header.
POST
requests MAY return a 202 when asynchronous, but SHOULD in this case return a Location
header for the client to be able to track the execution.
PATCH method extends HTTP to update parts of the resource representation
Please refer to RFC 5789.
In contrast to PUT, only a specific subset of the resource properties should be changed. The set of changes is represented in a format called a patch document passed as
payload and identified by a specific media type. The semantic is best described as “please change the resource identified by the URL according to my patch document”.
Generally, PATCH
requests give clients the ability to update a partial representation of the resource. The syntax and semantics of the patch document is not defined
in RFC-5789 and must be described in the API specification by using one (or several) specific media types:
application/json
. Send a subset of the object representation. Replaces the values of any listed property. Completely replaces all items of an array property.
Beware of the lack of control (and standard) such an implementation presents.application/merge-patch+json
. JSON Merge Patch standard, which is a standardized way of achieving
what’s described above.application/json-patch+json
. JSON Patch standard, which gives proper support for granular array manipulations.
It supports updating an array item identified via its index, but not via some of its properties.In general, as implementing the PATCH
method can prove quite tricky, we recommend handling updates through the PUT
method, which forces clients to send the complete
representation of resource, but gives better control (idempotency and no side-effects).
PATCH
requests SHOULD support If-Match
and If-None-Match
headers in order to given clients a better control over concurrency and avoid lost updates.
DELETE requests are used to delete resources.
The semantic is best described as “please permanently delete the resource identified by the URL”.
DELETE
requests MUST be idempotent.
DELETE
requests MAY be supported on collection resources, in order to support batch deletes. In this case, DELETE
requests on collection endpoints SHOULD
support query parameters in order to filter on the items of the collection to delete.
DELETE
request MUST return a 204 when successful. Any subsequent GET
request MUST return a 404.
DELETE
request MUST NOT have any request body. When needed, please implement the corresponding feature through a POST request.
DELETE
requests SHOULD support If-Match
and If-None-Match
headers in order to given clients a better control over concurrency and avoid lost updates.
HEAD requests are used to retrieve the header information of single resources and resource collections.
This method has exactly the same semantics as GET
, but only returns the headers (i.e. no response body).
As a result, like a GET
request, any HEAD
request MUST be safe, idempotent, and SHOULD be cacheable.
HEAD
requests SHOULD be supported in order to give clients the ability to efficiently lookup whether large resources or collection resources have been
updated in conjunction with the If-Match
and If-None-Match
headers.
Glossary: safe, idempotent, cacheable
If an idempotent implementation of an HTTP method that is not supposed to be idempotent is required, then please consider using the
Idempotency-Key
HTTP header (c.f. RFC).
The API describes resources, so the only place where actions should appear is in the HTTP methods. In URLs, use only nouns and avoid verbs.
Showing structural relationships in URIs can be a good way of revealing dependencies and/or access rights scoping.
Nonetheless, it also:
If you choose nonetheless to have sub-resources, then you SHOULD avoid having more than 3 levels of depth. Going deeper increases complexity as well as URL length (bear in mind some web browsers truncate URLs over 2,000 characters).
Pathing ambiguity naturally emerges from nested URL resources:
You must only use official HTTP status codes consistently with their intended semantics. Official HTTP status codes are defined via RFC standards and registered in the IANA Status Code Registry.
Code | Methods | MUST document? | RFC | Description |
---|---|---|---|---|
200 OK | all | ✅ | rfc9110 | General success response. Prefer a more specific success code when possible. |
201 Created | POST, PUT | ✅ | rfc9110 | Returned on successful resource creation, even if response body is empty. Used along the Location header when the created resource URI is indeterminate for clients. |
202 Accepted | POST, PUT, PATCH, DELETE | ✅ | rfc9110 | The request was successful and will be processed asynchronously. Only applicable to methods which change the state on the server side. |
204 No Content | PUT, PATCH, DELETE | ✅ | rfc9110 | Returned in place of a 200 if no response body is returned. Only applicable to methods which change the state of the resource. |
304 Not Modified | GET, HEAD | ✅ | rfc9110 | Returned whenever a conditional GET or HEAD request would have resulted in 200 response if the condition evaluated to false Usually: the resource has not been modified since the version passed via request headers If-Modified-Since or If-None-Match. For PUT, PATCH or DELETE requests, use 412 instead. |
400 Bad Request | all | ✅ | rfc9110 | Unspecific client error indicating that the server cannot process the request due to something that is perceived to be a client error (e.g. malformed request syntax, invalid request). Should also be delivered in case of input body fails business logic / semantic validation (instead of using 422). |
401 Unauthorized | all | ❌ | rfc9110 | Actually “Unauthenticated”. The credentials are missing or not valid for the target resource. For an API, this usually means that the provided token or cookie is not valid. As this can happen for almost every endpoint, APIs should normally not document this. |
403 Forbidden | all | ❌ | rfc9110 | The user is not authorized to change this resource. For an API, this can mean that the request’s token was valid, but was missing a scope for this endpoint. Or that some object-specific authorization failed. We recommend only documenting the second case. |
404 Not Found | all | ❌ | rfc9110 | The target resource was not found, either because it does not exist, or because the user cannot access it. This will be returned by most (not documented) paths on most APIs. For a PUT endpoint that does not support creation (only updates), then this might be returned if the resource does not exist. Apart from these special cases, this does not need to be documented. You should not return a 404 on a DELETE request on a resource that does not exist (or was already deleted), but a 204. You may return a 404 on write requests whenever the resource references another resource that is not accessible or does not exist. |
405 Method Not Allowed | all | ✅ | rfc9110 | The request method is not supported for this resource. Using this response code on a documented endpoint only makes sense if it depends on some internal resource state whether a specific method is allowed. Do not use it unless you have such a special use case, but then make sure to document it, making it clear why a resource might not support a method. |
406 Not Acceptable | all | ❌ | rfc9110 | Resource only supports generating content with content-types that are not listed in the Accept header sent in the request. |
409 Conflict | POST, PUT, PATCH, DELETE | ✅ | rfc9110 | The request cannot be completed due to conflict with the current state of the target resource. For example, you may get a 409response when updating a resource that is older than the existing one on the server, resulting in a version control conflict. If this is used, it MUST be documented. For successful robust creation of resources (PUT or POST) you should always return 200 or 204 and not 409, even if the resource exists already. If any If-* conditional headers cause a conflict, you should use 412 and not 409. Only applicable to methods which change something. |
410 Gone | all | ❌ | rfc9110 | The resource does not exist any longer (but did exist in the past), and will most likely not exist in the future. This can be used e.g. when accessing a resource that has intentionally been deleted. This normally does not need to be documented, unless there is a specific need to distinguish this case from the normal 404 |
411 Length Required | POST, PUT, PATCH | ✅ | rfc9110 | The server requires a Content-Length header for this request. This is normally only relevant for large media uploads. The corresponding header parameter should be marked as required. If used, it MUST to be documented (and explained). Only applicable for methods with a request body. |
412 Precondition Failed | PUT, PATCH, DELETE | ❌ | rfc9110 | Returned for conditional requests (If-Match and If-None-Match) if the condition failed. Used for optimistic locking. Normally only applicable to methods that change something. For HEAD or GET requests, use 304 instead. |
415 Unsupported Media Type | POST, PUT, PATCH | ❌ | rfc9110 | The client did not provide a supported Content-Type for the request body. Only applicable to methods with a request body. |
423 Locked | PUT, PATCH, DELETE | ✅ | rfc4918 | Used for pessimistic locking. |
428 Precondition Required | all | ❌ | rfc6585 | Server requires the request to be conditional (If-Match and If-None-Match headers). Instead of documenting this response status, the required headers should be documented and marked as required. |
429 Too Many Requests | all | ❌ | rfc6585 | The client is not abiding by the rate limits in place and has sent too many requests. |
500 Internal Server Error | all | ❌ | rfc9110 | A generic error indication for an unexpected server execution problem. |
501 Not Implemented | all | ❌ | rfc9110 | Server cannot fulfill the request (usually implies future availability, e.g. new feature). |
503 Service Unavailable | all | ❌ | rfc9110 | Service is temporarily down. Ideally, also return a Retry-After header giving clients the time to wait before retrying. |
Problem JSON is a standard way of describing errors. It is defined in the RFC 9457. It provides extensible human and machine readable failure information, beyond the HTTP response status code.
All 4xx and 5xx status codes MUST respond with a Problem JSON.
The Problem JSON object MUST be served with the content-type application/problem+json
.
The x-provider
OpenAPI extension is used to indicate which application offers concrete implementation for the given endpoint.
It makes it possible to bundle an application specification, with only their own subset of operations.
Stick to standards, like application/json
, application/problem+json
, application/hal+json
, etc…
You should avoid using custom media types like application/com.luccasoftware.leave+json
. Custom media
types beginning with x bring no advantage compared to the standard media type for JSON, and make automated
processing more difficult.
All services must normalize request paths before processing by removing duplicate and trailing slashes. Hence, all these requests must resolve to the same resource:
Polymorphic endpoints are harder to document, understand and use (both in reads and writes). So you should avoid implementing such an endpoint unless the different sub-types are very similar and/or there are so many sub-types that implementing a new endpoint for each would make the documentation harder to read.
If you choose to implement a polymorphic endpoint, please refer to the OpenAPI spec in order to properly document it.
In the context of the Lucca API, the discriminator MUST be the reserved type
property (if the root
resource is polymorphic, not a sub-object).
Use Internet JSON (RFC 7493) to represent resource representations passed with HTTP in requests as well as responses bodies.
As a consequence, any JSON payload MUST:
In a response body, you must always return a JSON object as a top level data structure to support future extensibility (through adding additional properties).
Why? Because we need to enforce consistent casing. We choose CamelCase to be consistent with the JavaScript syntax (JSON stands for JavaScript Object Notation).
Some resource representation properties are reserved and as such MUST NOT be used in any other way:
Property name | Type | Semantics | |
---|---|---|---|
id | string | null | Unique identifier of the resource in its collection. |
type | string<enum> | Name of the type of the resource. Should be equal to the name of its JSON schema in the spec. Used as the discriminator of a polymorphic resource. | |
url | string<uri> | Absolute URL to the resource. | |
totalCount | integer<int32> | null | Counts all items of a collection (i.e. across all pages) that match the query parameters. |
items | array (not nullable, but may be empty) | Lists the representations of all ressources in the collection page. | |
embedded | object | Lists representations of related resources. | |
links | object | Lists relationships with other resources which are not structural (i.e. not already present as reference properties of the target resource). |
Properties of certain types SHOULD be named with a certain prefix or suffix to further indicate their type:
Type | Prefix | Suffix |
---|---|---|
string<date> | N/A | On |
string<date-time> | N/A | At |
boolean | “is”, ”has“, “can”… | N/A |
Regarding booleans: do name boolean properties with an affirmative phrase (canDo
instead of cantDo
) in order to avoid double negations ; and prefix them with is
, can
, or has
whenever it adds value.
Enumerations should be represented as string typed OpenAPI definitions of request parameters or schemas properties. Enum values (declared either via enum
or x-extensible-enum
) need to consistently use the upper-snake case format, e.g. VALUE or YET_ANOTHER_VALUE. This is in order to better discriminate them from other properties.
Please refer to the OpenAPI spec for available formats.
This is enforced in order to prevent clients from guessing the precision incorrectly, and thereby changing the value unintentionally.
You MUST use the string typed formats date, date-time, time, duration, or period for the definition of date and time properties. The formats are based on the standard RFC 3339 internet profile (a subset of ISO 8601).
Properties that represent durations and time intervals SHOULD be represented as strings formatted as defined by ISO 8601 (RFC 3339).
Please note that Extended Date/Time Format (EDTF) defines an extension to express open ended time intervals that could be very convenient in query parameters filtering.
This is in order to avoid weird behaviors due to fallbacks (particularly on the server’s timezone).
DateTimeOffset
, then clients MUST send the offset. Otherwise, return a Problem.Date(Only)
, then clients MUST not send a time component (nor an offset, even though it’s valid). Otherwise, return a Problem.DateTime
and a DateTimeOffset
. Otherwise, return a Problem.Type | Standard | Description |
---|---|---|
Country | https://en.wikipedia.org/wiki/ISO_3166-1_alpha-3 | Three letters country code. |
Language | https://www.rfc-editor.org/rfc/rfc4646.html | Language tag. |
Currency | https://en.wikipedia.org/wiki/ISO_4217 | Three letters currency code. |
Always use string rather than number type for identifiers in the Web layer, as it gives you more flexibility to evolve the identifier naming scheme.
Be aware that monotonically increasing numeric identifiers may reveal confidential information to non-privileged clients. In which case you may consider random generators.
UUIDs may solve common issues:
But they have major drawbacks:
Advantages:
In which case, creation SHOULD be handled through a PUT
request.
Empty array values can unambiguously be represented as the empty list, i.e. []
.
A boolean is essentially a closed enumeration of two values, true
and false
. If the content has a meaningful null value, then we recommend replacing the boolean with enumeration of named values or statuses (e.g. “yes”, “no”, “undefined”).
OpenAPI makes it possible to mark a property as nullable (oas2: nullable: true
; oas3: type: ['null', …]
) or non required (through the required property of an object).
A non-required property may be absent from the body. A nullable property may be null.
In other words, we could make a distinction between the two cases. But we decided against.
You MUST consider that an absent property is equivalent to declaring it null
.
Even though an absent property is equivalent to it being null
, we recommend avoiding elastic schema definitions. Always serializing all properties makes de-serializing easier in many languages and libraries.
When describing an amount of money, then you must use the common Money
object.
You MUST NOT use this schema for inheritance. Treat it as a closed data type.
You SHOULD $ref
to this schema in your OpenAPI spec.
When describing the duration of a work-related event of an employee (e.g. a shift, a leave, etc…), then you must use the common WorkEventDuration
object.
This gives you the ability to distinguish between two units: days and hours. Events in “days” unit have a duration defined as a fraction of a day of work (0.5 days might be 3.5 hours for an employee working 7h/day, and 4 hours for an employee working 8h/day).
Having a common definition grants us the ability to aggregate all work-related events of an employee and make calculations.
You MUST NOT use this schema for inheritance. Treat it as a closed data type.
You SHOULD $ref
to this schema in your OpenAPI spec.
You SHOULD only support write requests on the iso
property.
Some query parameters are reserved and as such MUST NOT be used in any other way:
Name | Type | Semantics |
---|---|---|
page | string | Cursor to the page. |
limit | integer<int32> | Number of items per page. |
include | Array<string<‘embedded’ | ‘links’ | ‘totalItems’>> | Control over the inclusion of embedded resource, the total count of a collection items and/or links to related resources. |
sort | Array<string<enum>> | Control over the sorting of a collection’s items. |
Outside of the Lucca API, the page parameter MAY be an integer (offset-based pagination) rather than a cursor.
This default sorting strategy should apply to a unique property. For this reason,
it is recommended to sort on the reserved id
property by default.
Giving clients control over the ordering of items of a collection is considered good practice and can ease emergent uses.
Sorting is handled through a sort query parameter whose BNF grammar is:
Sorting options should be named in compliance with the actual ressource property names the sorting is applied on.
When query parameters are used as a way to filter resources based on the value of one of their properties, then the query parameter SHOULD be named with the same name as the relevant property.
As a result of the previous rule, query parameters MUST be named using the same casing as properties: camelCase.
When these query parameters filter on a nested property (i.e. the property of an object property in the resource representation), then the query parameter name SHOULD stick to the JSON path to said property (i.e. list the properties from the root object, separated with a dot “.” character).
Examples:
Unless it absolutely makes sense, in which case the default value MUST be documented in the OpenAPI specs.
When applying an equality filter on one property of the resource, then you should support multiple values. In this case, values must be serialized as a comma-separated list:
{propName}.between
named query parameterTODO
The canonical notation with a “/” (forward slash) delimiter between dates SHOULD not be used as it could break URLs when used as query parameter values.
You MAY choose to support date(-time)-ranges defined with a duration component.
You SHOULD support open-handed ranges (i.e. with a “..” start or end).
Why? Because this is the most common human way of apprehending date and date-time ranges.
Giving API clients control over response exansion is a way of making integrations easier.
Nonetheless, be aware that such features may prove costly, due to:
Paging is handled through two query parameters:
Parameter name | Parameter value type | Example | Description |
---|---|---|---|
page | <string | int<int32>> | ?page=2 | The identifier of the page to retrieve. |
limit | int<int32> | ?limit=100 | The page size. Subject to default and max values. |
Whenever representing a collection of resources whose maximum size is not constant and can prove high, then make sure to implement paging. Paging is the key to ensuring satisfactory performance.
Cursor-based paging offers better performance than index-based paging, but at the cost of one feature: API clients must iterate through each page (they cannot randomly access a given page), as the cursor of a page is obtained through retrieving the previous or next one.
On top of allowing random-access, index-based paging is also more common and more readily comprehensible by humans.
Having a low default page size:
We recommend having a default page size between 10 and 25.
For the same reasons as above.
We recommend not exceeding 100 items per page.
Monotony reduces complexity. As such, make sure your default as well as max page size are the same for all collections.
Paging links consists in giving API clients links to the previous and next page when retrieving a page of resources.
Reponse expansion is considered a good practice as it may reduce the number of required HTTP requests and as such make integrations easier.
The embedded representation of a resource MUST be in the reserved embedded
response property
(at the root object level).
The embedded
property MUST be a JSON object whose keys are the name of the resources types
, and its values
are a JSON object whose keys are the embedded resources ids
and values the related representation.
Resource representations embedded in the reserved embedded
property MAY be partial, i.e.
only serialize a subset of the related resource properties.
You MUST make sure the authenticated user has access to the resource when embedding it in the response.
When representing an embedded related resource, then any change to this embedded resource also changes the representation of the embedding representation.
As a result, the HTTP cache of the embedding resource SHOULD be invalidated whenever one of its embedded resources changes.
This is a SHOULD and not a MUST. Consequently, embedded resources representations MAY be stale from cache.
This is why the embedding is done at root level rather than nested in the embedding resource representation.
In order to avoid events propagation, avoid triggering events on the embedding resource whenever one of its embedded resource is changed, except if the embedding resource bears a property that references this embedding resource.
In other words: you SHOULD trigger an event whenever an intrinsic attribute of the resource changes ; and embedded representations are not intrinsic attributes.
Custom headers SHOULD NOT be prefixed with “X-” (this practice was deprecated along with RFC-6648).
Header MUST be named using hyphen-separated PascalCase.
You SHOULD stick to standard HTTP headers whenever possible.
Some custom headers are reserved for the Lucca API and as such MUST NOT be used in any other way.
Versioning an API should be seen as a last resort when implementing APIs, as it breaks integrations.
Prefer additions to deletions / modifications.
Have a rather “lax” definition of breaking changes. For example: clients should consider all ressources extensibles without breaking. Same with enumerations.
The major quality expected of the Lucca API is its stability for clients. As we cannot prevent all breaking changes, a versioning must be implemented.
Api-Version
HTTP header (both requests and responses)Either make the Api-Version
HTTP header required, or make it possible for clients to “lock” a specific version (e.g. API key based).
You may implement version selection through a “as of” behaviour: the server will select the API version that exactly matches the given version or is the closest prior one.
Deprecation
and Sunset
HTTP headers to convey an API version lifecycleFor the Deprecation
HTTP header, refer to the draft-ietf-httpapi-deprecation-header-03.
For the Sunset
HTTP header, refer to RFC 8594.
Once an API version has been deprecated, then it must stay available to all clients for at least 6 months. This date is ideally indicated in the Sunset
HTTP response header.
Once an API version is released, it may not be deprecated before a 6 months time period has passed.
The information should be available to all clients, and you should be extra zealous and notify those that actively use it.
In other words: if the deployed API version is not compliant with the spec, then you may fix it even though it could lead to breaking changes (as long as the fix makes it compliant with the spec).
You should always be explicit regarding cacheability. As default, when not implementing cache, servers and clients should always set the Cache-Control
header to Cache-Control: no-store
(rather than no-cache
).
As a reminder:
no-cache
: responses may be cached but should be validated before use.no-store
: responses may not ever be cached.HTTP Caching is the prime means of building scalable APIs.
Please refer to HTTP methods cacheability.
Events give API clients more optimal ways of integrating to the Lucca API (polling can prove costly and complex).
As a reminder:
“Exactly-once” is the most difficult delivery guarantee to implement. It sure is friendly to API clients, but it has a high cost for the system’s performance and complexity.
Therefore, when implementing the “at-least-once” guarantee, make sure events receivers are idempotent and handle message duplication correctly.
An Event represents a change in an application state that might be of interest to third-party apps.
In order to be properly coordinated with the REST API, an Event must always be a change that is in relation to a single resource represented in the API. In other words:
embedded
and links
).In other words, embrace data-change events (e.g. job-position.created
) and stay clear of business-events
(e.g. employee.promoted
) as well as free-form events (e.g. employee-10-years-anniversay
).
The event schema is imposed and described in the API Reference.
Events are “fat” (rather than “thin”), i.e. contain the representation of the related resource in order to improve the development experience among receivers.
Topics naming MUST conform to the following conventions:
You may add an affix though, for example: calendar-event
.
updatedAttributes
on *.updated
topicsGiving receivers the list of updated attributes on *.updated
events.
When implementing this feature, you SHOULD serialize a JSON object whose keys are the updated attributes and whose values are their previous value.
Ensure authenticity of the event (a fake event might be injected or the data of a legitimate event might be forged). To make sure an event has not been tampered with, a cryptographic signature must be used, like HMAC (Hash-based Message Authentication Code).
The sender MUST sign the event payload with a shared secret and includes the signature
in the Lucca-Signature
HTTP header.
This offers the following guarantees:
In order to protect receivers againts replay attacks (an attacker might record traffic and play it back later), make sure the signature depends on a timestamp that matches the moment the event delivery is attempted.
Reeivers need to validate the signature and the timestamp contained in the event (a tolerance of +/- 5 minutes should be applied). In other words, any event older than 5 minutes should be automatically rejected by the subscriber.
Prevent event flooding: a receiver endpoint might get flooded with events. Best practice is for receivers to “acknowledge receipt of events fast, and queue events for asynchronous processing”.
A timeout can help enforce satisfactory response times on receivers (e.g.: Slack imposes a very short 3 seconds timeout on receivers). Otherwise, deliveries will be suspended.
For obvious security reasons.
When the API is versioned, then make sure your events are as well (given that the representations of resources embedded in events payload are versioned).
When multiple API versions are live, then make sure to generate as many events as there are ongoing versions (one for each version).
CloudEvents is a specification for describing event data in common formats to provide interoperability across services, platforms and systems.