REST API Design
Theory
Ivan Novakov
What are the typical features of a RESTful API?
What is REST?
REpresentational State Transfer
A PhD dissertation by Roy Fielding
A core set of principles, properties and constraints describing the architectural style of the World Wide Web.
Focus on component roles and interactions rather than implementation details.
200 OK
Content-Type: text/html
<html>
<body>
<h1>Title</h1>
<p>John Smith</p>
<!-- ... -->
</body>
</html>
200 OK
Content-Type: application/json
{
"id": 123,
"title": "Title",
"author": "John Smith",
...
}
REST != API
REST is the foundation of the World Wide Web.
GET /posts/123 HTTP/1.1
Accept: text/html
Request
GET /posts/123 HTTP/1.1
Accept: application/json
Response
REST principles
Richardson Maturity Model:
- Level 0: HTTP as a transport layer
- Level 1: Resources
- Level 2: HTTP verbs (and codes, headers, ...)
- Level 3: Hypermedia
REST and HTTP
HTTP as a transport layer.
HTTP as a tunneling mechanism for remote interaction.
SOAP, XML-RPC, ... they (may) also use HTTP.
Resources
Everything is a resource!
Resource is an object, entity, ... anything, which can be described using a noun.
...or a collection of resources.
Identifiers
Every resource has a unique identifier (URI/URL).
Resource URLs usually resemble directory-like hierarchical structures.
https://api.news.example.com/v1/articles/123
https://api.finance.example.com/v2/currencies/USD
https://api.directory.example.com/v1/users
https://api.directory.example.com/v1/users?location=Prague
/articles
/articles/123
/articles/123/tags
/articles/123/tags/elections
Sub-resources
Resources dependent on the main resource:
/articles/123/paragraphs
/articles/123/paragraphs/1
/articles/123/tags
/articles/123/tags/elections
/users/jsmith/certificates
/users/jsmith/roomNumber
Resources related to the main resource:
Much better than:
/paragraphs?article=123
Resources and identifiers
/articles/123
/articles/123/author
/authors
/authors/456
/authors/456/publications
/categories
/categories/789
/categories/789/articles
/server/restart
/rpc/getStatus
/rpc/updateStatus
/rpc/getUsersByName
/article/123/publish
/article/123/delete
Resource:
Not a resource:
Verbs
Resources are manipulated through verbs - GET, POST, PUT, PATCH, DELETE, ...
The usage of these verbs should follow closely the HTTP specification.
GET
- Used to "fetch" a resource.
- A "safe" operation, it doesn't modify data on the server.
- Never use GET for modifying content.
GET /articles/123 HTTP/1.1
Accept: application/json
POST
- A "write" operation.
- Used to create new resources.
- Non-idempotent.
POST /articles HTTP/1.1
Content-Type: application/json
{
"title": "A new title",
"author": "John Smith",
...
}
PUT
- A "write" operation.
- Used to update (replace) resources as a whole.
- Idempotent.
PUT /articles/123 HTTP/1.1
Content-Type: application/json
{
"title": "A new title",
"author": "John Smith",
...
}
PATCH
- A "write" operation.
- Used for partial updates.
- Non-idempotent.
PATCH /articles HTTP/1.1
Content-Type: application/json
{
"title": "A new title"
}
DELETE
- A "write" operation.
- Used to delete (deactivate, remove, ...) resources.
- Idempotent.
DELETE /articles/123 HTTP/1.1
Verbs
Verb | /articles | /articles/123 |
---|---|---|
GET | Fetch a collection | Fetch a resource |
POST | Create a new resource | |
PUT | Update a resource | |
PATCH | Partially update a resource | |
DELETE | Delete a resource |
HTTP status codes
A RESTful service uses the HTTP status code as a primary indicator about the success of a request.
- 2xx: Successful
- 3xx: Client should perform an additonal action
- 4xx: Error caused by the client
- 5xx: Error caused by the server
2xx
- 200 OK
- 201 Created
- 204 No Content
- 206 Partial Content
Success
3xx
- 301 Moved Permanently
- 302 Found
- 304 Not Modified
The client should perform an additonal action
4xx
- 400 Bad Request
- 401 Unauthorized
- 403 Forbidden
- 404 Not Found
- 405 Method Not Allowed
- 415 Unsupported Media Type
Error caused by the client
5xx
- 500 Internal Server Error
- 501 Not Implemented
- 503 Service Unavailable
Error caused by the server
Error reporting
400 Bad Request
Content-Type: application/json
{
"error": "invalid input",
"message": "The username should be more than 3 characters long.",
}
503 Service Unavailable
Content-Type: application/json
{
"error": "service down",
"message": "Cannot connect to the database",
"details": {
"db": "ORA-12150: TNS:unable to send data"
}
}
Headers
Use HTTP headers for any metadata you need to transfer between the client and the server.
Metadata - any data, which are not part of the resource, but are somehow part of the interaction:
- describe the data
- describe how to handle the data
- transport related
Content negotiation
GET /users/123 HTTP/1.1
Accept: application/json
200 OK
Content-Type: application/json;charset=utf-8
Content-Length: 180
{
"username": "jsmith",
...
}
Authentication and authorization
GET /users/123
Accept: application/json
Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=
401 Unauthorized
WWW-Authenticate: Basic realm="My Service"
GET /users/123
Accept: application/json
Request
Second request
Response
Cache control
GET /users/123
Accept: application/json
200 OK
Content-Type: application/json
Last-Modified: Fri, 03 Jun 2016 14:30:30 GMT
{ ... }
GET /users/123
Accept: application/json
If-Modified-Since: Fri, 03 Jun 2016 14:30:30 GMT
304 Not Modified
Content-Length: 0
Request
Request
Response
Response
Custom headers
X-Merck-<name>: <value>
X-Merck-APIKey: 1234567890
X-Merck-Some-Other-Header: foo
Current naming recommendation:
But the usage of "X-" prefixed headers has been deprecated!
Not quite relevant for us. Anyway, use some unique prefix, when defining custom headers.
Query parameters
/users?location=Prague
/users?location=Prague&firstName=Ivan
Filtering
Paging
/users?page=12&pageSize=10
/users?limit=12&offset=110
Sorting
/users?sortBy=firstName&sortDest=ASC
/users?sortBy=employeeNumber&sortDest=DESC
Combined
/users?location=Prague&firstName=Ivan&sortBy=employeeNumber&page=12
Hypermedia (HATEOAS)
- Resources are provided with links pointing to related resources.
- The client is able to traverse the resources using the links starting from the "entry point".
- No other knowledge is required.
Hypermedia As The Engine Of Application State
Hypermedia (HATEOAS)
200 OK
Content-Type: application/json
{
_links: {
self: {
href: "/users/123"
},
manager: {
href: "/users/456"
}
}
username: "jsmith",
...
}
200 OK
Content-Type: application/json
Link: </users/123>;rel=self,</users/456>;rel=manager
{
"username": "jsmith",
...
}
Hypermedia (HATEOAS)
200 OK
Content-Type: application/json
[
{
"_links": { "self": { "href": "/users/123" } },
"username": "jsmith"
},
{
"_links": { "self": { "href": "/users/456" } },
"username": "foobar"
}
...
]
GET /users
Accept: application/json
Representation
The JSON (XML, YAML, CSV, ...) you get from a RESTful endpoint is not the resource, but its representation.
With content negotiation you can be quite flexible when choosing the right representation for your API.
Choosing representation
- Be consistent
- Use content negotiation
- Adopt hypermedia
- Prefer standards
HAL
Hypertext Application Language
- JSON based (application/hal+json)
- Hyperlinks in every resource with the _links meta field.
- Sub-resources with the _embedded meta field.
HAL
Hypertext Application Language
{
"_links": {
"self": { "href": "/employees/123456" },
"manager": { "href": "/employees/789012" }
},
"employeeId": 123456,
"firstName": "Ivan",
"lastName": "Novakov",
"email": "ivan.novakov@company.com"
}
Single resource
HAL
Hypertext Application Language
{
"_links": {
"self": { "href": "/employees/123456" }
},
"employeeId": 123456,
"firstName": "Ivan",
"lastName": "Novakov",
"email": "ivan.novakov@company.com",
"_embedded": {
"manager": {
"_links": {
"self": { "href": "/employees/789012" }
},
"employeeId": 789012,
"firstName": "John",
"lastName": "Smith"
}
}
}
Single resource with embedded subresource
{
"_links": {
"self": { "href": "/employees/123456" }
},
"employeeId": 123456,
"firstName": "Ivan",
"lastName": "Novakov",
"email": "ivan.novakov@company.com",
"_embedded": {
"department": {
"_links": {
"self": { "href": "/departments/123" }
},
"id": 123,
"name": "IT Department",
"description": "The department running all the computers"
},
"manager": {
"_links": {
"self": { "href": "/employees/789012" }
},
"employeeId": 789012,
"firstName": "John",
"lastName": "Smith"
}
}
}
Single resource with embedded subresource
{
"_links": {
"self": { "href": "/employees?page=4" },
"first": { "href": "/employees" },
"next": { "href": "/employees?page=5" },
"previous": { "href": "/employees?page=3" },
"last": { "href": "/employees?page=120" }
},
"count": 10,
"total": 1250,
"_embedded": {
"employees": [
{
"_links": { "self": { "href": "/employees/41" } },
"employeeId": 41
},
{
"_links": { "self": { "href": "/employees/42" } },
"employeeId": 42
},
{
"_links": { "self": { "href": "/employees/43" } },
"employeeId": 43
},
...
]
}
}
Collection
Some representation formats
Consistency
Resources should be consistent in the way they return data.
An API request should (always) return one of the following:
- a single resource
- a collection of resources
- an error message
- no content (redirect, delete)
Consistency
GET /articles --> collection
GET /articles/123 --> resource
GET /articles/123/author --> resource
GET /articles/123/tags --> collection
GET /articles/123/tags/foo --> resource
POST /articles --> returns the newly created resource or
--> 201 Created + redirect to the new resource
PUT or PATCH /articles/123 --> returns the modified resource
DELETE /articles/123 --> 204 No Content
Versioning
Why should your API support different versions?
Backward compatibility!
Versioning
URL:
https://iapi.merck.com/comet-otc/v1/orders
https://iapi.merck.com/comet-otc/v2/orders
Custom request header:
GET /comet-otc/orders
X-Merck-API-Version: 2
Content-Type / Accept header:
GET /comet-otc/orders
Accept: application/vnd.merck.api.comet-otc.v2+json
200 OK
Content-Type: application/vnd.merck.api.comet-otc.v2+json
{ ... }
Versioning
- Distinguish major versions only.
- A major version is a version introducing backward incompatibility.
- When introducing a new version, run it in parallel with the old one for enough time for the clients to switch.
- Do not run too many versions in parallel - ideally run only two at a time.
Design
Do you really need a REST API?
When you don't need an API:
- Synchronization.
- Batch data transfer.
- Asynchronous messaging.
- ...
But you can have an API to manage the process.
API vs. DB
API | DB |
---|---|
GET /resource/123 | SELECT * from table WHERE id = 123 |
POST /resource | INSERT INTO table VALUES (...) |
PATCH /resource/123 | UPDATE table SET field = value, ... WHERE id = 123 |
DELETE /resource/123 | DELETE FROM table WHERE id = 123 |
API vs. DB
When the API is just a thin layer above the DB:
- implementation details are exposed
- internal complexity is revealed
- the API changes every time the DB changes
API vs. DB
APIs should provide a certain level of abstraction
Discussion
Backup
AuthN / AuthZ
Client authenticaton - API key (client ID, client secret, ...)
User authentication - OAuth v2, Open ID Connect, JSON Web Token, ...
GET /comet-otc/orders
X-Merck-APIKey: 4ef87329b14a314a9549fa26a5e3931f
GET /comet-otc/v2/orders
X-Merck-APIKey: 4ef87329b14a314a9549fa26a5e3931f
Authorization: Bearer aa355d09e6a9d0f9e791274f163870be
Security
Confidential vs. public client applications
Mobile app --> public
Browser based HTML5 app --> public
Web app (on a remote server) --> private
Security
Always use HTTPS!
Sensitive values should not be sent as a part of the URL.
GET /comet-otc/v2/orders?apikey=aa355d09e6a9d0f9e791274f163870be
Avoid Basic Auth if possible and use OAuth2 instead. Use standard AuthN/AuthZ mechanisms.
Use rate limiting both on for the client side and the backend side.
Validate all input!
REST API Design - theory
By Ivan Novakov
REST API Design - theory
- 1,748