RESTful or RESTless

Javascript Romandie Meetup

Created by olivier morel / @algorithme

The Big Picture

The Hidden Details

Identifying resources?

  • Domain entities (photo, user, article, etc.)
  • Collections (list of articles, address book, etc.)
  • Composites (a user with its address book and following events, etc.)
  • Computing/Processing (convert an image, calculate the area of a circle, etc.)

Designing URI

  • Use domains & subdomains:
    - http://mobile.example.net (client-type)
    - http://fr.example.net (language)
  • Forward slash separator /
  • Underscore OR Hyphen
    - http://api.example.net/photo_book/spring_holidays/photo/dsn_265
  • Query separator with the Ampersand &
    - http://api.example.net/search?field=id&name_start=a
  • Use %20 to represent spaces
  • Be careful with capital letters, avoid them
  • Check your framework support!!!

Resource representation

  • representation consist of 2 parts: Header, Body
  • avoid to create new headers, use the existing one, yours could be ignored by proxy, caches, etc.
  • use standards: dates, currency, languages, locales
  • consider your clients to select the best representations to implement (HTML, JSON for JS, XML for Java, etc.)

Header

  • Content-Type: describe the type of the content (in requests too!)
  • Content-Length: size of body in bytes
  • Content-Language: language of the content
  • Content-MD5: md5 of the body
  • Content-Encoding: describe how the body was encoded (gzip, compress, deflate)
  • Last-Modified: the time the resource was modified

Body

  • structured format: JSON, XML, etc.
  • plain text
  • binary: images, video, audio, etc.
  • multipart: ie. JSON + image (not nice with JS, so prefer to use 2 resources and link them)

HTTP Methods

Method Safe? Idempotent? Main usages
GET YES YES retrieve a resource
HEAD YES YES retrieve a resource metadata only, check for existence
OPTIONS YES YES retrieve supported methods for a resource
PUT NO YES replace or create a resource
DELETE NO YES delete a resource
POST NO NO other actions

GET example

# GET request for a simple resource
GET /user/1 HTTP/1.1
Host: api.example.net

# No body


# GET response
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8

# Body
{ "id": "urn:user:1",
  "username": "algorithme",
	"email": "olivier@example.net" }

HEAD example

# HEAD request for a simple resource
HEAD /article/1 HTTP/1.1
Host: api.example.net

# No body


# HEAD response
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Last-Modified: Sun, 29 Mar 2009 08:00:00 GMT

# No body

PUT example (resource creation)

# PUT request to create a new resource with resource identifier
PUT /user/1/article/1 HTTP/1.1
Host: api.example.net

# Body
{ "title": "My first article"
  "body": "..." }


# PUT response
HTTP/1.1 201 Created
Content-Type: application/json;charset=UTF-8
Last-Modified: Sun, 29 Mar 2009 08:00:00 GMT

# Body
{ "id": "urn:user:1:article:1"
  "title": "My first article"
  "body": "..." }

PUT example (update completely)

# PUT request to update a resource completely
PUT /user/1/article/1 HTTP/1.1
Host: api.example.net

# Body
{ "title": "My first article"
  "body": "Ok I'll write some text" }


# PUT response
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Last-Modified: Sun, 29 Mar 2009 08:00:00 GMT

# Body
{ "id": "urn:user:1:article:1"
  "title": "My first article"
  "body": "Ok I'll write some text" }

DELETE example

# DELETE request
DELETE /user/1/article/1 HTTP/1.1
Host: api.example.net

# No Body

# PUT response
HTTP/1.1 204 No Content

# No Body

POST example (resource creation)

# POST request to create a new resource
POST /user/1/task HTTP/1.1
Host: api.example.net

# Body
{ "name": "prepare my slides"
  "priority": 10 }

# POST response
HTTP/1.1 201 Created
Location: http://api.example.net/user/1/task/1
Content-Location: http://api.example.net/user/1/task/1
Content-Type: application/json;charset=UTF-8

{ "id": "urn:user:1:task:1"
  "name": "prepare my slides"
  "priority": 10 }

POST example (modify partial resource)

# POST request to modify part of the resource
POST /user/1/article/1/title HTTP/1.1
Host: api.example.net

# Body
a new name


# PUT response
HTTP/1.1 303 See Other
Location: http://api.example.net/user/1/article/1

# No Body

Asynchronous tasks with POST

# POST request to initiate the task
POST /video/conversion/convert_to_mp4 HTTP/1.1
Host: api.example.net
Content-Type: application/json;charset=UTF-8

# Body
{ "src": "https://youtu.be/Kbwk2vwXNyU" }


# POST response with newly created task
HTTP/1.1 202 Accepted
Content-Location: http://api.example.net/video/conversion/task/1
Content-Type: application/json;charset=UTF-8
Date: Sun, 13 Sep 2009 01:49:27 GMT

# Body
{ "id": "urn:video:conversion:task:1",
  "status": "pending",
  "self": { "href": "http://api.example.net/video/conversion/task/1" },
  "message": "Your request has been accepted and is being processed.",
  "ping-after": "2009-09-13T01:49:42Z" }


# After a while
# GET request to check the state of the processing
GET /video/conversion/task/1 HTTP/1.1
Host: api.example.net

# No body


# GET response with new state of the processing
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8

# Body
{ "id": "urn:video:conversion:task:1",
  "status": "pending",
  "self": { "href": "http://api.example.net/video/conversion/task/1" },
  "message": "Your request is being processed.",
  "ping-after": "2009-09-13T01:49:57Z" }


# After a while
# GET request to check the state of the processing
GET /video/conversion/task/1 HTTP/1.1
Host: api.example.net

# No body


# GET response with the result of the processing
HTTP/1.1 303 See Other
Location: http://api.example.net/video/youtube/1
Content-Location: http://api.example.net/video/youtube/1

# Body
{ "id": "urn:video:conversion:task:1",
  "status": "done",
  "self": { "href": "http://api.example.net/video/conversion/task/1" },
  "message": "Your request has been successfully processed." }

Asynchronous tasks with DELETE

# DELETE request to initiate the task
DELETE /user/1 HTTP/1.1
Host: api.example.net
Content-Type: application/json;charset=UTF-8

# No Body


# DELETE response with newly created task
HTTP/1.1 202 Accepted
Content-Location: http://api.example.net/user/task/1
Content-Type: application/json;charset=UTF-8
Date: Sun, 13 Sep 2009 01:49:27 GMT

# Body
{ "id": "urn:user:task:1",
  "status": "pending",
  "self": { "href": "http://api.example.net/user/task/1" },
  "message": "Your request has been accepted and is being processed.",
  "ping-after": "2009-09-13T01:49:42Z" }


# After a while
# GET request to check the state of the processing
GET /user/task/1 HTTP/1.1
Host: api.example.net

# No body


# GET response with new state of the processing
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8

# Body
{ "id": "urn:user:task:1",
  "status": "done",
  "self": { "href": "http://api.example.net/user/task/1" },
  "message": "Your request has been processed.",
  "ping-after": "2009-09-13T01:49:57Z" }

HTTP Error Code

Code Message Description
4xx Client Inputs Error
400 Bad Request general code
403 Forbidden not allowed to access
404 Not Found resource does not exist (or not allowed to access)
405 Not Allowed http method not handled
5xx Server Error
500 Internal Server Error something somewhere went wrong
503 Service Unavailable temporary not available (Retry-After)

How to return errors

# HTTP Error Header
HTTP/1.1 403 Forbidden
Content-Type: application/json;charset=UTF-8
Content-Language: en
Date: Wed, 14 Oct 2009 10:16:54 GMT
Link: <http://api.example.net/user/errors/unauthorized_access.html>;rel="help"

# HTTP body
{ "message": "User is not authorized to other users profile.",
  "error-id": "error-access-unauthorized-profile",
  "rel": { "href": "http://api.example.net/user/2" }
}

How to handle errors in the client

try {
  response = httpRequest.send("GET ...")

  if (response.code >= 200 && response.code < 400) {
    // Success handle the success based on the action
    ...
  } else if (response.code == 500) {
    // Server error
    // Try to work without a server for a while :)
    ...
  } else if (response.code == 503) {
    // Temporary server error
    delay = response.header.retry_after
    wait(delay)

    // Wait delay and then retry
  } else if (response.code >= 400) {
    // Error in the input, there should be an implementation error in the client
    // You could retry, after modifying some parameters or methods
  }
} catch (NetworkFailure failure) {
  // Retry now or with a delay
  ...
}

Working with links

  • Goal: Teach the client to use the RESTful API
  • Side benefit: Stateless communication between client and server
  • Links can be set in content
    {"name": "olivier", "alternate": "http://api.example.net/user/dump/1"}
  • or in the header (ie. for binary format)
    Link: <http://api.example.net/user/dump/1>;rel=alternate
  • Can help design permission systems, the server can return which actions are possible on the resource

Queries

GET /events?after=2015-01-01&before=2015-12-31&sortBy=date&limit=10

# GET response HEADER
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: max-age=60
Content-Language: en

# GET response BODY
{ "total": 50,
  "events": [
    { "title": "...", "date": "...", "link": {
      "rel": "http://api.example.net/rels/event",
      "href": "http://api.example.net/event/1",
    }},
    ...
  ],
  "next": "http://api.example.net/events?after=2015-01-01&before=2015-12-31&sortBy=date&limit=10&start=10",
  "last": "http://api.example.net/events?after=2015-01-01&before=2015-12-31&sortBy=date&limit=10&start=40"
}

Big Queries

  • Size of URI is limited (not by specification, but by implementation)
  • Try to create queries template such as:
    - http://api.example.net/events?view=last_month
    - http://api.example.net/events?view=every-friday
  • Otherwise use POST, but there will be no caching

Content negociation

  • Accept: specify the desired type
    - Accept: application/json;q=1.0, application/html;q=0.6, */*;q=0.0
  • Accept-Language: specify the desired language
    - Accept-Language: fr;q=1.0, en;q=0.5
  • Accept-Encoding: specify the desired encoding
    - Accept-Encoding: gzip
  • Can also be specified in the URI (subdomain, parameters, path, etc.)
    - http://fr.example.net/article/1
    - http://api.example.net/article/1?format=json

YES, there a lot more to look at?

  • How do you handle authentication?
  • How do you document?
  • How can you improve caching?
  • What is Conditional Request and how to use it to improve performances?
  • How do you implement transactions?
  • Versionning anybody?

But... Enough for Today!

Let's have a chat, any question?

Created by olivier morel / @algorithme