Web Api Design « »
March 3, 2016
Introduction
Api Ecosystem
HTTP / RPC
- Verbs Included in API
- URI Endpoints
Restful
Stateless
- URI Endpoints
- Resource URIs
- HTTP Verbs
- Stateless Server
- Content Negotiation
HATEOAS
Hipermedia as the engine of application state.
- Resource URIs
- HTTP Verbs
- Stateless Server
- Content Negotiation
- Link Relations
Resource Based Architectures
- Resources are representations of Real Wolrd Entities (People, Invoices, Payments, …)
- Relationships are typically nested
- Hierarchies or Webs, NOT Relational Models
- Resources are represented in URIs
- Query strings for non-data elements (eg. format, sorting, etc)
REST
REpresentational State Transfer
- Seperation of Client and Server
- Server Requests are Stateless
- Cacheable Requests
- Uniform Interface
Hypermedia
- Method of self-describing messages
- Links in resources that describe how to process data
- Hyperlinks for resources
- Hypermedia == HATEOAS
- It can be very useful or just additional overhead, so it is only as important as we need it to be
- Self-documenting APIs
Desigining the API
URI Design
- Nouns are Good, Verbs are Bad
- Prefer Plurals (e.g.: Customers, Gaimes, Invoices)
- Use Identifiers to locate individual items in URIs
- Does not have to be Internal Key… but it has to point to only one item.
Using Verbs
Resource | GET (read) | POST (create) | PUT (update) | DELETE (delete) |
---|---|---|---|---|
/Customers | Get List | Create Item | Update Batch | Error |
/Customers/123 | Get Item | Error | Update Item | Delete Item |
Returning
Resource | GET (read) | POST (create) | PUT (update) | DELETE (delete) |
---|---|---|---|---|
/Customers | List | New Item | Status Code Only | Status Code Only* |
/Customers/123 | Item | Status Code Only* | Updated Item | Satus Code Only |
Update Example
Headers
// Method PUT
User-Agent: Fiddler
Host: localhost:8863
Content-Type: application/json
Request Body
{"id":1,"name":"Pere Pages"}
Response
HTTP/1.1 200 OK
Status Codes
Code | Description | Code | Description |
---|---|---|---|
200 | OK | 400 | Bad Request |
201 | Created | 401 | Not Authorized |
202 | Accepted | 403 | Forbidden |
302 | Found | 404 | Not Found |
304 | Not Modified | 405 | Method Not Allowed |
307 | Temp Redirect | 409 | Conflict |
308 | Perm Redirect | 500 | Internal Error |
- Use HTTP Status Codes (minimum 200,400,500)
- 200 “It worked”
- 400 “You did bad”
- 500 “We did bad”
- Probably
- 201 Created
- 304 Not Modified
- 404 Not Found
- 401 Unauthorized
- 403 Forbidden
Associations
- For sub-objects
- Use URI Navigation (‘http://myDomain.com/api/Customers/123/Invoices’)
- Should return a List of Related Objects or a Related Object
- May include multiple associtations on same object
- ‘http://myDomain.com/api/Customers/123/Invoices’
- ‘http://myDomain.com/api/Customers/123/Payments’
- ‘http://myDomain.com/api/Customers/123/Shipments’
- Anything more complex should use query string
- ‘http://myDomain.com/api/Customers?state=GA’
- ‘http://myDomain.com/api/Customers?state=GA&salesperson=144’
- ‘http://myDomain.com/api/Customers?hasOpenOrders=true’
Formatting Results
- Content Negotiation is the Best Practice. Use Accept header to determine how to format.
- Use the URI components to format (not the best practice)
- ‘http://myDoomain/api/Customers?format=json’
- ‘http://myDoomain/api/Customers?format=jsonp&callback=foo’
Accept Header
GET /api/games2 HTTP/1.1
Accept: applications/json,text/xml
Host: localhost:8863
Content Types
Type | MIME Type |
---|---|
JSON | application/json |
XML | text/xml |
JSONP* | applicaiton/javascript |
RSS | application/xml+rss |
ATOM | application/xml+atom |
- Requires callback query parameter too:
http://mydomain/api/Customers?callback=foo
Designing Results
Single Results
Single results should be simple objects
- Member names (naming shouldn’t expose server details)
- just be consistent!
- camelCasing
- _underscore
- just be consistent!
Collections
- Wrap the collection around a single object which can contain information about the set.
Collection Example
{
"numberResults": 345,
"results": [
{"id": 1,"name": "Pere Pages"},
{"id": 2,"name": "John Smith"},
{"id": 3,"name": "Andrew Williams"},
]
}
ETags (Entit Tags)
- Header to support smart server caching
- Strong and Weak Caching Support
- Returned in the Response
- Client should sent ETag back to see if new version is available
- Request with If-None-Match
- Use 304 to indicate that it hasn’t changed
- Cliend Should Send ETag back to see if new version is available
- For PUT, use If-Match
- Use status code if it doesn’t match (412 Precondition failed)
Example
// The ETag shows a version number for the Entity
HTTP/1.1 200 OK
Content-Type: text/xml; charset=utf-8
Date: Thu, 30 March 2015 08:23:45 GMT
ETag: "4894526782065"
Content-Length: 639
// Weak ETag, weak cache
HTTP/1.1 200 OK
Content-Type: text/xml; charset=utf-8
Date: Thu, 30 March 2015 08:23:45 GMT
ETag: W/"4894526782065"
Content-Length: 639
If-None-Match example
// The reponse will have 304 if the entity hasn't changed
GET /api/games/2 HTTP/1.1
Accept: application/json, text/xml
Host: localhost:8863
If-None-Match: "489302392098"
If-Match example
// PUT /api/games/2 HTTP/1.1
// It will retgurn HTTP/1.1 412 Precondition failed if the entity has changed
Accept: application/json, text/xml
Host: localhost: 8863
If-Match: "4893023942098"
Paging
- Lists should always support paging
- Query String parameters to request paging information
- Object wrapper to indicate next/prev links
- Can use different page sizes too
- Limit size of page to limit sever load
Paging URI example
http://myDomain.com/api/games?page=5&pageSize=50
Paging Result example
{
"totalREsults": 1958,
"nextPage" : "http://myDomain/api/games/?page=3",
"prevPage" : "http://myDomain/api/games/?page=1",
"results" : [...]
}
Partial Items
- Allow for request of partial items
- Query String is a common pattern for requesting parts
- Updating of Partial Items
- Can use PATCH HTTP Verb
- Update of partial is possible with ETag support
Update using Patch example
PATCH /api/games/2 HTTP/1.1
Accept: application/json
If-Match: "1234567890"
Host: localhost:8863
Content-Length: 192
{
"id": 2,
"name": "Super Mario Bros",
"price" : 150.0,
"imageUrl" : "...",
...
}
Requesting parts example
http://myDomain.com/api/games/123?fields=id,name,price,imageUrl
Non-Resource APIs
Non resource calls.
- Functional Parts of your API (see example to understand)
- Be pragmatic and make sure that these parts of our API are documented
- Be sure that the user can tell it is a different type of operation
- Should be completely functional
- Don’t use them as an excuse to build a PRC API
Functional Call example
http://myDomain.com/api/calculateTax?state=GA&total=149.99
http://myDomain.com/api/restartServer?isColdBoot=true
http://myDomain.com/api/beginWorldDomination?isVolcanoLairRequired=true
Versioning
Versioning with URI components are more common.
You must version your API.
Why?
- Once you publish an API, it’s set in stone
- Publishing an API is not a trivial move
- Users/Customers rely on the API not changing, but requirements will change
- Need a way to evolve the API without breaking existing clients
- API Versioning is not Product Versioning
Examples
- Tumblr (URI path): ‘http://api.tumblr.com/v2/user’
- Netflix (URI parameter): ‘http://api.netfilix.com/catalog/titles/series/7002464?v=1.5’
- GitHub API (Content Negotiation): ‘Content Type: application/vnd.github.1.param+json’
- Azuere (Request Header): ‘x-ms-version: 2011-08-18’
URI Path
Allow to drastically change the API. Everything below the version is open to change.
http://myDomain.com/api/v1/Customers?type=Current&id=123
http://myDomain.com/api/vs/CurrentCustomers/123
- Pro(s)
- Simple to segregate old APIs for backwards compatibility
- Con(s)
- Requires lots of client changes as you version
- Increases the size of URIS to maintain
URI Parameter
Being an optional parameter, being withut it would always point to the latest version.
- Pro(s)
- Without version, users always get latest version of the API
- Little client changes as versions mature
- Con(s)
- CAn surprise developers with unintened changes
Content Negotiation
Instead of using standard MIME types, use custom. Can include informatin in Accept Header for format too.
Standard indicates you can use “vnd.” as starting point (meaning vendor).
Examples
GET /api/customer/123
HOST: http://...
Accept: application/vnd.myapp.v1.customer
GET /api/customer/123
HOST: http://...
Accept: application/vnd.myapp.v1.customer.json
- Pro(s)
- Packages API and Resource Versioning in one
- Removes versioning from API so clients don’t have to change
- Con(s)
- Adds complexity - adding headers ins’t esasy on all platforms
- Can encourage incread versioning which causes more code churning
Request Headers
Should be a header value that is only of value to your API. Common use of Date instead of number.
GET /api/customer/123
HOST: http://...
x-MyApp-Version: 2.1
GET /api/customer/123
HOST: http://...
x-MyApp-Version: 2015-09-12
- Pro(s)
- Separates Versioning from API call signatures
- Not tied to resource versioning (e.g. Content Type)
- Con(s)
- Adds complexity - adding headers ins’t easy on all platforms
Resource Versioning
Structures and constraints change.
Versioning with Custom Content Types is easier, but adds complexity. Including Version in resource body pollutes the data.
Web Apis Security
- SSL is almost always appropiate
- Cros domain security
- Authorization and Authentication
Cross Domain Security
- Support JSONP as a format
- Enable Cross-origin Resource Sharing (CORS)
- Requires some handshaking
Who should you Authenticate
Who is calling the API?
- Server-to-Server Authentication
- API Keys and Shared Screts
- User Proxy Authentication
- OAuth or Similar
- Direct User Authentication (your own apps)
- Cookies or token
Definitions
- Credential (A fact that describes an identity - email, id, password …)
- Authentication (validate a set of credentials to identify an entity)
- Authorization (verification that an entity has rights to access a resource or action)
Working with API keys
For non-user specific usage.
How it works
Developer Signs Up for API -> API Issues API key and Shared Secret
Developer creates Request -> Action API Key Timestamp -> Developer Signs Request with Shared Secret -> Developer Sends Request + Signature to Service -> Service Loks up Shared Secret Via API Key -> Service Signs Request With Shared Secret -> Service Verifies Same Signature and Within Timeout -> Executes Request or Returns Error
User Security
- 1st party API -> Piggybacking on web security is acceptable
- 3rd party API -> OAuth
OAuth
- Allows you to accept user credentials
- Ten trust a 3rd party with a token that represents the API developer
- The developer never receives user credentials
Developer | API | User |
---|---|---|
Requests API Key | Supplies API Key and Shared Secret | |
Requests Request Token | Validates and Returns Token | |
Redirects to API’s Auth URI | Displays Authorization UI | User Confirms Authoriation |
Redirects Back To Developer | ||
Request Access Tocken Via OAuth & Request Token | Returns Access Token (With Timeout) | |
Ues API with Acccess Token until Timeout |
Hypermedia
Going for HATEOAS HAL is the right choice right now.
If the users are internal or your API is trivial, using hypermedia may be more a ceremony than useful.
REST and HATEOAS
- Are links for APIs
- Links are to help developers know how to use the API
- API becomes self-describing
- Links become the Application State
- Hypermedia As the Engine of Application State (HATEOAS)
Links
- Links should help developer use the API
- Common scenarios:
- Paging
- Creating New Items
- Retrieving Associations
- Actions
- Common scenarios:
{
"totalResults": 1598,
"links":[
{"href":"http://localhost:8863/api/v1/games?page=1","rel":"prevPage"},
{"href":"http://localhost:8863/api/v1/games?page=3","rel":"nextPage"},
{"href":"http://localhost:8863/api/v1/games","rel":"insert"}
],
"results":[...]
}
{
"links": [
{"href":"http://myDomain.com/api/v1/Games/2","rel":"self"}
{"href":"http://myDomain.com/api/v1/Games/2/rating","rel":"rating"}
],
"id":2,
"name": "Final Fantasy",
"description":"Final Fantasy"
}
Standard HATEOAS Formats
Profile Media Types
Profiles are descriptions of the data
- Alternative to using custom MIME type in versioning
- Should be included inside the Accept header
- Though servers can return the profile too
GET /api/order/123
HOST: http://.../
Accept: application/json;profile=http://wilderminds.com/orders
HAL
HAL: Hypertext Application Language
- A lean Hypermedia type
- Plain-old Formats (JSON or XML) to inlcude resources and links
- application/hal+json
- application/hal+xml
- application/hal+json;profile=http://wilderminds.com/orders
{
"_links:"{
"self": {"href":"/games"},
"next":{"href":"/games?page=2"},
"find":{"href":"/games{?query}","templated":true}
},
"totalCount":100,
"_embedded": {
"games": [
{"_links": {
"self":{"href":"/games/123"}
},
"price": 30.00,
"currency": "USD",
"name":"Halo 3"
}
]
}
}
Collection + JSON
- Supports standard for reading and writing of collections
- Defines standard way of communicationg lists and individual items
- Includes UI elements in messages
- Ues MIME Type:
- application/vnd.collection+json
- Uses Profile Media Type too:
- application/vnd.collection+json;profile=http://foo.com/bar
- Good for simple, machine driven lists
- But probably too verbose for real data-driven REST
- Including UI elements sets scene for automatinc code
- But you just end up paying for it in lage payloads
{
"collection": {
"version":"1.0",
"href":"http://example.org/friends/",
"links": [
{"rel": "feed", "href": "http://example.org/friends/rss"}
],
"items":[
{
"href":"http://example.org/friends/jdoe",
"data":[
{"name":"full-name","value":"J.Doe","prompt":"Full Name"},
{"name":"email","value":"[email protected]","prompt":"Email"},
]
}
],
"links": [
{"rel":"blog","href":"http://examples.org/blogs/jdoe","prompt":"Blog"}
],
"queries": [
{"rel":"search","href":"http://example.org/friends/search","prompt":"Search",
"data": [{"name":"search","value":""}]
}
],
"template": {
"data":[
{"name":"full-name","value":"","prompt":"Full Name"},
{"name":"email","value":"","prompt":"Email"},
{"name":"blog","value":"","prompt":"Blog"},
{"name":"avatar","value":"","prompt":"Avatar"}
]
}
}
}