Following on from Designing a RESTful API, this post will be a guide on building the API, itself.
The application will be used as benchmark for future posts, most of my decisions are simply because this setup is in my comfort zone.
The main tools I’ll be using are,
- Vagrant manages the virtual machine
- Scotchbox is a one-size-fits all LAMP stack image for Vagrant
- Laravel is a framework that is great at building RESTful APIs
- Composer is a package manager for PHP projects
Setting up the Virtual Machine
First we will clone Scotchbox Pro. Scotchbox Pro is a one-size-fits-all image which I like to use when I want to get straight into the code.
The non-pro does not have PHP 7.2, but alternatives are easy to find.
Run these commands to download the Vagrantfile from github,
git clone https://github.com/scotch-io/scotch-box-pro ctt-helloworld
cd ctt-helloworld
Then spin the virtual machine up (The first time you do this may take a while to download the image).
vagrant up
vagrant ssh
After calling vagrant ssh, you will be in the shell of the guest machine. The guest machine’s “/var/www” directory now points to the directory where “VagrantFile” is.
All commands will be run from inside the guest’s /var/www directory.
This is a good practice to ensure there are no host-machine dependencies; it also will be easier to explain things.
Setting up the project
Laravel will be the framework we are using. You could argue that Lumen would be more efficient fit for what we are building, but Laravel will offer more flexibility later.
This command will bootstrap a Laravel 5.5 (Current LTS) project in a “tmp” directory, then move the contents of that directory back into our home. (It will not install to an empty directory but my IDE has already added files…)
composer create-project --no-install --prefer-dist laravel/laravel:5.5.* tmp
cp -r /var/www/tmp/{.,} /var/www
rm -rf tmp
Extended Generators will generate migration scripts from the command-line.
composer require laracasts/generators --dev
Eloquent Model Generator will generate models a bit better than Extended Generators
composer require krlove/eloquent-model-generator --dev
Let Laravel know about the package providers (but only locally), update App\Providers\AppServiceProvider::register()…
public function register()
{
// Service providers only required by development
if ($this->app->environment() == 'local') {
$this->app->register('Laracasts\Generators\GeneratorsServiceProvider');
$this->app->register('Krlove\EloquentModelGenerator\Provider\GeneratorServiceProvider');
}
}
To finishing setting up the project, copy .env.example file, and update the database password to match the database provided by Scotchbox,
cp .env.example .env
sed -i -e 's/DB_USERNAME=homestead/DB_USERNAME=root/g' .env
sed -i -e 's/DB_PASSWORD=secret/DB_PASSWORD=root/g' .env
sed -i -e 's/DB_DATABASE=homestead/DB_DATABASE=music/g' .env
Setting up the database
Referencing the MVP data-structure from Designing a RESTful API, these commands build the migration scripts and models using Laracast’s generators…
albums, artist, and tracks are three simple meta tables for the time-being, “HasTimestamps” trait is generated, but not “SoftDeletes”, so I define the deleted_at manually.
php artisan make:migration:schema create_albums \
--schema="title:string, deleted_at:date:nullable"
php artisan make:migration:schema create_artists \
--schema="title:string, deleted_at:date:nullable"
php artisan make:migration:schema create_tracks \
--schema="title:string, deleted_at:date:nullable"
And finally a couple of pivot tables to define the relationships
php artisan make:migration:pivot tracks artists
php artisan make:migration:pivot albums track
The migration script will not create a schema for us, so let’s do that first
mysql -uroot -p -Bse "create schema music;"
Running the migration script will now create our database
php artisan migrate
Completing the models
Discard the models created by Laracast’s Generators, the package fall shorts on a few things that the Eloquent Model Generator solves.
rm -rf app/*.php
The Eloquent Model Generator to builds models based on the database structure itself.
php artisan krlove:generate:model Artist \
--namespace=App\\Models \
--output-path=Models
php artisan krlove:generate:model Album \
--namespace=App\\Models \
--output-path=Models
php artisan krlove:generate:model Track \
--namespace=App\\Models \
--output-path=Models
Logic and serialisation
Firstly create the resources, these will be used to shape our responses.
php artisan make:resource Album
php artisan make:resource Track
php artisan make:resource Artist
php artisan make:resource Albums --collection
php artisan make:resource Tracks --collection
php artisan make:resource Artists --collection
Creating resources controllers will save some effort when routing, but the controllers themselves will have a lot of dead code until we expand on the resources.
php artisan make:controller AlbumController --resource --model=Models\\Album
php artisan make:controller ArtistController --resource --model=Models\\Artist
php artisan make:controller TrackController --resource --model=Models\\Track
Update all controllers with this “index”, replacing the Resources and Album classes respectively.
public function index()
{
return new AlbumResources(AlbumModel::all());
}
The album controller needs two extra methods to handle the extra routes. Notice how “Album” into both methods, this is because of the static route binding we are about to do.
public function show(AlbumModel $album)
{
return new AlbumResource($album);
}
public function artists(Album $album) {
return new ArtistsResource($album->artists);
}
Read API Resources and Retrieving models in the Laravel Documentation for more information.
Setting up routing and controllers
Routing will serve to direct HTTP requests to the controllers which we created in the previous section.
Begin by defining some static bindings for the routes. This means we don’t need to handle whether the objects are available further down the line (It will handle 404s etc). Add the following lines to App\Providers\RouteServiceProvider::boot()
Route::model('album', 'App\Models\Album');
Route::model('artist', 'App\Models\Artist');
Route::model('track', 'App\Models\Track');
Define the routes in routes/web.php. Most routes can be handled with Route::resource(), but the specification defines additional ‘helper routes’, which logically map to other resources with an additional filter.
Route::group([ 'prefix' => 'v1' ], function () {
// Album resources (our vertical prototype)
Route::resource('albums', 'AlbumController')->only(['index', 'show']);
Route::resource('albums/{album}/tracks', 'AlbumController::tracks');
// Other resources
Route::resource('artists', 'ArtistController')->only(['index']);
Route::resource('tracks', 'TrackController')->only(['index']);
});
Read HTTP Routing in the Laravel Documentation for more information.
Populating the database with dummy data
At this stage, the RESTful API is almost ready, but we don’t have any data to respond with.
I eventually plan in populating the dataset with real-data, but for now I’d be happy with a dummy dataset.
To do this, I use a factory to create “fake” data, then seeder to populate the fake data into the database.
To bootstrap our factories, run the following commands
php artisan make:factory AlbumFactory --model=Models\\Album
php artisan make:factory ArtistFactory --model=Models\\Artist
php artisan make:factory TrackFactory --model=Models\\Track
Then we need to manually update each factory in database/factories to shape our data. “Faker” is used to randomly generate values. Our data doesn’t have much shape yet, so each factory should simple define a title…
$factory->define(App\Models\Track::class, function (Faker $faker) {
return [
'title' => $faker->name
];
});
Then go to database/seeds/DatabaseSeeder.php and replace the run method with a quick and dirty seeder. I attempt to create as many realistic ‘shapes’ of data as possible, whilst keeping it consise…
public function run()
{
DB::statement('SET FOREIGN_KEY_CHECKS=0;');
// Clear existing data
DB::table('artist_track')->truncate();
DB::table('album_track')->truncate();
\App\Models\Album::truncate();
\App\Models\Artist::truncate();
\App\Models\Track::truncate();
// Create 50 artists
$poolOfArtists = factory(\App\Models\Artist::class, 50)->create();
// Create 100 albums, each 10 tracks, each with 1 random artist
factory(\App\Models\Album::class, 100)->create()->each(static function(\App\Models\Album $album) use ($poolOfArtists) {
$tracks = factory(\App\Models\Track::class, 10)->create()->each(function(\App\Models\Track $track) use ($poolOfArtists) {
$track->artists()->attach($poolOfArtists->random());
});
$album->tracks()->attach($tracks);
});
// 50 tracks will be given a second artist
\App\Models\Track::all()->random(50)->each(function(\App\Models\Track $track) use ($poolOfArtists) {
$track->artists()->attach(
$poolOfArtists->whereNotIn( 'id', $track->artists()->pluck('id') )->random()
);
});
DB::statement('SET FOREIGN_KEY_CHECKS=1;');
}
To run the seeder, run this command:
php artisan seed:run
Read Database Testing in the Laravel Documentation for more information.
Querying the API
The app now respects the OpenAPI specification that we designed in the first post. Let’s call this the MVP.
Take a look for yourslef, here are some curl requests to try…
curl -X GET http://192.168.33.10/v1/artists
curl -X GET http://192.168.33.10/v1/tracks
curl -X GET http://192.168.33.10/v1/albums
curl -X GET http://192.168.33.10/v1/albums/1
curl -X GET http://192.168.33.10/v1/albums/1/tracks
And some 404s for good luck
curl -X GET http://192.168.33.10/v1/albums/999
curl -X GET http://192.168.33.10/v1/albums/999/artists
curl -X GET http://192.168.33.10/v1/albums/999/tracks
This post was a lot bigger than I expected it to be. In the next post in the series I’ll look at testing the application using the OpenAPI schema created in the previous post.