Slides from a presentation given at Laravel Chicago on November 18, 2014. Goes over the basics of building a REST API using the Laravel framework as well as some handy tips and tools.
2. A bit about me
• 3 years of experience in web development
• Now work for Packback
• Background mostly in WordPress, a little bit of
Rails, now use Laravel full-time
• About 1 year experience building APIs
3. What we’re going to talk
about tonight
• “Typical” Laravel application / MVC
• What is an API, and why should you care?
• Shitty way to structure an API
• Better way to structure an API
• Demonstration of interacting with API
4. Our Application
• Packback
• Digital textbook rentals for
college students
• Resources: Users, Books
• Users need to be able to “rent”
books
• https://github.com/samanthamic
hele7/packback-rest-api-101
• Two branches: master and
api_with_fractal
5. Anatomy of the“Typical”
Laravel Application
• Model (Eloquent) + database
• Controller + routes
• View (Blade)
6. Problems!
• What happens if we want to build an iOS/Android
application with our data?
• What happens if we want to use AngularJS or
EmberJS?
• What happens when we want to rebuild the front-end
in a few years?
• What happens if we want to let other companies work
with our data? (Twitter API, Facebook API, etc.)
8. What the hell does that even mean?
• APIs only care about data - not what things look like
• Data in, data out
• JSON is what most of the cool kids are using
• Keeps data types intact
• You can also use XML if you like typing a lot
• Turns everything into a string
9. <book>
<id>1</id>
<title>The Lord of the Rings</title>
<author>J. R. R. Tolkien</author>
</book>
XML Example
{
"book": {
"id" : 1,
"title": "The Lord of the Rings",
"author": "J. R. R. Tolkien"
}
}
JSON Example
10. Laravel makes it really easy
• The client can access routes (which are basically just URLs)
• Controllers handle logic (or call other classes to handle it for
them)
• Get data from or store data in database (via models)
• ?????
• PROFIT!!!
• Return data as JSON
11. Stuff that we need to do
• /createUser
• /fetchUser
• /setUserPassword
• /updatePaymentInfo
• /addBook
• /getBook
• /deleteBook
• /addBooktoUser
• /removeBook
13. REST to the Rescue!
• Representational State Transfer
• Hopefully not what you’re doing if I’m boring you
14. It does CRUD
• C - Create (POST)
• R - Read (GET)
• U - Update (PATCH/PUT)
• D - Destroy (DELETE)
15. Stuff that we need to do
(the RESTful way)
• POST /users - Create a new user
• GET /users - Get all users
• GET /users/{id} - Get one user by ID
• PATCH /users/{id} - Update a user by ID
• DELETE /users/{id} - Delete a user by ID
• POST /users/{id}/books/{id} - Add a book (or books) to a user
• GET /users/{id}/books/ - Get a list of books for a specific user
• etc.
17. Step 1: Create Databases
• Run database migrations (the same way as in a regular
Laravel application)
• Books, users, pivot
php artisan migrate
18. app/database/migrations/2014_11_18_024437_create_users_table.<?php
use IlluminateDatabaseSchemaBlueprint;
use IlluminateDatabaseMigrationsMigration;
class CreateUsersTable extends Migration {
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('users', function(Blueprint $table) {
$table->increments('id');
$table->string('email');
$table->string('name');
$table->string('password');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::drop('users');
}
}
19. app/database/migrations/2014_11_18_024939_create_books_table.<?php
use IlluminateDatabaseSchemaBlueprint;
use IlluminateDatabaseMigrationsMigration;
class CreateBooksTable extends Migration {
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('books', function(Blueprint $table) {
$table->increments('id');
$table->string('isbn13');
$table->string('title');
$table->string('author');
$table->float('price');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::drop('books');
}
}
20. database/migrations/2014_11_18_025038_create_books_users_<?php
use IlluminateDatabaseSchemaBlueprint;
use IlluminateDatabaseMigrationsMigration;
class CreateBooksUsersTable extends Migration {
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('books_users', function(Blueprint $table) {
$table->increments('id');
$table->integer('user_id')->unsigned();
$table->foreign('user_id')->references('id')->on('users');
$table->integer('book_id')->unsigned();
$table->foreign('book_id')->references('id')->on('books');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('books_users', function(Blueprint $table) {
$table->dropForeign('books_users_user_id_foreign');
$table->dropForeign('books_users_book_id_foreign');
});
Schema::drop('books_users');
}
21. Step 2: Seed data
• Your life will be much easier if you fill your database with fake
data
• Faker is easy to use and has realistic fake data:
https://github.com/fzaninotto/Faker
• I generally do one seed file per table
• Hook in database/seeds/DatabaseSeeder.php
• Make sure you truncate every time the seeder is run or you will
end up with a ton of data
php artisan db:seed
22. Seed Users
app/database/seeds/UserTableSeeder.php
<?php
use CarbonCarbon;
use FakerFactory as Faker;
class UserTableSeeder extends Seeder
{
public function run()
{
$faker = Faker::create();
DB::table('users')->truncate();
for ($i = 0; $i < 50; $i++) {
DB::table('users')->insert([
'email' => $faker->email,
'name' => $faker->name,
'password' => Hash::make($faker->word),
'created_at' => Carbon::now(),
'updated_at' => Carbon::now()
]);
}
}
}
23. Seed Books
app/database/seeds/BookTableSeeder.php
<?php
use CarbonCarbon;
use FakerFactory as Faker;
class BookTableSeeder extends Seeder
{
public function run()
{
$faker = Faker::create();
DB::table('books')->truncate();
for ($i = 0; $i < 50; $i++) {
DB::table('books')->insert([
'isbn13' => $faker->ean13(),
'title' => $faker->sentence,
'author' => $faker->name,
'price' => $faker->randomNumber(2) . '.' . $faker->randomNumber(2),
'created_at' => Carbon::now(),
'updated_at' => Carbon::now(),
]);
}
}
}
25. Step 3: Models
• Very little difference compared to a more traditional
Laravel app
• Define a ManyToMany relationship between users and
books
26. app/models/User.php
class User extends Eloquent implements UserInterface, RemindableInterface {
use UserTrait, RemindableTrait;
protected $table = 'users';
protected $fillable = ['email', 'name', 'password'];
protected $hidden = array('password', 'remember_token');
public function books()
{
return $this->belongsToMany('Book', 'books_users');
}
public function setPasswordAttribute($password)
{
$this->attributes['password'] = Hash::make($password);
}
}
27. app/models/Book.php
class Book extends Eloquent {
protected $table = 'books';
protected $fillable = ['isbn13', 'title', 'author', 'price'];
public function users()
{
return $this->belongsToMany('User', 'books_users');
}
}
28. Step 4: Routes
• Should you use Laravel magic? (Route::resource() or
Route::controller())
• Pros: Less code
• Cons: Less code
• It is generally clearer (to me) to explicitly define your
routes (so you have a blueprint)
• However, some people would disagree, so we’ll look
at both
29. RESTful routes
Option 1 (Less code)
Route::group(['prefix' => 'api'], function() {
Route::resource('users', 'UserController');
Route::resource('books', 'BookController');
});
This will automatically look for create, edit,
index, show, store, update, and destroy
methods in your controller.
31. Let’s talk about status codes
• Your API needs to send back a HTTP status code
so that the client knows if the succeeded or failed
(and if it failed, why)
• 2xx - GREAT SUCCESS
• 3xx - Redirect somewhere else
• 4xx - Client errors
• 5xx - Service errors
32. Some common status codes
• 200 - generic OK
• 201 - Created OK
• 301 - Moved permanently and redirect to new location
• 400 - Generic bad request (often used for validation on models)
• 401 - Unauthorized (please sign in)
• 403 - Unauthorized (you are signed in but shouldn’t be accessing this)
• 404 - Does not exist
• 500 - API dun goofed
• 503 - API is not available for some reason
• Plus lots more!
33. Step 5: Controllers
• You will need (at least) 5 methods: index (get all), show (get
one), store, update, destroy
• What about create() and edit() (if you use
Route::resource())?
• You don’t need them if you’re building a pure data-driven
API!
• Use Postman (http://www.getpostman.com/) to interact
with your API instead of Blade templates
34. Controllers / Index
/**
* Get all books
*
* @return Response
*/
public function index()
{
$books = Book::all();
return Response::json([
'data' => $books
]);
}
/**
* Get all users
*
* @return Response
*/
public function index()
{
$users = User::all();
return Response::json([
'data' => $users
]);
}
35. Controllers / Show
/**
* Get a single user
*
* @param $user_id
* @return Response
*/
public function show($user_id)
{
$user = User::findOrFail($user_id);
return Response::json([
'data' => $user
]);
}
/**
* Get a single book
*
* @param $book_id
* @return Response
*/
public function show($book_id)
{
$book = Book::findOrFail($book_id);
return Response::json([
'data' => $book
]);
}
36. Controllers / Store
/**
* Store a book
*
* @return Response
*/
public function store()
{
$input = Input::only('isbn13', 'title', 'author',
'price');
$book = Book::create($input);
return Response::json([
'data' => $book
]);
}
/**
* Store a user
*
* @return Response
*/
public function store()
{
$input = Input::only('email', 'name',
'password');
$user = User::create($input);
return Response::json([
'data' => $user
]);
}
37. Controllers / Update
/**
* Update a book
*
* @param $book_id
* @return Response
*/
public function update($book_id)
{
$input = Input::only('isbn13', 'title',
'author', 'price');
$book = Book::find($book_id);
$book->update($input);
return Response::json([
'data' => $book
]);
}
/**
* Update a user
*
* @param $user_id
* @return Response
*/
public function update($user_id)
{
$input = Input::only('email', 'name',
'password');
$user = User::findOrFail($user_id);
$user->update($input);
return Response::json([
'data' => $user
]);
}
38. Controllers / Destroy
/**
* Delete a book
*
* @param $book_id
* @return Response
*/
public function destroy($book_id)
{
$book = User::findOrFail($book_id);
$book->users()->sync([]);
$book->delete();
return Response::json([
'success' => true
]);
}
/**
* Delete a user
*
* @param $user_id
* @return Response
*/
public function destroy($user_id)
{
$user = User::findOrFail($user_id);
$user->books()->sync([]);
$user->delete();
return Response::json([
'success' => true
]);
}
40. Let’s Review!
• Request is sent through client (we used Postman, but could
be AngularJS app, iPhone app, etc.)
• Route interprets where it needs to go, sends it to
appropriate controller + method
• Controller takes the input and figures out what to do with it
• Model (Eloquent) interacts with the database
• Controller returns the data as JSON
• Look, ma, no views!
41. A few problems…
• We’re relying on the Laravel “hidden” attribute to avoid
showing sensitive information but otherwise have no
control over what is actually output. This is dangerous.
• What happens if our database schema changes?
• For example, we need to add a daily vs semester
rental price and rename the “price” database column
• How can we easily show a user + associated books with
one API call?
43. Transformers
(Not like the robots)
• “Transform” data per resource so that you have a lot more
control over what you’re returning and its data type
• Easy to build your own, or you can use Fractal for more
advanced features: http://fractal.thephpleague.com/
• Serialize, or structure, your transformed data in a more
specific way
• Uses items (one object) and collections (group of objects)
• Easily embed related resources within each other
44. Book Transformer
app/Packback/Transformers/BookTransformer.php
/**
* Turn book object into generic array
*
* @param Book $book
* @return array
*/
public function transform(Book $book)
{
return [
'id' => (int) $book->id,
'isbn13' => $book->isbn13,
'title' => $book->title,
'author' => $book->author,
// If we needed to rename the 'price' field to 'msrp'
'msrp' => '$' . money_format('%i', $book->price)
];
}
45. User Transformer
app/Packback/Transformers/UserTransformer.php
/**
* Turn user object into generic array
*
* @param User $user
* @return array
*/
public function transform(User $user)
{
return [
'id' => (int) $user->id,
'name' => $user->name,
'email' => $user->email
];
}
46. /**
* List of resources possible to include
*
* @var array
*/
protected $availableIncludes = [
'books'
];
/**
* Include books in user
*
* @param User $user
* @return LeagueFractalItemResource
*/
public function includeBooks(User $user)
{
$books = $user->books;
return $this->collection($books, new BookTransformer);
}
47. API Controller
Extend the ApiController in
UserController and BookController
/**
* Wrapper for Laravel's Response::json() method
*
* @param array $array
* @param array $headers
* @return mixed
*/
protected function respondWithArray(array $array, array $headers = [])
{
return Response::json($array, $this->statusCode, $headers);
}
class UserController extends ApiController
class BookController extends ApiController
app/controllers/ApiController.php
49. Controller with Fractal
public function index()
{
$books = Book::all();
return $this->respondWithCollection($books, new BookTransformer);
}
public function show($book_id)
{
$book = Book::findOrFail($book_id);
return $this->respondWithItem($book, new BookTransformer);
}
51. A Disclaimer
• This app is an over-simplified example
• Feel free to ignore everything I’ve told you tonight
• Different conventions/opinions
• Strict REST doesn’t make sense for every scenario
• BUT the more you scale, the harder it will be to keep
your code organized
52. REST APIs with Laravel 102
AKA
Things we didn’t have time to cover tonight
• Testing :(
• Pagination - return lots of records, a little bit at a time
• Validation
• Better error handling
• Authentication
• OOP best practices + design patterns