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.htmlAdding an extension will override the Accept header
Side Notes
Allow Method Overrides
POST /tasks/3?_method=DELETEThis 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