Rest API
Fundamentals
Resources
- Nouns, not verbs
- Coarse grained, not fine grained
I.e
/getAccount /createDirectory /updateGroup /verifyAccountEmailAddress
does not scale because resources is tightly coupled to behaviours.
Resource types
Collection resources
/applications
- self documenting, plural of the instance resources
Instance resources
/applications/a1b2c3
Behavior
- GET
- PUT
- POST
- DELETE
- HEAD (metadata request)
Post, Get, Put, Delete does not neccessery have a one-to-one mapping to CRUD.
GET = READ DELETE = DELETE HEAD = Headers, no data
PUT and POST can both be used for CREATE and UPDATE
Using PUT to CREATE or UPDATE: /applications/myapplicationid
PUT can be used to create a resource where the resourceid is given by the PUT.
PUT request can not contain partial data, it must contain all data that is needed to CREATE or UPDATE the resource. This is because PUT is idempotent according to the HTTP specification. This mean that a PUT request gives the same result if it is executed one or more times. In our REST API that means that the serverstate is the same after the first and fifth PUT request.
Using POST to CREATE: /applications
POST is used to create a resource on the parent resource, i.e the resource id is not given in the POST request. When using POST to CREATE its important to give a 201 response to tell the client where the new resource is living, i.e "Location: https://fqdn/applications/myapplicationid".
Using POST top UPDATE: /apllications/myapplicationid
PUT can be used to do a partial update of a resource. POST is not idempotent according to the specifications. You can make it idempotent, but you dont have to. It is important to give a 200 ok response if the update succeed.
Media types
- Format specifications + parsing rules
- Requests: Accept header
- Response: Content-Type header
The rest client set a comma separated list of mediatypes in the Accept header, i.e "application/json, plain/text", meaning that the client support both but prefer "application/json".
The server set the Content-Type header telling the client what it actually can have.
- application/json , global accepted json mediatype
- application/foo+json , your own mediatype (can be registered with IANA to be global)
- application/foo+json;application , your own mediatype and resourcetype
Design
Base URL
- Keep the baseurl short
http(s).api.foo.com VS http(s)://www.foo.com/dev/service/api/rest
- Clients can be REST applications or webbrowser. Threat the in the same way. If you do, developers can test and debug their application by using the webbrowser to investigate your API.
Versioning
URL: https://api.foo.com/v1
- Clean and concise
- Can be cumbersome if versionnumbers changes often so try to keep it stable
- Pragmatic
or
Mediatype: application/json+foo;application&v1
- more flexible for server developers
- more heeavyweight and complicated with custom mediatypes
- idealistic
Resource format
Coventions
camelCase
JS in JSON = JavaScript
myCollection.forEach
not
myColection.for_each
Underscores are unconventional in JavaScript, stay consistent.
Date / Time / Timestamp
There is a standard, ISO 8601, use it!
2015-05-24 06:07Z 2015-W21-7 06:07Z
Also use UTC to avoid problems.
HREF
- Distributed hypermedia is paramount!
- Every accessible resource should have its own unique URL
- Replace IDs (IDs exist, but are opaque)
- Always return a resource unique URL as a property when its consumed
{ href: https://www.foo.com/dev/service/api/rest/apllications/myapplicationid }
Response body
Should you always return a response body when queried?
- If its a GET, its obvious
- If feasible, return the representation in the response
- Allow override, ?_body=false, for control
Content negotiation
- Header, Accept
Comma separated list of values, in order of preference
GET /apllications/myapplicationid
Accept: application/json, text/plain
- Resource Extension
Conventionally overrides the Accept header
GET /apllications/myapplicationid.json GET /apllications/myapplicationid.csv
Resources References, linking
- Linking is fundamental to scalability
- Tricky in JSON
- XML has it (XLink), JSON doesnt
Instance reference
GET /apllications/myapplicationid
200 OK { "href": "https://www.foo.com/dev/service/api/rest/apllications/myapplicationid", "property1": "Value1", "property2": "Value2", ... , "directory": { "href": ""https://www.foo.com/dev/service/api/rest/directories/someid" } } ##### Collection referenceGET /apllications/myapplicationid200 OK { "href": "https://www.foo.com/dev/service/api/rest/aplications/myapplicationid", "property1": "Value1", "property2": "Value2", ... , "groups": { "href": ""https://www.foo.com/dev/service/api/rest/aplications/myapplicationid/groups" } } #### Reference Expansion How to obtain a resource and its linked properties in the same repsonse?GET /applications/myapplicationid?epxand=directory200 OK { "href": "https://www.foo.com/dev/service/api/rest/apllications/myapplicationid", "property1": "Value1", "property2": "Value2", ... , "directory": { "href": "https://www.foo.com/dev/service/api/rest/directories/someid", "name": "Name of a directory", } }#### Partial representationsGET /applications/myapplicationid?fields=property2, directory(name)200 OK { "property2": "Value2", ... , "directory": { "name": "Name of a directory", } } #### Pagination Collection resource should support query parameters: * Offset * Limit../applications?offset=50&limit=20or you could embed the needed information in the collection resource itself.GET /accounts/id/groups200 OK { "href": "../accounts/id/groups", "offset": 0, "limit": 25, "first": { "href": "../accounts/id/groups?offset=0" }, "previous": null, "next": { "href": "../accounts/id/groups?offset=25" }, "last": { "href": ".." }, "items": [ { "href": ".." }, { "href": ".." }, ... ]#### Many to many Group to account: * Group can have many accounts * An account can be in many groups * Each mapping is a resource Use GroupMembership as a resource that represents all the groups an account is assosiated with.GET /groupMemberships/someuniqueid 200 OK { "href": "../groupMemberships/someuniqueid", "account": { "href": "..", }, "group": { "href": ".." }, ... } Which will give us a accountobject that looks like:GET /applications/myapplicationid200 OK { "href": "../aplications/myapplicationId", "property1": "Value1", "property2": "Value2", ... , "groups": { "href": "../applications/myapplicationId/groups" }, "groupMemberships": { "href": "../applications/myapplicationid/groupMemberships?applicationId=myapplicationId" } }#### Errors * As descriptive as possible * As much information as possible * Developers are your customers Example:POST /directories409 Conflict { "status": 409, "code": 40924, "property": "name", "message": "A resource named 'Somename' already exists.", "developerMessage": "A resource named 'Somename' already exists. If you have a stale local cache, please exire it now.", "moreInfo": "https://fqdn/docs/api/errors/40924" }## Security ### General tips * Avoid sessions when possible ** Authenticate every request if neccessary ** Stateless * Authorize basd on resource content, NOT URL * Use existing protocol: ** Oauth 1.0a, Oauth2, Basic over SSL only * Custom authentication scheme: ** Only if you provide client code / SDK ** Only if you really, really know what you are doing * Use API keys instead if 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 you are not allowed ### HTTP Authentication schemes * Server response to issue challenge:WWW-Authenticate:* Client request to submit credentials:realm="Application name" Authorization:### API keys * Entropy * Password reset * Independence * Speed * Limited exposure * Traceability ### Ids * IDs should be opaque * Should be globally unique * Avoid sequential numbers (contention, fuskin) * Good candidates: UUIDs, Url64 ### HTTP method overrides If a client does not support other methods than i.e POST and GET you can add method overrides to support the remaning methods. POST /applications/myapplicationId?_method=DELETEAlways to method overrides in POST requests due to the idempotency. ### Caching & Concurrency control * Server (initial response): ** ETAG: "6483483485jdfjg39" * Client (later request): ** If-None-Match: "6483483485jdfjg39" * Server (later response): ** 304 Not Modified Remember that if you use SSL, caching is not an issue ### Maintaince * Use HTTP redirects * Create abstraction layer / endpoints when migrating * Use well defined custom media types