Beautiful REST
API Design
with Lumen
Brian Retterer
@bretterer
brian@stormpath.com
Developer Evangelist at Stormpath
Self Proclaimed Whovian
Laravel / WordPress / PHP Hacker
http://charisteapot.tumblr.com/post/116544339467/tardis-charisteapot-doctor-who-fanart-this-is
/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
"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
!= 1:1
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
Identifier is known by Client:
PUT /applications/{clientSpecifiedId}
{
...
}Full Replacement:
PUT /applications/{existingId}
{
"name": "Marvel Comics",
"description": "We are the best!"
}On Parent Resource
POST /applications
{
"name": "DC Comics",
"description": "We are the best!"
}On Instance Resource:
POST /applications/abc123
{
"description": "Ok Marvel is better"
}/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
Accept: application/json
Content-Type: application/json
https://example.io/developer/api/public/rest
vs.
https://api.example.io
https://api.example.io/?version=1
vs.
https://api.example.io/v1
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"
}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"
}]
}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"
}
}
}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"}
]
}
}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"}
]
}
}GET /
200 OK
{
"meta": {"href": "https://example.io/"},
"tasks": {
"meta": {
"href": "https://example.io/tasks",
"rel": ["collection"]
}
}
}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"
}Date/Time/Timestamp
{
...
"createdAt": "2016-10-19T17:55:00.123Z"
...
}Use UTC (Zulu) time
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
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
ID's
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
);
}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(),
];
}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"
]
];
}public static function collectionToArray(
Collection $collection,
$offset,
$limit,
$total
)
{
return [
'meta' => [...]
'items' => $collection->toArray(),
'create' => $collection->createModel(),
'search' => $collection->searchModel()
];
}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']
]
];
}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
Rate: https://joind.in/talk/fde8d
Slides: http://bit.ly/2egbcYH