Fundamental REST API Design Best Practices 简记

(这篇简记源自观看 REST+JSON API Design - Best Practices for Developers

APIs

Primary users/customers are developers of all levels.

Why REST?

  • Scalability (Not performance, but extensible)
  • Generality
  • Independence
  • Latency (Caching)
  • Security
  • Encapsulation

Why JSON?

  • Ubiquity
  • Simplicity
  • Readability
  • Scalability
  • Flexibility

HATEOAS -> Hypermdedia As The Engine Of Application State

For clients, REST is easy. But for providers, REST is freaking hard.

And REST can be easy if following guidelines.

Fundamentals

Resources

  • Nouns, not verbs, not actions
  • Coarse Grained, not Fine Grained
  • Architectural style for use-case scalability

Give custome resources, let customers define the actual use cases.

Not like this!

Don’t add behaviors/actions to the end URL!

Keep it simple, fundamentally two types of resources:

  • Collection resource: /applications
  • Instance resource: /applications/a1b2c3

Behavior

  • GET
  • PUT
  • POST
  • DELETE
  • HEAD

But GET, PUT, POST, DELETE are not equivalent to Create, Read, Update, Delete! At least it is not guaranteed.

GET is for read and DELETE is for deletion.

But PUT and POST can both be used for create and update.

  • PUT for Create: PUT /application/clientSpecifiedId, all data need to be there for PUT request
  • PUT for update: (Full replacement)
  • PUT is idempotent
  • PUT can not be used for partial update

POST as Create: Create on a parent resource:

1
2
3
4
5
6
7
8
9
Request:
POST /applications
{
"name": "app_name"
}
Response:
201 Created
Location: https:/.../applications/a1b2c3

POST as Update

1
2
3
4
5
6
7
8
Request:
POST /applications
{
"name": "app_name"
}
Response:
200 OK

POST is the only NOT idempotent operation, it can be used for partial update, or anything.

Media Types

  • Format Specification + Parsing Rules
  • Request: Accept header
  • Response: Content-Type header

Examples:

  • application/json
  • application/foo+json
  • application/foo+json;application

When writing REST clients, always put json in Accepted header so the server knows to return json to you.

Design

Base URL

https://api.foo.com vs http://www.foo.com/dev/service/api/rest?

Prefer the first one since it is concise and easy to understand

Rest Client vs Browser? No redirection.

Versioning

URL http://api.foo.com/v1 vs Media-Type application/json+foo;application&v=1

URL is hard for API developers, Media-Type is hard for API users. URL scheme is more preferred.

Use coarse version numbers in URL scheme.

Resource Format

  • Use camelCase as JS in JSON is JavaScript: myArray.forEach not my_array.for_each
  • Date/Time/Timestamp: Use ISO 8601. And use UTC!

HREF:

  • Every accessible Resource has a unique URL
  • Replaces IDs (IDs exist, but are opaque)
1
2
3
{
"href": "https://api.foo.com/v1/accounts/x7y8z9",
}

HREF is critical for linking.

Response Body

  • GET is must
  • POST? Return the representation in the response when feasible. And override (?_body=false) for control

Content Negotiation

  • Accept header
  • Header values comma delimited in order of preference

Resource Extension:

  • applications/a1b2c3.json
  • applications/a1b2c3.csv

Resource References a.k.a ‘Linking’

  • Hypermedia is paramount
  • Linking is fundamental to scalability
  • Tricky in JSON (XML has XLink, JSON doesn’t) How do we do it?

Instance Reference

1
2
3
4
5
6
7
8
9
GET /accounts/x7y8z9
200 OK
{
"href": "https://api.foo.com/v1/accounts/x7y8z9",
"name": "Tony",
...
"directory": ??? // What to do here?
}

Stormpath way:

1
2
3
4
5
6
7
8
9
10
11
GET /accounts/x7y8z9
200 OK
{
"href": "https://api.foo.com/v1/accounts/x7y8z9",
"name": "Tony",
...
"directory": {
"href": "https://api.foo.com/v1/directories/g4h516"
}
}

Collection Reference

1
2
3
4
5
6
7
8
9
10
11
GET /accounts/x7y8z9
200 OK
{
"href": "https://api.foo.com/v1/accounts/x7y8z9",
"name": "Tony",
...
"groups": {
"href": "https://api.foo.com/v1/accounts/x7y8z9/groups"
}
}

Reference Expansion

Use extra expand argument

1
2
3
4
5
6
7
8
9
10
11
12
13
GET /accounts/x7y8z9?expand=directory
200 OK
{
"href": "https://api.foo.com/v1/accounts/x7y8z9",
"name": "Tony",
...
"directory": {
"href": "https://api.foo.com/v1/directories/g4h516"
"name": "Avengers",
"creationDate": ...
}
}

This is a separate resource from REST’s perspective

Partial Representation

GET /accounts/x7y8z9?fields=givenName,surname,directory(name) to restrict the response data

Pagination

Collection Resource supports query params:

  • Offset
  • Limit

/applications?offset=50&limit=25

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
GET /accounts/x7y8z9?groups
200 OK
{
"href": "https://api.foo.com/v1/accounts/x7y8z9/groups",
"offset": 0,
"limit": 25,
"first": {
"href": "https://api.foo.com/v1/accounts/x7y8z9/groups?offset=0"
},
"previous": null,
"next": {
"href": "https://api.foo.com/v1/accounts/x7y8z9/groups?offset=25"
},
"last": {
"href": "https://api.foo.com/v1/accounts/x7y8z9/groups?offset=500"
},
"items": {
{
"href": "..."
},
{
"href": "..."
}
...
}
}

Many to Many

e.g. Group to Account

  • A group can have many accounts
  • An account can be in many groups
  • Each mapping is a resource: groupMemberships
1
2
3
4
5
6
7
8
9
10
11
12
13
GET /groupMemberships/231k3j2j3
200 OK
{
"href": ".../groupMemberships/231k3j2j3",
"account": {
"href": "..."
},
"group": {
"href": "..."
},
...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
GET /accounts/x7y8z9
200 OK
{
"href": ".../accounts/x7y8z9",
"name": "Tony",
"group": {
"href": "..."
},
"groupMemberships": {
"href": ".../groupMembership?accountId=x7y8z9"
},
...
}

Errors

  • As descriptive as possible
  • As much information as possible (security information should not be exposed!)
  • Developers are your customers
1
2
3
4
5
6
7
8
9
10
11
POST /directory
409 Conflict
{
"status": 409,
"code": 40924,
"property": "name",
"message": "A directory named 'Avengers' already exists",
"developerMessage": "A directory named 'Avengers' already exists. If you have a stale local cache, please expire it now.",
"moreInfo": "https://www.foo.com/docs/api/errors/40924"
}

Security

  • Avoid sessions when possible
    • Authenticate every request if necessary
    • Stateless
  • Authorize based on resource content, NOT URL!
  • Use existing protocal:
    • Oauth 1.0a, Oauth2, Basic over SSL only
  • Custom Authentication Scheme:
    • Only if you provide client code/SDK
    • Only if you really, really now what you’re doing
  • Use API Keys instead of username/password combos

401 vs 403

  • 401 “Unauthorized” really means unauthenticated: You need valid credentials!
  • 403 “Forbidden” really means unauthorized: You are not allowed!

HTTP Authentication Schemes

  1. Server response to issue challenge: WWW-Authenticate:<scheme name> realm="Application Name"
  2. Client request to submit credentials: Authorization:<scheme name><data>

API Keys

  • Entropy (takes much longer time to brute force attack)
  • Password Reset
  • Independence
  • Speed (Bcrypt is more reliable than SHA1 and MD5)
  • Limited Exposure
  • Traceability

IDs

  • IDs should be opaque
  • Should be globally unique
  • Avoid sequential numbers (contention, fucking)
  • Good candidates: UUIDs, Url64

HTTP Methods

POST /accounts/x7y8z9?_method=DELETE

Caching

  • Server (initial response):
    • ETag: “1234567890”
  • Client (later request):
    • If-None-Match: “1234567890”
  • Server (later response):
    • 304 Not Modified

Maintenance

  • Use HTTP Redirects
  • Create abstraction layer / endpoints when migrating
  • Use well defined custom Media Types