REST APIs
Best Practices
Why?
Outline
- APIs, REST & JSON
- REST Fundamentals
- Design
- Base URL
- Versioning
- Resource format
- Return Values
- Content Negotiation
- References (linking)
- Pagination
- Query Parameters
- Associations
- Errors
- IDs
- Method Overloading
- Resource Expansion
- Partial Responses
- Caching & Etags
- Security
- Multi Tenancy
- Maintenance
APIs, REST & JSON
APIs
- Applications
- Developers
- Pragmatism over Ideology
- Adoption (make developers happy!!)
- Scale
Why REST?
- Scalability (dissemination)
- Generality (HTTP)
- Independence
- Latency (Caching)
- Security
- Encapsulation
Why JSON?
- Ubiquity
- Simplicity
- Readability
- Scalability
- Flexibility
HATEOAS
- Hypermedia
- As
- The
- Engine
- Of
- Application
- State
Further restriction on REST architectures.
REST Is Easy
REST is *&@#$! Hard
(for Providers)
REST can be Easy
(if you follow some guidelines)
Problem Domain: Churchill
Problem Domain: Churchill.com
-
Navigation L1, L2
- Vehicles
- Gallery
- Specifications
- Offers
- Owner Accounts
- Email verification
- Email validation
- Account management
- Vehicle management
Fundamentals
Resources
-
Nouns, not Verbs.
- Coarse Grained, not Fine Grained.
- Architectural style for use-case scalability.
What if?
/getNavigation
/createAccount
/updateVehicle
/verifyAccountEmailAddress
What if?
/getNavigation
/getAllNavigationItems
/searchAccounts
/createVehicle
/createAccount
/updateVehicleName
/updateUserInformation
/findVehiclesByUser
/searchContentByTitle
/verifyAccountEmailAddress
/verifyAccountEmailAddressByToken
...
Smells like bad RPC. DON'T DO THIS.
Keep It Simple
The Answer
Fundamentally two types of resources:
- Collection Resource
-
Instance Resource
Collection Resource
/accounts
Instance Resource
/accounts/a1b2c3
Behavior
- GET
- PUT
- POST
- DELETE
- HEAD (Metadata)
- PATCH (HTTP1.1 - IE8?? )
Behavior
POST, GET, PUT, DELETE
!= 1 : 1
Create, Read, Update, Delete
Behavior
As you would expect:
GET = Read
DELETE = Delete
HEAD = Headers, no Body
Not so obvious:
PUT and POST can both be used for Create and Update.
PATCH for Update.
PUT for Create
Identifier is known by the client:
PUT /accounts/username
{
...
}
PUT for Update
Full Replacement
PUT /accounts/username
{
"name": "Juan Perez",
"vehicles": [{
...
}]
...
}
PUT
Idempotent
(Send all the fields in the request)
PATCH
Partial updates
Non idempotent.
PATCH /accounts/username
{
"name": "Juan Perez"
}
POST as Create
On a parent resource
POST /accounts
{
"name": "Juan Perez"
}
201 Created
Location: https://api.kia.com/owners/data/accounts/a1b2c3
POST as Update
On instance resource
POST /accounts/a1b2c3
{
"name": "Juan Perez"
}
Response:
200 OK
POST
NOT Idempotent
Media Types
- Format Specification + Parsing Rules
- Request: Accept Header
- Response: Content-Type header
- application/json
- application/kia+json
- application/kia+json;account
- ...
REST Design
Design Time!
Base URL
http(s)://api.kia.com
Vs
http://www.kia.com/dev/service/api/rest
http(s)://api.kia.com
Rest Client
Vs
Browser
Versioning
URL
https://api.kia.com/v1
VS
Media-Type
application/json+kia;account&v=1
DON'T USE 1.3.4
Resource Format
Media Type
Content-Type: application/json
When time allows:
application/kia+json
application/kia+json;account&v=1
...
CamelCase
'JS' in 'JSON' = JavaScript
myArray.forEach
Not myArray.for_each
account.givenName
Not account.given_name
Underscores for property/function names are unconventional for JS. Stay consistent.
Date/Time/Timestamp
There's already a standard. Use it: ISO 8601
Example:
{
...,
"createdTimestamp": "2014-04-10T18:02:24.354Z"
}
Use UTC!
HREF
- Distributed Hypermedia is paramount!
- Every accessible Resource has a unique URL.
- Replaces IDs (IDs exist, but are opaque).
{
"href": "https://api.kia.com/v1/accounts/x7y8z9",
...
}
Critical for linking, as we'll soon see
Response Body
GET obvious
What about POST?
Return the representation in the response when feasible.
Add override (?_body=false) for control
Content Negotiation
Header
- Accept header
- Header values comma delimited in order to preference
GET /accounts/a1b2c3
Accept: application/json, text/plain
Resource Extension
/accounts/a1b2c3.json
/accounts/a1b2c3.csv
...
Conventionally overrides Accept header
Resource References
aka 'Linking'
-
Hypermedia is paramount.
- Linking is fundamental to scalability.
- Tricky in JSON
- XML has it (XLink), JSON doesn't
- How do we do it?
Instance Reference
GET /accounts/x7y8z9
200 OK
{
"href": "https://api.kia.com/v1/accounts/x7y8z9",
"givenName" : "Tony",
"surname": "Stark",
...,
"vehicle": ????
}
Instance Reference
GET /accounts/x7y8z9
200 OK
{
"href": "https://api.kia.com/v1/accounts/x7y8z9",
"givenName" : "Tony",
"surname": "Stark",
...,
"vehicle": {
"href": "https://api.kia.com/v1/vehicles/g4h5i6"
}
}
Collection Reference
GET /navigations/x7y8z9
200 OK
{
"href": "https://api.kia.com/v1/navigations/x7y8z9",
"givenName" : "Tony",
"surname": "Stark",
...,
"items": {
"href": "https://api.kia.com/v1/navigations/x7y8z9/items"
}
}
Reference Expansion
(aka Entity Expansion, Link Expansion)
Account and its Vehicle?
Expand Parameter
GET /accounts/x7y8z9?expand=vehicle
200 OK
{
"href": "https://api.kia.com/v1/accounts/x7y8z9",
"givenName" : "Tony",
"surname": "Stark",
...,
"vehicle": {
"href": "https://api.kia.com/v1/vehicles/g4h5i6","name": "Avengers Sportage","creationDate": "2014-04-15T15:55:52.5654",...
}
}
Partial Representations
GET /accounts/x7y8z9?fields=firstName,surName,vehicle(name)
Pagination
Collections Resource supports query params:
- Offset
- Limit
/accounts/x7y8z9/vehicles?offset=50&limit=25
RESPONSE
GET /accounts/x7y8z9/vehicles
200 OK
{
"href": ".../accounts/x7y8z9/vehicles","offset": 0,"limit": 25,"first": { "href": ".../accounts/x7y8z9/vehicles?offset=0"},"previous": null}"next":{ "href": ".../accounts/x7y8z9/vehicles?offset=25"},
"last": { "href": "..."},
"items": [
{ href: "..."},{ href: "..."},...
]}
Errors
ERRORS
- As descriptive as possible
- As much information as possible
- FE Developers are your customers
POST /vehicles
409 Conflict
{
"status": 409,"code": 40985,"property": "name","message": "A vehicle named 'Avengers Sportage' already exists.","developersMessage": "A vehicle named 'Avengers Sportage' already exists. If you have a stale local cache, please expire it now.","moreInfo": "https://razorfish.attlasian.com/wiki/api/errors/40985"
}
Security
Avoid sessions when possible
-
Authenticate every request if necessary
- Stateless
Use Existing Protocol:
- Oauth 1.0a, Oauth2, Basic over SSL ONLY
Custom schemes?
Use API Keys instead of Username/passwords
401 vs 403
- 401 "Unauthorized" really means Unauthenticated
- "You need valid credentials for me to respond to this request"
- 403 "Forbidden" really means Unauthorized
- "I understood your credentials, but so sorry, you're not allowed!"
HTTP Authentication Schemes
-
Server response to issue challenge:
www-Authenticate: <scheme name>
realm="Application Name"
-
Client request to submit credentials:
Authorization: <scheme name> <data>
API Keys
- Entropy
- Password Reset
- Independence
- Speed (MD5, SHA1, BCrypt)
- Limited Exposure
- Traceability
ID's
- IDs should be opaque
- Should be globally unique
- Avoid Sequential numbers (contention, fusking)
- Good candidates: UUIDs, "Url64"
HTTP Method Overrides
POST /accounts/x7y8z9?_method=DELETE
Caching
Server (initial response)
ETag: "685584758444e7d744d"
Client (later request):
If-None-Match: "685584758444e7d744d"
Server (later response):
304 Not Modified
Maintenance
Use HTTP Redirects
Create abstraction layer / endpoints when migrating
Use well defined custom Media Types
Visit www.kia.com/us
Thanks
REST Best Practices
By Helbert Rico
REST Best Practices
- 607