Beautiful REST

API Design

with Lumen

Brian Retterer

@bretterer

 

brian@stormpath.com

Who is Brian Retterer?

Developer Evangelist at Stormpath

Self Proclaimed Whovian

Laravel / WordPress / PHP Hacker

http://charisteapot.tumblr.com/post/116544339467/tardis-charisteapot-doctor-who-fanart-this-is

Bad REST API design

/getTask?id=3

/createTask

/deleteTask?id=3

/updateTask?id=3&json=1

/getAllTasks

/searchForTask?category=home

/getTaskComments?id=3&return=json

/getTask?id=3&withComments=1

/getAllTask?format=json

/searchForTask?category=home&return=xml

/deleteTask?id=3

/addCommentToTask?id=3&comment=This+API+SUCKS

/getTask?id=3&return=json

/getTask?id=3&withComments=1&fmt=json

/getTaskComments?id=3

/updateTask?id=3

/getAllTasks?json=true

Bad

REST
API

HATEOAS

"A REST API should be entered with no prior knowledge beyond the initial URI (bookmark) and set of standardized media types that are appropriate for the intended audience (i.e., expected to be understood by any client that might use the API). From that point on, all application state transitions must be driven by client selection of server-provided choices that are present in the received representations or implied by the user’s manipulation of those representations."  - Dr. Fielding

WHAT?

What is a good REST API?

  • Discoverable and self documented
  • Represent Instance Resources and Collections
  • Use HTTP verbs
  • Keep it simple where possible (KISS Method)

Lets
Talk
Verbs

Behavior

Create, Read, Update, Delete

!= 1:1

Post, Get, Put, Delete

Behavior

  • GET = READ

  • DELETE = DELETE

  • HEAD = Headers Only, No Body

  • OPTIONS = All available options for endpoint

POST and PUT can be used for create and update

PUT FOR CREATE

Identifier is known by Client:

PUT /applications/{clientSpecifiedId}

{
	...
}

PUT FOR UPDATE

Full Replacement:

PUT /applications/{existingId}

{
	"name": "Marvel Comics",
	"description": "We are the best!"
}

POST FOR CREATE

On Parent Resource

POST /applications

{
    "name": "DC Comics",
    "description": "We are the best!"
}

POST FOR UPDATE

On Instance Resource:

POST /applications/abc123

{
    "description": "Ok Marvel is better"
}

Rebuilding
Our
Example

Rebuilding Our Example

/tasks

/tasks/3

/tasks/3/comments

/comments

GET: List all tasks

POST/PUT: Create Task

GET: List the task

POST/PUT: Update the task

DELETE: Delete the task

GET: List the tasks Comments

POST/PUT: Create a comment for Task

GET: List all comments

Search:

/tasks?category=home

Pagination:

/tasks?offset=1&limit=10

Media Types

  • Request: Accept Header
  • Response: Content-Type Header

 

Accept: application/json

Content-Type: application/json

Domain Design

https://example.io/developer/api/public/rest

 

vs.

 

https://api.example.io

Versioning

https://api.example.io/?version=1

 

vs.

 

https://api.example.io/v1

RESPONSES

ION Working Group

http://ionwg.org/draft-ion.html

Getting Single User

GET /tasks/3


200 OK
{
    "meta": {"href": "https://example.io/tasks/3"},
    "item": "Do Something",
    "description": "Get out and do something good today!"
    "category": "urgent"
}

Getting All Users

GET /tasks

200 OK
{
    "meta": {"href": "https://example.io/tasks},
    "items": [{
        "meta": {"href": "https://example.io/tasks/3"},
        "item": "Do Something",
        "description": "Get out and do something good today!"
        "category": "urgent"
    }, {
        "meta": {"href": "https://example.io/tasks/4"},
        "item": "Something Else",
        "description": "Do some more good!"
        "category": "home"
    }]
}

Linked Resources

GET /tasks/3


200 OK
{
    "meta": {"href": "https://example.io/tasks/3},
    "item": "Do Something",
    "description": "Get out and do somthing good today!",
    "category": "urgent",
    "comments": { 
        "meta": {
            "href": "https://example.io/tasks/3/comments"
        } 
    }
}

Discoverable Forms

GET /tasks
200 OK
{
    "meta": {"href": "https://example.io/tasks"},
    "items": [...],
    "create": {
        "meta": {
            "href": "https://example.io/tasks",
            "rel": ["create-form"],
            "method": "POST"
        },
        "items": [
            {"name": "item"},
            {"name": "description"},
            {"name": "category"}
        ]
    }
}

Discoverable Forms

GET /tasks

200 OK
{
    "meta": {"href": "https://example.io/tasks"},
    "items": [...],
    "create": {...}
    "search": {
        "meta": {
            "href": "https://example.io/tasks",
            "rel": ["search-form"],
            "method": "GET"
        },
        "items": [
            {"name": "category"}
        ]
    }
}

The Gateway (example.io/)

GET /

200 OK
{
    "meta": {"href": "https://example.io/"},
    "tasks": {
        "meta": {
            "href": "https://example.io/tasks",
            "rel": ["collection"]
        }
    }
}

How to respond

with errors

Errors Should...

  • be as descriptive as possible
  • provide a way to get more info
  • be clear about http error code
  • have reference to internal info

Error Message

POST /tasks


409 Conflict

{
    "status": 409,
    "code": 40924,
    "property": "email",
    "message": "A task with the item `Do Something` already exists.",
    "developerMessage": "A task with the item `Do Something` 
              already exists. If you have a stale local cache, 
              please expire it now",
    "moreInfo": "https://docs.example.io/errors/40924"
}

401 VS 403

  • 401 "Unauthorized" really means Unauthenticated
  • 403 "Forbidden" really means Unauthorized

Side Notes

Date/Time/Timestamp

{
    ...
    "createdAt": "2016-10-19T17:55:00.123Z"
    ...
}

Use UTC (Zulu) time

Side Notes

Resource Extensions

https://example.io/tasks/3.xml
https://example.io/tasks/3.txt
https://example.io/tasks/3.html

Adding an extension will override the Accept header

Side Notes

Allow Method Overrides

POST /tasks/3?_method=DELETE

This should be done if you expect your users to use the web browser or some way that does not allow for anything verbs other than GET and POST

Side Notes

ID's

  • Avoid auto incrementing ids
  • Should be globally unique
  • Good Candidate: UUID

Examples
In
Lumen

Api Request

public static function apiRequest($offset = 0, $limit = 25)
{
  $query = Task::orderBy('created_at', 'desc');
  $total = $query->count();
  $tasks = $query->skip($offset)->take($limit)->get();
  return Task::collectionToArray(
        $tasks, 
        $offset, 
        $limit, 
        $total
    );
}

Collection To Array

public static function collectionToArray(
    Collection $collection, 
    $offset, 
    $limit, 
    $total
)
{
  return [
    'href' => getenv('APP_API_URL') . "/tasks",
    'offset' => $offset,
    'limit' => $limit,
    'size' => $total,
    'items' => $collection->toArray(),
  ];
}

To Array

public function toArray()
{
  $baseUrl = getenv('APP_API_URL');
  return [
    "href" => $baseUrl . "/tasks/{$this->id}",
    "title" => $this->item,
    "description" => $this->description,
    "comments" => [
        "href" => $baseUrl . "/tasks/{$this->id}/comments"
    ]
  ];

}

Forms

public static function collectionToArray(
    Collection $collection, 
    $offset, 
    $limit, 
    $total
)
{
  return [
    'meta' => [...]
    'items' => $collection->toArray(),
    'create' => $collection->createModel(),
    'search' => $collection->searchModel()
  ];
}

Forms

public function createModel()
{
  $baseUrl = getenv('APP_API_URL');

  return [
    'meta' => [
      'href' => $baseUrl . '/tasks',
      'rel' => ['create-form'],
      'method' => 'POST'
    ],
    'items' => [
      ['name' => 'item'],
      ['name' => 'description'],
      ['name' => 'category']
    ]
  ];
}

Forms

public function searchModel()
{
  $baseUrl = getenv('APP_API_URL');

  return [
    'meta' => [
      'href' => $baseUrl . '/tasks',
      'rel' => ['search-form'],
      'method' => 'GET'
    ],
    'items' => [
      ['name' => 'category']
    ]
  ];
}

http://rafflebot.io/

Win Liona!

Beautiful REST

API Design

with Lumen

Brian Retterer

@bretterer

 

brian@stormpath.com

Thank You!

Rate: https://joind.in/talk/fde8d

Slides: http://bit.ly/2egbcYH

Designing a Beautiful REST+JSON API with PHP

By Brian Retterer

Designing a Beautiful REST+JSON API with PHP

  • 1,206