Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Simple API Development with Laravel

Simple API Development with Laravel

Presentation from Laracon 2013 - Washington, D.C.

Aaron Kuzemchak

February 23, 2013
Tweet

Other Decks in Programming

Transcript

  1. Simple API Development with Laravel Aaron Kuzemchak Laracon 2013 –

    Washington, D.C. kuzemchak.net • @akuzemchak • github.com/akuzemchak
  2. I’m here to hang out with you... the Laravel community

    AKA: The “I used to be with CodeIgniter and met Laravel at the funeral” community
  3. Well, except for my wife ... and my dog ...

    and my college degree No, I’m not from there!
  4. “Representational State Transfer (REST) is a style of so!ware architecture

    for distributed systems such as the World Wide Web …” “REST has emerged as a predominant Web service design model. The REST architectural style was developed in parallel with HTTP/1.1, based on the existing design of HTTP/1.0 ...” “REST exemplifies how the Web's architecture emerged by characterizing and constraining the macro-interactions of the four components of the Web, namely origin servers, gateways, proxies and clients, without imposing limitations on the individual participants.” — Wikipedia
  5. “A RESTful web service (also called a RESTful web API)

    is a web service implemented using HTTP and the principles of REST. It is a collection of resources ...” — Wikipedia
  6. 4. These are my solutions to these problems 4.1. They

    are not applicable to every developer or project
  7. HTTP/1.1 200 OK Content-Type: text/html "1|My first task|false|2012-02-11 22:34:00|2012-02-11 22:34:00\n2|My

    second task|false|2012-02-11 22:36:00|2012-02-11 22:36:00" This was considered a valid JSON response
  8. // app/config/session.php /* |-------------------------------------------------------------------------- | Default Session Driver |-------------------------------------------------------------------------- |

    | This option controls the default session "driver" that will be used on | requests. By default we will use the light-weight cookie driver but | you may specify any of the other wonderful drivers provided here. | | Supported: "cookie", file", "database", "apc", | "memcached", "redis", "array" | */ 'driver' => 'array',
  9. // app/errors.php // General HttpException handler App::error(function(Symfony\Component\HttpKernel\Exception\HttpException $e, $code) {

    $headers = $e->getHeaders(); switch ($code) { case 401: $default_message = 'Invalid API key'; $headers['WWW-Authenticate'] = 'Basic realm="REST API"'; break; case 403: $default_message = 'Insufficient privileges to perform this action'; break; case 404: $default_message = 'The requested resource was not found'; break; default: $default_message = 'An error was encountered'; } return Response::json(array( 'error' => $e->getMessage() ?: $default_message, ), $code, $headers); });
  10. // app/errors.php // ErrorMessageException handler App::error(function(ErrorMessageException $e) { $messages =

    $e->getMessages()->all(); return Response::json(array( 'error' => $messages[0], ), 400); }); // NotFoundException handler App::error(function(NotFoundException $e) { $default_message = 'The requested resource was not found'; return Response::json(array( 'error' => $e->getMessage() ?: $default_message, ), 404); }); // PermissionException handler App::error(function(PermissionException $e) { $default_message = 'Insufficient privileges to perform this action'; return Response::json(array( 'error' => $e->getMessage() ?: $default_message, ), 403); });
  11. GET /lists Get all lists GET /lists/1 Get list w/

    id == 1 GET /lists/1/tasks Get all tasks for list w/ id == 1 GET /lists/1/tasks/1 Get task w/ id == 1 for list w/ id == 1
  12. POST /lists Create new list PUT /lists/1 Update list w/

    id == 1 DELETE /lists/1 Delete list w/ id == 1
  13. POST /lists/1/tasks Create new task for list w/ id ==

    1 PUT /lists/1/tasks/1 Update task w/ id == 1 DELETE /lists/1/tasks/1 Delete task w/ id == 1
  14. 200: Success! 201: Resource created 204: Success, but no content

    to return 400: Request not fulfilled 401: Not authenticated 403: Refusal to respond 404: Not found 500: Other error
  15. // Request setup stuff up here... $response = curl_exec($ch); $code

    = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($code !== 200) { // Handle the error... if ($code === 404) { // Do some additional logging... } } // Continue on with code...
  16. HTTP/1.1 200 OK Content-Type: application/json { "id": 1, "name": "Things

    to do at Laracon", "created_at": "2013-02-04 18:18:52", "updated_at": "2013-02-04 18:18:52" } GET /v1/lists/1
  17. // app/models/TaskList.php public static function findByOwnerAndId($owner, $id) { if (!is_numeric($owner)

    && !($owner instanceof UserInterface)) { throw new InvalidArgumentException('Owner must be either a numeric ID or an instance of UserInterface'); } $list = static::find($id); if (!$list) { throw new NotFoundException('List was not found'); } $owner_id = ($owner instanceof UserInterface) ? (int) $owner->id : (int) $owner; if ((int) $list->user_id !== $owner_id) { throw new PermissionException('Insufficient access privileges for this list'); } return $list; }
  18. // app/models/TaskList.php public function toArray() { $data = parent::toArray(); $data['id']

    = (int) $data['id']; $data['created_at'] = $this->fromDateTime($this->created_at); $data['updated_at'] = $this->fromDateTime($this->updated_at); return $data; }
  19. POST /v1/lists/1/tasks REQUEST: description=Make+CodeIgniter+joke+during+presentation RESPONSE: HTTP/1.1 201 Created Content-Type: application/json

    { "description": "Make CodeIgniter joke during presentation", "updated_at": "2013-02-04 18:23:08", "created_at": "2013-02-04 18:23:08", "id": 1, "completed": false }
  20. // app/routes.php Route::post('lists/{id}/tasks', function($id) { $list = TaskList::findByOwnerAndId(Auth::user(), $id); $task

    = new Task(Input::get()); $task->validate(); $task->list_id = $id; if (!$task->save()) { App::abort(500, 'Task was not saved'); } return Response::json($task->toArray(), 201); })->where('id', '\d+');
  21. HTTP/1.1 200 OK Content-Type: application/json [ { "id": 1, "description":

    "Make CodeIgniter joke during presentation", "completed": false, "created_at": "2013-02-04 18:23:08", "updated_at": "2013-02-04 18:23:08" } ] GET /v1/lists/1/tasks
  22. GET /v1/lists/1/tasks/1 HTTP/1.1 200 OK Content-Type: application/json { "id": 1,

    "description": "Make CodeIgniter joke during presentation", "completed": false, "created_at": "2013-02-04 18:23:08", "updated_at": "2013-02-04 18:23:08" }
  23. // app/routes.php Route::get('lists/{list_id}/tasks/{id}', function($list_id, $id) { $list = TaskList::findByOwnerAndId(Auth::user(), $list_id);

    $task = $list->tasks()->find($id); if (!$task) { App::abort(404); } return Response::json($task->toArray()); })->where('list_id', '\d+')->where('id', '\d+');
  24. PUT /v1/lists/1/tasks/1 REQUEST: completed=true RESPONSE: HTTP/1.1 200 OK Content-Type: application/json

    { "id": 1, "description": "Make CodeIgniter joke during presentation", "completed": true, "created_at": "2013-02-04 18:23:08", "updated_at": "2013-02-04 18:33:19" }
  25. // app/routes.php Route::put('lists/{list_id}/tasks/{id}', function($list_id, $id) { $list = TaskList::findByOwnerAndId(Auth::user(), $list_id);

    $task = $list->tasks()->find($id); if (!$task) { App::abort(404); } $task->fill(Input::get()); $task->validate(); if (!$task->save()) { App::abort(500, 'Task was not updated'); } return Response::json($task->toArray()); })->where('list_id', '\d+')->where('id', '\d+');
  26. // app/routes.php Route::delete('lists/{list_id}/tasks/{id}', function($list_id, $id) { $list = TaskList::findByOwnerAndId(Auth::user(), $list_id);

    $task = $list->tasks()->find($id); if (!$task) { App::abort(404); } $task->delete(); return Response::make(null, 204); })->where('list_id', '\d+')->where('id', '\d+');
  27. POST /v1/lists/1/tasks REQUEST: *This space intentionally left blank* RESPONSE: HTTP/1.1

    400 Bad Request Content-Type: application/json { "error": "Description is required" }
  28. To sum it up: Don’t test in the browser Use

    the “array” session driver Handle errors properly Pay attention to URI design Send good responses
  29. // app/models/User.php public static function createApiKey() { return Str::random(32); }

    // app/events.php User::creating(function($user) { $user->api_key = User::createApiKey(); });
  30. // app/filters.php Route::filter('api.auth', function() { if (!Request::getUser()) { App::abort(401, 'A

    valid API key is required'); } $user = User::where('api_key', '=', Request::getUser())->first(); if (!$user) { App::abort(401); } Auth::login($user); });
  31. Auth w/ cURL in PHP $ch = curl_init('http://api.laracon.app/v1/lists'); curl_setopt($ch, CURLOPT_RETURNTRANSFER,

    true); curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC); curl_setopt($ch, CURLOPT_USERPWD, 'aa284e44ba20b63f880fb68157736829:x'); $response = curl_exec($ch); curl_close($ch);
  32. HTTP/1.1 401 Unauthorized WWW-Authenticate: Basic realm="REST API" Content-Type: application/json {

    "error": "A valid API key is required" } If we fail authentication
  33. // app/filters.php Route::filter('api.limit', function() { $key = sprintf('api:%s', Auth::user()->api_key); //

    Create the key if it doesn't exist apc_add($key, 0, 60*60); // Increment by 1 $count = apc_inc($key); // Fail if hourly requests exceeded if ($count > Config::get('api.requests_per_hour')) { App::abort(403, 'Hourly request limit exceeded'); } });
  34. Utilize your resources Copy from well-designed APIs Have other developers

    use it Ask if you’re not sure about something