Building a simple API

Laravel Montréal #3 - September 25th, 2014

Before we start ...

Thanks for comming !

Your feedback is important !

Sponsors

Jobs

Get involved

Comming soon in Laravel Montréal

Getting Started

What in the world is an API ?

In computer programming, an application programming interface (API) specifies a software component in terms of its operations, their inputs and outputs and underlying types. Its main purpose is to define a set of functionalities that are independent of their respective implementation, allowing both definition and implementation to vary without compromising each other.

It's like Mc'Donalds drive through

Let's talk about REST

"A RESTful web service (also called a RESTful web API) is a web service implemented using HYYP and the principles of REST. It's a collection of resources..."
- Wikipedia

Let's talk about REST

  • REST uses standard HTTP methods to perform specific actions
  • GET => Read
  • POST => Create
  • PUT => Update
  • DELETE => Delete really ??

There is not a right way to do things...

but there are a series of good practives that you can follow :

Mistakes you want to avoid ...

Standards are not inforced...

Your API will probably still work if you dont follow them...

but it will eventually catch you up !

This is a valid 404 response they said...


HTTP/1.1 200 OK
Content-Type: text/html

Not found

		

This is valid JSON


HTTP/1.1 200 OK
Content-Type: text/html

"1|My first contact|benjamin.rosell@gmail.com|2014-12-12|2015-12-20|active
\n2|My second contact|jonathan.rosell@gmail.com|2013-10-19|2010-24-04|inactive"

		

And so was this...


HTTP/1.1 200 OK
Content-Type: text/html

VHlwZSBvciBwYXN0ZSBhbnkgc3RyaW5nIHRvIHRoZSBibHVlIHRleHQgYm94LiBJdCBjYW4gYmUgZWl0aGVyIG5vcm1
hbCBzdHJpbmcgb3IgQkFTRTY0IHN0cmluZy4=

		

DO not use the browser to test...

Best practices

Simple routing

Just follow the RESTful standards

/{type}/{id}

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

For instance...

/contacts/2

/contacts/1/insurance/24

Method + URI = Action

GET /contacts

Gets all contacts

GET /contacts/1

Gets contact with ID of 1

POST /contacts

Creates a new contact

PUT /contacts/2

Upadtes the contact with an id of 2

DELETE /contacts/2

Deletes the contact with an id of 2

Laravel makes it really easy for you !


// Simple resource routing
Route::resource('/contacts', 'ContactsController');

		

It's always nice to version your API

GET /v1/contacts


Route::group(array('prefix' => 'v1'), function()
{
		Route::resource('/contacts', 'ContactsController');
});

		

Setting up the Controller


php artisan controller:make ContactsController

	

Route::resource('/contacts', 'ContactsController');

	

class ContactsController extends BaseController {

	public function index() {}          // GET    /
	public function create() {}         // GET    /create
	public function store() {}          // POST   /
	public function show($id) {}        // GET    /1
	public function edit($id) {}        // GET    /1/edit
	public function update($id) {}      // PUT    /1
	public function destroy($id) {}     // DELETE /1

}

	

php artisan routes

	

Migrations

Preparing


php artisan migrate:make create_contacts_table --create="contacts"

		

Migration File


use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreatePostsTable extends Migration {

	/**
	 * Run the migrations.
	 *
	 * @return void
	 */
	public function up()
	{
		//
	}

	/**
	 * Reverse the migrations.
	 *
	 * @return void
	 */
	public function down()
	{
		//
	}

}

		

Schema Building


public function up()
{
	Schema::create('contacts', function(Blueprint $table) {
		$table->increments('id');
		$table->string('name', 50);
		$table->string('email')->nullable()->unique();
		$table->string('telephone')->nullable()->unique();
		$table->boolean('active')->default(true);
		$table->timestamps();
	});
}


public function down()
{
	Schema::drop('contacts');
}

		

Run Migrations


php artisan migrate

		

Ooops!


php artisan migrate:rollback

			

Models

Eloquent


class Contact extends Eloquent {}

	

Getting all results



    /**
     * Display a listing of contacts
     *
     * @return Response
     */
    public function index()
    {
        return Contact::all();
    }

	

Maybe even better



    /**
     * Display a listing of contacts
     *
     * @return Response
     */
    public function index()
    {
        $contacts =  Contact::all();
        return Response::json($contacts->toArray());
    }

	

GET /contacts


HTTP/1.1 200 OK
Content-Type: text/html

{
	[
		"id": "1",
		"name": "Benjamin Gonzalez",
		"email": "benjamin.rosell@gmail.com",
		"telephone": "514-236-1889",
		"active": "true",
		"created_at": "2014-09-25 10:00:00",
		"updated_at": "2014-09-25 10:00:00"
	]
}

	

Storing a new contact



    /**
     * Store a newly created contact in storage.
     *
     * @return Response
     */
    public function store()
    {
        $validator = Validator::make($data = Input::all(), Contact::$rules);

        if ($validator->fails())
        {
            App::abort('500', 'The model was not saved, validation failed');
        }

        $contact = Contact::create($data);
        return Response::json($contact->toArray(), 201);
    }


	

POST /contacts


REQUEST:
name=Benjamin+Gonzalez&email=benjamin.rosell@gmail.com&telephone=514+236+1889

RESPONSE:
HTTP/1.1 201 Created
Content-Type: text/html

{
	"id": "1",
	"name": "Benjamin Gonzalez",
	"email": "benjamin.rosell@gmail.com",
	"telephone": "514-236-1889",
	"active": "true",
	"created_at": "2014-09-25 10:00:00",
	"updated_at": "2014-09-25 10:00:00"
}

	

Showing a contact



    /**
     * Display the specified contact.
     *
     * @param  int  $id
     * @return Response
     */
    public function show($id)
    {
        return Response::json(Contact::findOrFail($id));
    }


	

GET /contacts/1


HTTP/1.1 200 OK
Content-Type: text/html

{
	"id": "1",
	"name": "Benjamin Gonzalez",
	"email": "benjamin.rosell@gmail.com",
	"telephone": "514-236-1889",
	"active": "true",
	"created_at": "2014-09-25 10:00:00",
	"updated_at": "2014-09-25 10:00:00"
}

	

Updating the contact



    /**
     * Update the specified resource in storage.
     *
     * @param  int  $id
     * @return Response
     */
    public function update($id)
    {
        $contact = Contact::findOrFail($id);

        $validator = Validator::make($data = Input::all(), Contact::$rules);

        if ($validator->fails())
        {
            App::abort('500', 'The model was not saved');
        }

        return Response::json($contact->update($data));
    }


	

PUT /contacts/1


REQUEST:
name=Benjamin+Rosell

RESPONSE:
HTTP/1.1 200 Ok
Content-Type: text/html

{
	"id": "1",
	"name": "Benjamin Rosell",
	"email": "benjamin.rosell@gmail.com",
	"telephone": "514-236-1889",
	"active": "true",
	"created_at": "2014-09-25 10:00:00",
	"updated_at": "2014-09-25 10:00:00"
}

	

Deleting a contact

	

    /**
     * Remove the specified resource from storage.
     *
     * @param  int  $id
     * @return Response
     */
    public function destroy($id)
    {
        Contact::destroy($id);

        return Response::make(null, 204);
    }

}

	

DELETE /contacts/1


HTTP/1.1 204 No content


	

What about errors ?

Use the right codes for the right situation


200: The request was successful.
201: The resource was successfully created.
204: The request was successful, but we did not send any content back.
400: The request failed due to an application error, such as a validation error.
401: An API key was either not sent or invalid.
403: The resource does not belong to the authenticated user and is forbidden.
404: The resource was not found.
500: A server error occurred.

	

Now we need to respond with JSON

	

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

	

Some custom exception

	

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

class ErrorMessageException extends RuntimeException {}
class NotFoundException extends RuntimeException {}
class PermissionException extends RuntimeException {}

	

GET /contacts/yehaa


HTTP/1.1 404 Not found
Content-Type: application/json

{
	"error": "The requested resource was not found";
}


	

Authentication

Let's stick to HTTP authentication

First we neet to create an API key...

	

		/**
	 * Generate a random, unique API key.
	 *
	 * @return string
	 */
	public static function createApiKey()
	{
		return Str::random(32);
	}

	
	

User::creating(function($user)
{
	$user->api_key = User::createApiKey();
});

	

Now, we create a filter

	

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


	

Rate Limiting

Laravel Magic at it's purest

	

Route::filter('api.limit', function()
{
	$key = sprintf('api:%s', Auth::user()->api_key);

	// Create the key if it doesn't exist
	Cache::add($key, 0, 60);

	// Increment by 1
	$count = Cache::increment($key);

	// Fail if hourly requests exceeded
	if ($count > Config::get('api.requests_per_hour'))
	{
		App::abort(403, 'Hourly request limit exceeded');
	}
});


	

Further Reading

That’s All Folks!