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

    View full-size slide

  2. Where to find me online
    Blog: http://kuzemchak.net/
    Twitter: @akuzemchak
    Github: akuzemchak

    View full-size slide

  3. Why am I here today?

    View full-size slide

  4. I’m in love with Laravel
    Have been since v2

    View full-size slide

  5. 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

    View full-size slide

  6. I’m here to talk to you about
    building REST APIs

    View full-size slide

  7. What we’ll be going
    over today

    View full-size slide

  8. 1. Foundation & best practices

    View full-size slide

  9. 2. Authentication

    View full-size slide

  10. 3. Rate limits

    View full-size slide

  11. Why Laravel?

    View full-size slide

  12. “The perfect merger of simplicity
    and power”
    — me

    View full-size slide

  13. REST is part of the foundation

    View full-size slide

  14. Filters & events keep things
    clean

    View full-size slide

  15. Laravel is perhaps Arkansas’
    greatest export since Walmart

    View full-size slide

  16. Well, except for my wife
    ... and my dog
    ... and my college degree
    No, I’m not from there!

    View full-size slide

  17. Let’s talk about REST

    View full-size slide

  18. “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

    View full-size slide

  19. “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

    View full-size slide

  20. REST uses standard HTTP
    methods to perform specific
    actions

    View full-size slide

  21. GET ‛ Read
    POST ‛ Create
    PUT ‛ Update
    DELETE ‛ Duh

    View full-size slide

  22. Also, it’s much cooler that
    other Web service protocol
    I’m the king of the WSDL!

    View full-size slide

  23. Let’s build a sample
    REST API

    View full-size slide

  24. A TODO list API
    I know... boring but effective

    View full-size slide

  25. A few things to note before we
    start:

    View full-size slide

  26. 1. I assume you have basic
    knowledge of Laravel

    View full-size slide

  27. 2. All of the code is available
    on Github
    akuzemchak/laracon-todo-api

    View full-size slide

  28. 3. Keeping things very simple
    No content negotiation, HATEOAS, ETags, or
    other assorted fanciness

    View full-size slide

  29. 4. These are my solutions to
    these problems
    4.1. They are not applicable to every
    developer or project

    View full-size slide

  30. 5. There is no one right way to
    build an API

    View full-size slide

  31. There are, however, a lot of
    wrong ways

    View full-size slide

  32. When building a REST API, you
    should follow the established
    guidelines/principles

    View full-size slide

  33. KEEP REFRIGERATED

    View full-size slide

  34. But they aren’t enforced, so it
    still technically works if you
    don’t follow them

    View full-size slide

  35. This means there are a lot of
    bad APIs out there

    View full-size slide

  36. Some awful stuff I’ve seen:

    View full-size slide

  37. This was considered a valid
    404 response
    HTTP/1.1 200 OK
    Content-Type: text/html
    Not found

    View full-size slide

  38. 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

    View full-size slide

  39. So was this
    HTTP/1.1 200 OK
    Content-Type: text/html
    MXxNeSBmaXJzdCB0YXNrfGZhbHNlfDIwMTItMDItMTEgMjI6MzQ6MDB8MjAxMi0w
    Mi0xMSAyMjozNDowMAoyfE15IHNlY29uZCB0YXNrfGZhbHNlfDIwMTItMDItMTEg
    MjI6MzY6MDB8MjAxMi0wMi0xMSAyMjozNjowMA==

    View full-size slide

  40. Which brings us to:

    View full-size slide

  41. Foundation & best
    practices

    View full-size slide

  42. 0. Find a good testing tool

    View full-size slide

  43. Testing directly in the browser
    is frustrating

    View full-size slide

  44. Postman for Chrome

    View full-size slide

  45. RESTed for Mac
    $2.99 in App Store

    View full-size slide

  46. HTTPie for terminal junkies

    View full-size slide

  47. 1. Session driver

    View full-size slide

  48. Every API request will spawn a
    new session

    View full-size slide

  49. Lots of junk rows/keys/files

    View full-size slide

  50. Unnecessary cookies

    View full-size slide

  51. Solution:
    Use the “array” driver

    View full-size slide

  52. // 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',

    View full-size slide

  53. You can still use session
    features

    View full-size slide

  54. No persistence, no cookies

    View full-size slide

  55. 2. Error handling

    View full-size slide

  56. Setup our errors to return
    JSON

    View full-size slide

  57. // 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);
    });

    View full-size slide

  58. // 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);
    });

    View full-size slide

  59. 3. URI design & routes

    View full-size slide

  60. /{type}/{id}
    /{type}/{id}/{sub-type}/{id}

    View full-size slide

  61. /lists/1
    /lists/1/tasks/1

    View full-size slide

  62. Method + URI = Action

    View full-size slide

  63. 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

    View full-size slide

  64. POST /lists
    Create new list
    PUT /lists/1
    Update list w/ id == 1
    DELETE /lists/1
    Delete list w/ id == 1

    View full-size slide

  65. 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

    View full-size slide

  66. While we’re on the topic of
    URIs...

    View full-size slide

  67. Versioning is a very good idea

    View full-size slide

  68. /v1/lists/1
    /v1/lists/1/tasks/1

    View full-size slide

  69. // app/routes.php
    Route::group(array('prefix' => 'v1', 'before' => 'api.auth|api.limit'), function()
    {
    // Routes go here...
    });
    Easy with route groups and
    prefixes

    View full-size slide

  70. Completely re-architect later
    without breaking anything

    View full-size slide

  71. So you won’t catch a
    beatdown for breaking your
    users apps

    View full-size slide

  72. 4. Send good responses
    That includes headers!

    View full-size slide

  73. Response codes tell your users
    what happened

    View full-size slide

  74. {"success": false}
    The old way

    View full-size slide

  75. Common response codes

    View full-size slide

  76. 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

    View full-size slide

  77. An example use case

    View full-size slide

  78. // 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...

    View full-size slide

  79. Let’s look at how our API
    responds

    View full-size slide

  80. 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

    View full-size slide

  81. // app/routes.php
    Route::get('lists/{id}', function($id)
    {
    $list = TaskList::findByOwnerAndId(Auth::user(), $id);
    return Response::json($list->toArray());
    })->where('id', '\d+');

    View full-size slide

  82. // 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;
    }

    View full-size slide

  83. // 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;
    }

    View full-size slide

  84. GET /v1/lists/1/tasks
    HTTP/1.1 200 OK
    Content-Type: application/json
    []

    View full-size slide

  85. // app/routes.php
    Route::get('lists/{id}/tasks', function($id)
    {
    $list = TaskList::findByOwnerAndId(Auth::user(), $id);
    return Response::json($list->tasks->toArray());
    })->where('id', '\d+');

    View full-size slide

  86. 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
    }

    View full-size slide

  87. // 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+');

    View full-size slide

  88. 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

    View full-size slide

  89. 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"
    }

    View full-size slide

  90. // 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+');

    View full-size slide

  91. 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"
    }

    View full-size slide

  92. // 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+');

    View full-size slide

  93. DELETE /v1/lists/1/tasks/1
    HTTP/1.1 204 No Content

    View full-size slide

  94. // 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+');

    View full-size slide

  95. What about errors?

    View full-size slide

  96. GET /v1/bazinga
    HTTP/1.1 404 Not Found
    Content-Type: application/json
    {
    "error": "The requested resource was not found"
    }

    View full-size slide

  97. 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"
    }

    View full-size slide

  98. 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

    View full-size slide

  99. Authentication

    View full-size slide

  100. Serves two primary purposes

    View full-size slide

  101. 1. Identify the user

    View full-size slide

  102. 2. Keep that user from causing
    trouble

    View full-size slide

  103. Don’t reinvent the wheel

    View full-size slide

  104. See what other APIs do

    View full-size slide

  105. You’re probably going to see
    these options:

    View full-size slide

  106. 1. API key via query string
    Old school APIs... don’t do this

    View full-size slide

  107. 2. HTTP authentication
    Secret API key or user/pass for credentials

    View full-size slide

  108. 3. OAuth
    Tokens, tokens, tokens!

    View full-size slide

  109. Since we’re talking about
    simple, we’ll go with option #2

    View full-size slide

  110. Specifically:
    API key as username over HTTP Basic auth
    Password can be anything... we don’t care

    View full-size slide

  111. We need to generate API keys
    for our users

    View full-size slide

  112. // app/models/User.php
    public static function createApiKey()
    {
    return Str::random(32);
    }
    // app/events.php
    User::creating(function($user)
    {
    $user->api_key = User::createApiKey();
    });

    View full-size slide

  113. Now we create a filter

    View full-size slide

  114. // 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);
    });

    View full-size slide

  115. 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);

    View full-size slide

  116. Auth w/ Postman

    View full-size slide

  117. Auth w/ RESTed

    View full-size slide

  118. http --auth aa284e44ba20b63f880fb68157736829:x api.laracon.app/v1/lists
    Auth w/ HTTPie

    View full-size slide

  119. 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

    View full-size slide

  120. If we pass, request succeeds

    View full-size slide

  121. Second line of defense against
    abuse
    Authentication is the first

    View full-size slide

  122. Determine how and what to
    limit

    View full-size slide

  123. In our case:
    Overall hourly limit per user

    View full-size slide

  124. Pick a key/value storage
    engine
    Preferably an in-memory solution

    View full-size slide

  125. •APC
    •Memcached
    •Redis

    View full-size slide

  126. Very lightweight and fast

    View full-size slide

  127. Automatically expire keys at
    specified intervals

    View full-size slide

  128. Let’s use APC, since you should
    already be running it

    View full-size slide

  129. Because there’s no way to
    update a value without
    changing the TTL

    View full-size slide

  130. A simple filter to do this

    View full-size slide

  131. // 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');
    }
    });

    View full-size slide

  132. Counts will reset every hour
    automatically

    View full-size slide

  133. Closing thoughts

    View full-size slide

  134. API’s don’t need to be
    complicated

    View full-size slide

  135. Please try to stick to REST
    principles

    View full-size slide

  136. Utilize your resources
    Copy from well-designed APIs
    Have other developers use it
    Ask if you’re not sure about something

    View full-size slide

  137. That’s it for me!
    Thanks for listening
    kuzemchak.net • @akuzemchak • github.com/akuzemchak

    View full-size slide