RESTful API's are hard! There are a lot of aspects to designing and writing a successful one. For instance, some of the topics that you may find yourself handling include authentication, hypermedia/HATEOS, versioning, rate limits, and content negotiation. Rather than tackling all of these concepts, however, let's instead focus on the basics of REST. We'll make some JSON endpoints behind a basic authentication system, and learn a few Laravel 4 tricks in the process.
If you need help with your Laravel development, try some of the useful Laravel scripts and plugins available on Envato Market.
The App
Let's build an API for a simple Read-It-Later app. Users will be able to create, read, update and delete URLs that they wish to read later.
Ready to dive in and get started?
Install Laravel 4
Create a new install of Laravel 4. If you're handy with CLI, try this quickstart guide. Otherwise, we have a video tutorial here on Nettuts+ that covers the process.
We're going to first create an encryption key for secure password hashing. You can do this easily by running this command from your project root:
$ php artisan key:generate
Alternatively, you can simple edit your app/config/app.php
encryption key:
/* |-------------------------------------------------------------------------- | Encryption Key |-------------------------------------------------------------------------- | | This key is used by the Illuminate encrypter service and should be set | to a random, long string, otherwise these encrypted values will not | be safe. Make sure to change it before deploying any application! | */ 'key' => md5('this is one way to get an encryption key set'),
Database
Once you have a working install of Laravel 4, we can get started with the fun. We'll begin by creating the app's database.
This will only require two database tables:
-
Users, including a username and password
-
URLs, including a url and description
We'll use Laravel's migrations to create and populate the database.
Configure Your Database
Edit app/config/database.php
and fill it with your database settings. Note: this means creating a database for this application to use. This article assumes a MySQL database.
'connections' => array( 'mysql' => array( 'driver' => 'mysql', 'host' => 'localhost', 'database' => 'read_it_later', 'username' => 'your_username', 'password' => 'your_password', 'charset' => 'utf8', 'collation' => 'utf8_unicode_ci', 'prefix' => '', ), ),
Create Migration Files
$ php artisan migrate:make create_users_table --table=users --create $ php artisan migrate:make create_urls_table --table=urls --create
These commands set up the basic migration scripts that we'll be using to create the database tables. Our job now is to fill them with the correct table columns.
Edit app/database/migrations/SOME_DATE_create_users_table.php
and add to the up()
method:
public function up() { Schema::create('users', function(Blueprint $table) { $table->increments('id'); $table->string('username')->unique(); $table->string('password'); $table->timestamps(); }); }
Above, we're setting a username (which should be unique), a password, as well as the timestamps. Save that, and now edit app/database/migrations/SOME_DATE_create_urls_table.php
, and add to the up()
method:
public function up() { Schema::create('urls', function(Blueprint $table) { $table->increments('id'); $table->integer('user_id'); $table->string('url'); $table->string('description'); $table->timestamps(); }); }
The only important note in this snippet is that we're creating a link between the url
and users
table, via the user_id
field.
Add Sample Users
We can use Laravel's seeds to create a few sample users.
Create a file within the app/database/seeds
folder that has the same name as the table that it corresponds to; in our case, UserTableSeeder.php
. Add:
<?php class UserTableSeeder extends Seeder { public function run() { DB::table('users')->delete(); User::create(array( 'username' => 'firstuser', 'password' => Hash::make('first_password') )); User::create(array( 'username' => 'seconduser', 'password' => Hash::make('second_password') )); } }
Next, make sure that seeder class gets run when the database is seeded. Edit app/database/seeds/DatabaseSeeder.php
:
public function run() { Eloquent::unguard(); // Add or Uncomment this line $this->call('UserTableSeeder'); }
Run the Migrations
Here's how to create those two tables, and insert our sample users.
// Create the two tables $ php artisan migrate // Create the sample users $ php artisan db:seed
Models
Laravel 4 continues to use the excellent Eloquent ORM. This will make the process of handling database calls a snap. We'll require one model per table.
Luckily, Laravel comes with a User model setup, so let's create a model for our urls table.
Create and edit file app/models/Url.php
.
<?php class Url extends Eloquent { protected $table = 'urls'; }
Authentication
Laravel's filters can handle authentication for us. In particular, Laravel now comes with a Basic Authentication filter, which we can use as a quick authentication model to be used with our API requests.
If you open app/filters.php
, you'll see what it looks like:
Route::filter('auth.basic', function() { return Auth::basic(); });
We just need to make one adjustment. By default, this filter looks for an "email" field to identify the user. Since we're using usernames instead of emails, we just need to adjust that preference. Change the Auth::basic()
call by giving it our username field as a parameter:
Route::filter('auth.basic', function() { return Auth::basic("username"); });
Routes
Let's test this out. Create a route, called testauth
, and make sure that our auth.basic
filter runs before it.
Edit app/routes.php
:
Route::get('/authtest', array('before' => 'auth.basic', function() { return View::make('hello'); }));
We can test this with a curl request. From your terminal, try pointing to your build of Laravel. In mine, it looks like this (Your URL will likely be different!):
$ curl -i localhost/l4api/public/index.php/authtest HTTP/1.1 401 Unauthorized Date: Tue, 21 May 2013 18:47:59 GMT WWW-Authenticate: Basic Vary: Accept-Encoding Content-Type: text/html; charset=UTF-8 Invalid credentials
As you can see, an unauthorized request is detected and a "Invalid Credentials" message is returned with a 401 status code. Next, try including basic authentication.
$ curl --user firstuser:first_password localhost/l4api/public/index.php/authtest HTTP/1.1 200 OK Date: Tue, 21 May 2013 18:50:51 GMT Vary: Accept-Encoding Content-Type: text/html; charset=UTF-8 <h1>Hello World!</h1>
It worked!
At this point, the baseline work of our API is done. We have:
- Installed Laravel 4
- Created our database
- Created our models
- Created an authentication model
Creating Functional Requests
You may be familiar with Laravel's RESTful controllers. They still exist in Laravel 4; however, we can also use Laravel's Resourceful Controllers, which set up some paradigms that we can use to make a consistent API interface. We'll be using a Resourceful controller.
Here's a breakdown of what each method in the resourceful controller will handle. Please note that you can remove the /resource/create and /resource/{id}/edit routes, since we won't be needing to show 'create' or 'edit' forms in an API.
Create a Resourceful Controller
$ php artisan controller:make UrlController
Next, setup a route to use the controller, and require each route to be authenticated.
Edit app/routes.php
and add:
// Route group for API versioning Route::group(array('prefix' => 'api/v1', 'before' => 'auth.basic'), function() { Route::resource('url', 'UrlController'); });
A few things are happening there.
- This is going to respond to requests made to
http://example.com/api/v1/url
. - This allows us to add extra routes, if we need to expand our API. For instance, if you add a user end-point, such as
/api/v1/user
.
- There is also a naming mechanism in place for versioning our API. This gives us the opportunity to roll out new API versions without breaking older versions - We can simply create a v2 route group, and point it to a new controller!
Note: You may want to consider more advanced API versioning techniques, such as using an Accept
header or subdomain which can help you point different API versions separate code bases.
Add the Functionality
Edit the new app/controllers/UrlController.php
file:
// Edit this: public function index() { return 'Hello, API'; }
Let's test it:
$ curl -i localhost/l4api/public/index.php/api/v1/url HTTP/1.1 401 Unauthorized Date: Tue, 21 May 2013 19:02:59 GMT WWW-Authenticate: Basic Vary: Accept-Encoding Content-Type: text/html; charset=UTF-8 Invalid credentials. $ curl --user firstuser:first_password localhost/l4api/public/index.php/api/v1/url HTTP/1.1 200 OK Date: Tue, 21 May 2013 19:04:19 GMT Vary: Accept-Encoding Content-Type: text/html; charset=UTF-8 Hello, API
We now have a resourceful controller with authentication working, and are ready to add functionality.
Create a URL
Edit app/controllers/UrlController.php
:
/** * Store a newly created resource in storage. * * @return Response */ public function store() { $url = new Url; $url->url = Request::get('url'); $url->description = Request::get('description'); $url->user_id = Auth::user()->id; // Validation and Filtering is sorely needed!! // Seriously, I'm a bad person for leaving that out. $url->save(); return Response::json(array( 'error' => false, 'urls' => $urls->toArray()), 200 ); }
It's time to test this with another curl request. This one will send a POST request, which will correspond to the store()
method created above.
$ curl -i --user firstuser:first_password -d 'url=http://google.com&description=A Search Engine' localhost/l4api/public/index.php/api/v1/url HTTP/1.1 201 Created Date: Tue, 21 May 2013 19:10:52 GMT Content-Type: application/json {"error":false,"message":"URL created"}
Cool! Let's create a few more, for both of our users.
$ curl --user firstuser:first_password -d 'url=http://fideloper.com&description=A Great Blog' localhost/l4api/public/index.php/api/v1/url $ curl --user seconduser:second_password -d 'url=http://digitalsurgeons.com&description=A Marketing Agency' localhost/l4api/public/index.php/api/v1/url $ curl --user seconduser:second_password -d 'url=http://www.poppstrong.com/&description=I feel for him' localhost/l4api/public/index.php/api/v1/url
Next, let's create methods for retrieving URLs.
/** * Display a listing of the resource. * * @return Response */ public function index() { //Formerly: return 'Hello, API'; $urls = Url::where('user_id', Auth::user()->id)->get(); return Response::json(array( 'error' => false, 'urls' => $urls->toArray()), 200 ); } /** * Display the specified resource. * * @param int $id * @return Response */ public function show($id) { // Make sure current user owns the requested resource $url = Url::where('user_id', Auth::user()->id) ->where('id', $id) ->take(1) ->get(); return Response::json(array( 'error' => false, 'urls' => $url->toArray()), 200 ); }
Let's test them out:
$ curl --user firstuser:first_password localhost/l4api/public/index.php/api/v1/url { "error": false, "urls": [ { "created_at": "2013-02-01 02:39:10", "description": "A Search Engine", "id": "2", "updated_at": "2013-02-01 02:39:10", "url": "http://google.com", "user_id": "1" }, { "created_at": "2013-02-01 02:44:34", "description": "A Great Blog", "id": "3", "updated_at": "2013-02-01 02:44:34", "url": "http://fideloper.com", "user_id": "1" } ] } $ curl --user firstuser:first_password localhost/l4api/public/index.php/api/v1/url/1 { "error": false, "urls": [ { "created_at": "2013-02-01 02:39:10", "description": "A Search Engine", "id": "2", "updated_at": "2013-02-01 02:39:10", "url": "http://google.com", "user_id": "1" } ] }
Almost done. Let's now allow users to delete a url.
/** * Remove the specified resource from storage. * * @param int $id * @return Response */ public function destroy($id) { $url = Url::where('user_id', Auth::user()->id)->find($id); $url->delete(); return Response::json(array( 'error' => false, 'message' => 'url deleted'), 200 ); }
Now, we can delete a URL by using a DELETE request:
$ curl -i -X DELETE --user firstuser:first_password localhost/l4api/public/index.php/api/v1/url/1 HTTP/1.1 200 OK Date: Tue, 21 May 2013 19:24:19 GMT Content-Type: application/json {"error":false,"message":"url deleted"}
Lastly, let's allow users to update a url.
/** * Update the specified resource in storage. * * @param int $id * @return Response */ public function update($id) { $url = Url::where('user_id', Auth::user()->id)->find($id); if ( Request::get('url') ) { $url->url = Request::get('url'); } if ( Request::get('description') ) { $url->description = Request::get('description'); } $url->save(); return Response::json(array( 'error' => false, 'message' => 'url updated'), 200 ); }
To test URL updates, run:
$ curl -i -X PUT --user seconduser:second_password -d 'url=http://yahoo.com' localhost/l4api/public/index.php/api/v1/url/4 HTTP/1.1 200 OK Date: Tue, 21 May 2013 19:34:21 GMT Content-Type: application/json {"error":false,"message":"url updated"} // View our changes $ curl --user seconduser:second_password localhost/l4api/public/index.php/api/v1/url/4 { "error": false, "urls": [ { "created_at": "2013-02-01 02:44:34", "description": "I feel for him", "id": "3", "updated_at": "2013-02-02 18:44:18", "url": "http://yahoo.com", "user_id": "1" } ] }
And That's It
We now have the beginnings of a fully-functioning API. I hope that you've learned a lot about how to get an API underway with Laravel 4.
To recap, we achieved the following in this lesson:
- Install Laravel
- Create the database, using migrations and seeding
- Use Eloquent ORM models
- Authenticate with Basic Auth
- Set up Routes, including versioning the API
- Create the API functionality using Resourceful Controllers
The Next Steps
If you'd like to push your API up a notch, you might consider any of the following as a next step.
- Validation (Hint: Laravel has a Validation library).
- API-request error handling – It's still possible to receive HTML response on API requests (Hint: Laravel Error Handling, plus Content Negotiation.)
- Content Negotiation - listening for the Accept header. (Hint: Laravel's Request Class will give you the request headers).
- Check out the API Craft Google Group.
- Learn about the different types caching and how Validation Caching can improve your API.
- Unit test your code.
- Check out Apigee's great API resources.
- Try some of the useful Laravel scripts and plugins available on Envato Market.
Comments