Slide 1

Slide 1 text

Simple API Development with Laravel Aaron Kuzemchak Laracon 2013 – Washington, D.C. kuzemchak.net • @akuzemchak • github.com/akuzemchak

Slide 2

Slide 2 text

Who am I?

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

Why am I here today?

Slide 5

Slide 5 text

I’m in love with Laravel Have been since v2

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

What we’ll be going over today

Slide 9

Slide 9 text

1. Foundation & best practices

Slide 10

Slide 10 text

2. Authentication

Slide 11

Slide 11 text

3. Rate limits

Slide 12

Slide 12 text

Why Laravel?

Slide 13

Slide 13 text

“The perfect merger of simplicity and power” — me

Slide 14

Slide 14 text

REST is part of the foundation

Slide 15

Slide 15 text

Filters & events keep things clean

Slide 16

Slide 16 text

And also...

Slide 17

Slide 17 text

Laravel is perhaps Arkansas’ greatest export since Walmart

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

Anywho...

Slide 20

Slide 20 text

Let’s talk about REST

Slide 21

Slide 21 text

What is it?

Slide 22

Slide 22 text

“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

Slide 23

Slide 23 text

No content

Slide 24

Slide 24 text

“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

Slide 25

Slide 25 text

REST uses standard HTTP methods to perform specific actions

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

Let’s build a sample REST API

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

A few things to note before we start:

Slide 31

Slide 31 text

1. I assume you have basic knowledge of Laravel

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

There are, however, a lot of wrong ways

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

KEEP REFRIGERATED

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

No content

Slide 41

Slide 41 text

This means there are a lot of bad APIs out there

Slide 42

Slide 42 text

Some awful stuff I’ve seen:

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

I know...

Slide 47

Slide 47 text

No content

Slide 48

Slide 48 text

Which brings us to:

Slide 49

Slide 49 text

Foundation & best practices

Slide 50

Slide 50 text

0. Find a good testing tool

Slide 51

Slide 51 text

Testing directly in the browser is frustrating

Slide 52

Slide 52 text

Postman for Chrome

Slide 53

Slide 53 text

RESTed for Mac $2.99 in App Store

Slide 54

Slide 54 text

HTTPie for terminal junkies

Slide 55

Slide 55 text

1. Session driver

Slide 56

Slide 56 text

Every API request will spawn a new session

Slide 57

Slide 57 text

Lots of junk rows/keys/files

Slide 58

Slide 58 text

No content

Slide 59

Slide 59 text

Unnecessary cookies

Slide 60

Slide 60 text

No content

Slide 61

Slide 61 text

Solution: Use the “array” driver

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

You can still use session features

Slide 64

Slide 64 text

No persistence, no cookies

Slide 65

Slide 65 text

2. Error handling

Slide 66

Slide 66 text

Setup our errors to return JSON

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

3. URI design & routes

Slide 70

Slide 70 text

/{type}/{id} /{type}/{id}/{sub-type}/{id}

Slide 71

Slide 71 text

/lists/1 /lists/1/tasks/1

Slide 72

Slide 72 text

Method + URI = Action

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

Versioning is a very good idea

Slide 78

Slide 78 text

/v1/lists/1 /v1/lists/1/tasks/1

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

Completely re-architect later without breaking anything

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

4. Send good responses That includes headers!

Slide 83

Slide 83 text

Response codes tell your users what happened

Slide 84

Slide 84 text

{"success": false} The old way

Slide 85

Slide 85 text

Common response codes

Slide 86

Slide 86 text

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

Slide 87

Slide 87 text

An example use case

Slide 88

Slide 88 text

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

Slide 89

Slide 89 text

Let’s look at how our API responds

Slide 90

Slide 90 text

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

Slide 91

Slide 91 text

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

Slide 92

Slide 92 text

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

Slide 93

Slide 93 text

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

Slide 94

Slide 94 text

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

Slide 95

Slide 95 text

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

Slide 96

Slide 96 text

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 }

Slide 97

Slide 97 text

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

Slide 98

Slide 98 text

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

Slide 99

Slide 99 text

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

Slide 100

Slide 100 text

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

Slide 101

Slide 101 text

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

Slide 102

Slide 102 text

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

Slide 103

Slide 103 text

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

Slide 104

Slide 104 text

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

Slide 105

Slide 105 text

What about errors?

Slide 106

Slide 106 text

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

Slide 107

Slide 107 text

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

Slide 108

Slide 108 text

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

Slide 109

Slide 109 text

Authentication

Slide 110

Slide 110 text

Serves two primary purposes

Slide 111

Slide 111 text

1. Identify the user

Slide 112

Slide 112 text

2. Keep that user from causing trouble

Slide 113

Slide 113 text

Don’t reinvent the wheel

Slide 114

Slide 114 text

See what other APIs do

Slide 115

Slide 115 text

You’re probably going to see these options:

Slide 116

Slide 116 text

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

Slide 117

Slide 117 text

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

Slide 118

Slide 118 text

3. OAuth Tokens, tokens, tokens!

Slide 119

Slide 119 text

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

Slide 120

Slide 120 text

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

Slide 121

Slide 121 text

We need to generate API keys for our users

Slide 122

Slide 122 text

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

Slide 123

Slide 123 text

Now we create a filter

Slide 124

Slide 124 text

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

Slide 125

Slide 125 text

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

Slide 126

Slide 126 text

Auth w/ Postman

Slide 127

Slide 127 text

Auth w/ RESTed

Slide 128

Slide 128 text

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

Slide 129

Slide 129 text

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

Slide 130

Slide 130 text

If we pass, request succeeds

Slide 131

Slide 131 text

Rate limits

Slide 132

Slide 132 text

Second line of defense against abuse Authentication is the first

Slide 133

Slide 133 text

Determine how and what to limit

Slide 134

Slide 134 text

In our case: Overall hourly limit per user

Slide 135

Slide 135 text

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

Slide 136

Slide 136 text

•APC •Memcached •Redis

Slide 137

Slide 137 text

Very lightweight and fast

Slide 138

Slide 138 text

Automatically expire keys at specified intervals

Slide 139

Slide 139 text

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

Slide 140

Slide 140 text

No content

Slide 141

Slide 141 text

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

Slide 142

Slide 142 text

A simple filter to do this

Slide 143

Slide 143 text

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

Slide 144

Slide 144 text

Counts will reset every hour automatically

Slide 145

Slide 145 text

Closing thoughts

Slide 146

Slide 146 text

API’s don’t need to be complicated

Slide 147

Slide 147 text

Please try to stick to REST principles

Slide 148

Slide 148 text

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

Slide 149

Slide 149 text

No content

Slide 150

Slide 150 text

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