This document has been adapted from a repository originally published by The White House, and will be continually updated to reflect API Standards at LeadPages. We are thankful to be able to use their work as a guide. Please refer to the credits for further information.
Items marked as "under review" should not be considered usable and may not be used as reference material.
LeadPages APIs embrace several core ideas that guide how our APIs present themselves. The aim of this document is to document those ideas, whether it be through concrete examples or prose. The rest of the overview provides some detail on what those core ideas are.
We place an emphasis on developer experience, both for others and ourselves. We are our own API consumers, and we must be efficient and concerted in both our creation and use of APIs.
An API presents itself as the best representation of something. It is structured, thought-out, canonical, and has semantic importance. An API could be considered a contract, or putting your best foot forward. These are justifications for utilizing a design process. This document helps in the design process by guiding the process of deciding "what goes where and why" and moving the "why" out of the core development process. This helps keep the API design process measured and repeatable.
This document provides guidelines and examples for LeadPages HTTP APIs, encouraging consistency, maintainability, and best practices across applications. Best practices may come with scenarios that pit The Right Way™ against the pragmatic way. LeadPages APIs aim to balance the rigor and structure of a REST API with a positive developer experience. We'll always go for the best solution when it's practical to do so.
It's in the name. APIs are a developer's user interface to a service. When tough questions arise about the design of the API, we can defer to experts. Jakob Nielsen's "Usability Heuristics for User Interface Design" have provided designers with research-based common ground and apply well to developers using an API.
Reading the heuristics themselves is the ideal starting point, but they are summarized here.
- Visibility of system status
- Match between system and the real world
- User control and freedom
- Consistency and standards
- Error prevention
- Recognition rather than recall
- Flexibility and efficiency of use
- Aesthetic and minimalist design
- Help users recognize, diagnose, and recover from errors
- Help and documentation
The base URL of an API resource reflects a few key considerations. This example base URL is the primary URL structure.
https://api.company.com/service/v1
For clarification, here is the base URL in the context of a full resource URL. This is not a base URL:
https://api.company.com/service/v1/sprockets/3rDrZRbdeSJJeR4jQSWAjK
In some cases, it may make sense to structure the base URL to include the API name as a subdomain.
https://service.company.com/api/v1
What follows are high-level things to notice in the base URL, and they are ordered by first appearance.
The protocol (https
) is specified in the base URL. This is purposeful.
SSL/TLS is a minimum requirement and specifying a base URL beginning with
http
is invalid. There are lots
of resources
and discussions
about why
SSL/TLS only is a good choice and why others choose that path, and addressing
concerns about speed and overhead.
The API in this example is hosted on a subdomain dedicated to a set of APIs. This distinction is important, because it necessitates the idea of an API name, discussed below.
- If there are multiple APIs, and they make sense grouped under the company as an entity, having several named APIs is a logical grouping.
- If the subdomain is a specific product, service, or similar, and there are
very few of them, the alternate example may make more sense. In this case,
the API name is replaced with the
api
fragment to indicate that this is the sole, self-contained API for this item. This API should be comprehensive and should not overlap functionality with other APIs located on other subdomains.
This API is named data
. You would call this the "Data API" based on the
fragment in the URL. You could also have API names like admin
or tasks
that you would call the "Administration API" and "Tasks API," respectively.
The goal of having an API name in the path is to provide a sensible grouping
of top-level objects. In an Admin API, you might have top-level objects like
"users" that would not make sense if placed next to "tasks" in a Tasks API.
In the full resource URL example above, the top-level objects are widgets
.
A few notes about choosing an API name:
- The name should either reflect a service or a branded product, and ideally
the name will reflect a service. Thus, an API name like
aggregator
should be chosen over some branded equivalent in most cases. - The name should never reflect an internal or informal name, like a project or team codename.
There are some heuristics for choosing an API name that reflects a generic service vs. a product-centric API name. For example, product-centric APIs are likely to utilize several service APIs as components, and may only function to house the data model and schema for a resource.
A version (v1
) is specified in the URL. API versioning helps ease
transitions when there are breaking changes to the interface put forward by
an API, and makes for smoother and more straightforward deprecation plans. If
an API update will break clients and implementations, at a minimum, we want to
know how many will be affected, and a versioned API is the place to
start for finding that information.
Version tags begin with a v
and end with a a positive integer and have
nothing in between. Requests without a version tag are invalid and must be
rejected.
An API must always provide the latest version as part of the API metadata, for
example, as a response at the root of the API (/sprockets/v1
).
Versions must be maintained at least one version back. If the v3
API is
current, the v2
API must be marked as deprecated but kept available. This
helps solidfy the "contract" between the service and the developer. One of the
reasons having an API version is so important is because the schema of the API
is provided by the API service itself; thus, the API version represents not
just functionality, but actually the version of the data model itself.
The API may include minor or patch version information as part of the metadata of the API or response.
Based on the idea that the version also reflects the data model, schema changes have the following impacts on the version:
Change | Major Version | Minor Version |
---|---|---|
Add optional field | No change | Increment |
Add required field | Increment | Reset |
Change field type | Increment | Reset |
Some valid examples of versions:
v1
v2
v3
Some invalid examples of versions:
v1.0
ver1
current
v1beta
Resources are represented by a path that follows a base URL. You can consider the resource path as the canonical path for a resource, despite the base URL.
This is the full URL from the base URL section, as well as the alternate version:
https://api.company.com/service/v1/widgets/3rDrZRbdeSJJeR4jQSWAjK
https://service.company.com/api/v1/widgets/3rDrZRbdeSJJeR4jQSWAjK
Note that if you strip the base URL from the front, they are the same:
/widgets/3rDrZRbdeSJJeR4jQSWAjK
/widgets/3rDrZRbdeSJJeR4jQSWAjK
What follows are some more specific guidelines around resource paths.
Here are some basics for RESTful URLs:
- A URL identifies a resource.
- URLs must use plural nouns, not verbs.
- Do not use "formats" in the URL. Some APIs place a type, like
.json
, at the end of the URL.
Given a resource path like...
/widgets/a1b2c3/sprockets
...you can note several things:
widgets
andsprockets
are collections. They are plural nouns. Collections hold resources and relate to the "type" of something.- Some specific resources, like this one (
a1b2c3
) may themselves hold collections (in this case,sprockets
). Only in extrodinary situations should you consider nesting any further, and you should not specify a resource after the secondary collection (like/widgets/a1b2c3/sprockets/d4f5g6
). - If you're using a nested resource, like
/widget/a1b2c3/sprockets
, note that this is a strong implication thatsprockets
must be created as a child of a singlewidget
(a1b2c3
). What this then implicates is that you may not, in general, create asprocket
at the rootsprocket
endpoint. The rootsprocket
endpoint may list all of the current context'ssprocket
s, or similar, but in general should not accept creation requests. In concrete examples:POST /sprockets
is invalid.POST /widgets/a1b2c3/sprockets
will create asprocket
that may be returned as a property within/widgets/a1b2c3
.GET /widgets/a1b2c3/sprockets
will return all thesprocket
s that are in the collection for thea1b2c3
widget.GET /sprockets/d4f5g6
will return a specificsprocket
.GET /sprockets
may return allsprocket
s, regardless ofwidget
collection.
This nested endpoint relationship avoids having to submit the parent ID as
part of the request body. For example, again assuming a sprocket
as a child
of a widget
:
POST /widgets/a1b2c3/sprockets
{
"key": "value",
"foo": "bar"
}
is preferable to:
POST /sprockets
{
"widget": "a1b2c3",
"key": "value",
"foo": "bar"
}
There are few stipulations involved with an API request. APIs should aim to be permissive and not rigid wherever that does not introduce ambiguity.
Methods, or HTTP verbs, should be used in compliance with their definitions under the HTTP/1.1 Method Definitions standard.
The following mapping guides the verbs to some equivalent wording.
Method | Synonym | Description |
---|---|---|
GET | Fetch | Return the specified resource or list of resources |
POST | Create | Create a new resource in the specified collection |
PATCH | Update | Update the specified existing resource |
PUT | Replace | Replace the specified resource |
DELETE | Remove | Delete the specified resource |
OPTIONS | Define | View the schema and available actions for the resource or collection |
HEAD | Describe | Return all of the information about a resource without returning the response itself |
The action taken on the representation will be contextual to the media type being worked on and its current state. Here's an example of how HTTP verbs can map to different actions depending on context.
Method | Endpoint | Description |
---|---|---|
POST |
/widgets |
Create a widget |
GET |
/widgets |
List widgets |
POST |
/widgets/3rDrZRbdeSJJeR4jQSWAjK |
Invalid |
GET |
/widgets/3rDrZRbdeSJJeR4jQSWAjK |
Get a single widget |
The most basic header that API services must support, and that clients
should provide, is the Content-Type
header. In general, the client will
supply this header with the value application/json
to specify that the
request body is JSON. However, API services must not require this header and
must default to this (application/json
) media type if unspecified.
This creates an implication that the API service must be particularly resilient and stringent in its request parsing in order to prevent security issues.
Similarly, if an Accepts
header is unspecified, the service may assume that
the media type should be application/json
, though it must be rigorous in
specifying this in the response's Content-Type
header.
These specifications can be considered a direct application of Postel's Robustness Principle.
Querying, to specify certain output in the response, is generally very open. No API service should reject or strip any particular query parameters, and modifying the query (or "search") portion of the URL must be an exclusively additive process. By extension, even queries that may seem nonsensical (for example, specifying a limit as if paginating, on a request for a single resource) must not be rejected as invalid input.
There are some query parameters that should be supported by any API service:
Parameter | Description |
---|---|
limit |
Maximum number of items to return, if listing resources. |
offset |
Where in the result set to begin listing resources. |
cursor |
Begin at this pointer into the listing response. |
Most APIs should return JSON responses in a particular format. We'll begin with an example response.
{
"_status": {
"code": 200
},
"_meta": {
"limit": 20,
"total": 30,
"count": 10,
"offset": 20
},
"_items": [
{
"_meta" :{
"id": "3rDrZRbdeSJJeR4jQSWAjK",
"uri": "https://api.leadpages.io/data/v1/widgets/3rDrZRbdeSJJeR4jQSWAjK",
"created": "2015-04-24T18:35:10.656940+00:00",
"updated": "2015-04-24T18:35:10.656976+00:00",
},
"color": "fuschia",
"make": "Spacely",
"manufacturedOn": "2015-03-20T12:25:10.446976+00:00"
}
]
}
Some things to notice about this response format:
- Several of the fields begin with an
_
. All underscore-prefixed keys are reserved and may not be used for data properties. - This is a response from listing widgets. We have two clear ways of inferring
this:
- The
_meta
section includes pagination information. - The reserved keys within the
_meta
section do not require an underscore prefix. - There is an
_items
key at the root of the response object, that is a list of objects.
- The
- For the widget in the list, there are additional
_meta
properties for that specific resource, as well as some actual data properties (color
andmake
). - A status is included. All responses must include status information as a way to display warnings for a successful request. Further information is available in the error handling section.
- JSON properties are camelCase, like
manufacturedOn
above. Backends should use whatever casing style is best and transform the output to camelCase.
There are also two very general patterns to notice:
- No values are used as keys.
- The response is an object despite the fact that it is returning a listing. The listing is nested within the containing object.
Every resource should have a unique ID associated with it, and in general
these should be version 4 UUIDs as specified in RFC 4122
and then compressed into base57. A concrete reference for this process is
using the shortuuid
library.
A resource's ID must be returned in the id
property in the _meta
section
of the response.
Timestamps included in responses must always be in the timezone-aware ISO 8601 format.
There are two timestamps that exemplify this format that must be returned
with every resource, as seen above. They represent the creation and update
times for this resoure, where the creation time (_created
) is set only once
when the resource is created, and the update time (_updated
) reflects the
most recent time the resource was updated.
URIs present in the response should be absolute, including host, protocol, and scheme (i.e., HTTPS). This prevents ambiguity if, in some future state, there is a need to reference schema, resources, etc. across domains, and also assists in making the HTTPS-only requirement concrete.
Additionally, resources must be returned with a _uri
property that
always returns the canonical version of the resource. Note that there may be
special cases regarding this URI as specified in the versioning
section.
The pagination parameters are as follows:
Key | Description |
---|---|
limit |
Max number of items that may be returned in this listing. |
total |
Total number of items that matched the listing or query. |
offset |
The starting point in the result set that this response represents. |
cursor |
A pointer into the result set that this response represents. |
When paginating, you may generally expect support for either:
- A limit and offset pair to navigate within the set, and a total count of records.
- A cursor.
Some databases do not efficiently support one or the other option, so these properties reflect that. Any properties should not be presented when paginating unless they are guaranteed. You may not return an approximate total when a proper cursor is available, for example.
You may not infer any meaning from a cursor if given. It may appear to have encoded data, or meaning, but it should be considered as a token with no value on its own.
Error responses should include a message for the developer, an optional internal (diagnostic) error code, and documentation links (if applicable) where developers can find more info. For example:
{
"_status": {
"code": 400,
"ref" : "a1b2c3",
"errors": [
{
"severity": "error",
"message": "The attribute `foo` is required."
},
{
"severity": "warning",
"message": "A timezone was not specified for `event`; assuming UTC."
}
],
"docs": [
"http://docs.leadpages.io/errors/400",
"http://docs.leadpages.io/errors/a1b2c3",
"http://docs.leadpages.io/errors/timezones",
"http://docs.leadpages.io"
]
}
}
All errors (and status messages) must be contained in a _status
property and
include a code
property. The only response that does not need to include
_status
is a 204 No Content
response, as it is defined to not have a
response body.
The errors
property must be a list of applicable errors, each specifying a
severity level and a message. In this way, things like form validation can be
well-supported when several errors may exist for a given request.
Tend towards the most common response codes when indicating success, failure, or status. All of the reason phrases should be as presented in RFC 2616. Some of the most common codes and a possible reason include:
200 OK
as a general success status201 Created
when a new resource is created204 No Content
when a resource is deleted400 Bad Request
when a required value is missing403 Forbidden
when not logged in404 Not Found
when a resource is not found500 Internal Server Error
when a server failure occurs503 Service Unavailable
when the service is temporarily overloaded
Services are responsible for providing a CORS implementation that is complete
and will allow natural communication with the API in a browser-based context.
This means that any request must also return a proper OPTIONS
response for
that resource that will allow the request.
Some other notes and considerations:
JSON is the only required response type at this time, and in general, XML as a response should be considered only in exceptional cases. Other response formats, particularly those that may be exposed as a service-to-service interface, are not covered in the scope of this HTTP-centric document.
JSONP is not supported. Use CORS instead. JSONP does not support methods other
than GET
, and is generally regarded by the community as being replaced by
CORS.
Contributing follows a fork, pull request & issue format. All ideas, improvements, critiques, and fixes are welcome. All contributers, including maintainers, must open pull requests, other than exceptional cases or basic repository chores.
- If you have something that you feel is best represented concretely, just fork the repository and create a pull request with your changes.
- If you have something that you feel is an idea, brainstorm topic, or is otherwise still cookin', feel free to open an issue and use the "idea" tag.
This document has been adapted from The White House's API standards. Additional credits from that document are reproduced here.
This document borrows heavily from:
- Fielding's Dissertation on REST
- API Facade Pattern by Brian Mulloy Apigee
- Web API Design by Brian Mulloy, Apigee
- Designing HTTP Interfaces and RESTful Web Services
Additionally, this document has been created and improved thanks to the following people: