Hello REST – Building the API

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.

One thought on “Hello REST – Building the API

Leave a comment