A place to define the conventions we use to build APIs
It's quite common in Hyper to build APIs with Ruby on Rails, so most of these conventions are designed to work well with Rails.
When the responses need to be localized, it's recommended for the client to send
the Accept-Language
header:
Accept-Language: nb-no, no, en
We take in account most of Heroku's conventions for building APIs.
Keep things simple while designing by separating the concerns between the different parts of the request and response cycle. Keeping simple rules here allows for greater focus on larger and harder problems.
Requests and responses will be made to address a particular resource or collection. Use the path to indicate identity, the body to transfer the contents and headers to communicate metadata. Query params may be used as a means to pass header information also in edge cases, but headers are preferred as they are more flexible and can convey more diverse information.
Require secure connections with TLS to access the API, without exception. It’s not worth trying to figure out or explain when it is OK to use TLS and when it’s not. Just require TLS for everything.
Ideally, simply reject any non-TLS requests by not responding to requests for
http or port 80 to avoid any insecure data exchange. In environments where this
is not possible, respond with 403 Forbidden
.
Redirects are discouraged since they allow sloppy/bad client behaviour without providing any clear gain. Clients that rely on redirects double up on server traffic and render TLS useless since sensitive data will already have been exposed during the first call.
Include an ETag
header in all responses, identifying the specific
version of the returned resource. This allows users to cache resources
and use requests with this value in the If-None-Match
header to determine
if the cache should be updated.
Accept serialized JSON on PUT
/PATCH
/POST
request bodies, either
instead of or in addition to form-encoded data. This creates symmetry
with JSON-serialized response bodies, e.g.:
$ curl -X POST https://service.com/apps \
-H "Content-Type: application/json" \
-d '{"name": "demoapp"}'
{
"id": "01234567-89ab-cdef-0123-456789abcdef",
"name": "demoapp",
"owner": {
"email": "username@example.com",
"id": "01234567-89ab-cdef-0123-456789abcdef"
},
...
}
Use the plural version of a resource name unless the resource in question is a singleton within the system (for example, in most systems a given user would only ever have one account). This keeps it consistent in the way you refer to particular resources.
In data models with nested parent/child resource relationships, paths may become deeply nested, e.g.:
/orgs/{org_id}/apps/{app_id}/dynos/{dyno_id}
Limit nesting depth by preferring to locate resources at the root path. Use nesting to indicate scoped collections. For example, for the case above where a dyno belongs to an app belongs to an org:
/orgs/{org_id}
/orgs/{org_id}/apps
/apps/{app_id}
/apps/{app_id}/dynos
/dynos/{dyno_id}
Return appropriate HTTP status codes with each response. Successful responses should be coded according to this guide:
200
: Request succeeded for a GET
call, for a DELETE
or
PATCH
call that completed synchronously, or for a PUT
call that
synchronously updated an existing resource201
: Request succeeded for a POST
call that completed
synchronously, or for a PUT
call that synchronously created a new
resource202
: Request accepted for a POST
, PUT
, DELETE
, or PATCH
call that
will be processed asynchronously206
: Request succeeded on GET
, but only a partial response
returned: see above on rangesPay attention to the use of authentication and authorization error codes:
401 Unauthorized
: Request failed because user is not authenticated403 Forbidden
: Request failed because user does not have authorization to
access a specific resourceReturn suitable codes to provide additional information when there are errors:
422 Unprocessable Entity
: Your request was understood, but contained invalid parameters429 Too Many Requests
: You have been rate-limited, retry later500 Internal Server Error
: Something went wrong on the server, check status
site and/or report the issueRefer to the HTTP response code spec for guidance on status codes for user error and server error cases.
Provide the full resource representation (i.e. the object with all
attributes) whenever possible in the response. Always provide the full
resource on 200 and 201 responses, including PUT
/PATCH
and DELETE
requests, e.g.:
$ curl -X DELETE \
https://service.com/apps/1f9b/domains/0fd4
HTTP/1.1 200 OK
Content-Type: application/json;charset=utf-8
...
{
"created_at": "2012-01-01T12:00:00Z",
"hostname": "subdomain.example.com",
"id": "01234567-89ab-cdef-0123-456789abcdef",
"updated_at": "2012-01-01T12:00:00Z"
}
202 responses will not include the full resource representation, e.g.:
$ curl -X DELETE \
https://service.com/apps/1f9b/dynos/05bd
HTTP/1.1 202 Accepted
Content-Type: application/json;charset=utf-8
...
{}
Provide created_at
and updated_at
timestamps for resources by default,
e.g:
{
// ...
"created_at": "2012-01-01T12:00:00Z",
"updated_at": "2012-01-01T13:00:00Z",
// ...
}
These timestamps may not make sense for some resources, in which case they can be omitted.
Accept and return times in UTC only. Render times in ISO8601 format, e.g.:
"finished_at": "2012-01-01T12:00:00Z"
Serialize foreign key references with a nested object, e.g.:
{
"name": "service-production",
"owner": {
"id": "5d8201b0..."
},
// ...
}
Instead of e.g.:
{
"name": "service-production",
"owner_id": "5d8201b0...",
// ...
}
This approach makes it possible to inline more information about the related resource without having to change the structure of the response or introduce more top-level response fields, e.g.:
{
"name": "service-production",
"owner": {
"id": "5d8201b0...",
"name": "Alice",
"email": "alice@heroku.com"
},
// ...
}
Generate consistent, structured response bodies on errors. Include a
machine-readable error id
, a human-readable error message
, and
optionally a url
pointing the client to further information about the
error and how to resolve it, e.g.:
HTTP/1.1 429 Too Many Requests
{
"id": "rate_limit",
"message": "Account reached its API rate limit.",
"url": "https://docs.service.com/rate-limits"
}
Document your error format and the possible error id
s that clients may
encounter.
Rate limit requests from clients to protect the health of the service and maintain high service quality for other clients. You can use a token bucket algorithm to quantify request limits.
Return the remaining number of request tokens with each request in the
RateLimit-Remaining
response header.
Extra whitespace adds needless response size to requests, and many clients for human consumption will automatically "prettify" JSON output. It is best to keep JSON responses minified e.g.:
{"beta":false,"email":"alice@heroku.com","id":"01234567-89ab-cdef-0123-456789abcdef","last_login":"2012-01-01T12:00:00Z","created_at":"2012-01-01T12:00:00Z","updated_at":"2012-01-01T12:00:00Z"}
Instead of e.g.:
{
"beta": false,
"email": "alice@heroku.com",
"id": "01234567-89ab-cdef-0123-456789abcdef",
"last_login": "2012-01-01T12:00:00Z",
"created_at": "2012-01-01T12:00:00Z",
"updated_at": "2012-01-01T12:00:00Z"
}