Build a modern web application with Laravel and Vue Part 2: Creating our endpoints with REST in mind

web-application-laravel-vue-part-2-header.png

This is part two of a five part series, taking you through building a modern web app with Laravel and Vue. In part two, design your REST API.

Introduction

In the last tutorial, we looked at how to successfully set up a development environment preparing us for PHP Development. Through the rest of the series, we will be building a simple “Trello Clone”.

In this tutorial, we will take a closer look at how setting up out application “RESTully” will play a role in us building modern apps using Vue and Laravel.

At the end of the series, what we’ll have will look like this:

App Demo

Prerequisites

This part of the series requires that you have:

  • Completed the first part of the series and have setup your development environment properly.

Creating the application

To get started, we need to create our application. Since we have already set up our development environment in part one, all we need to is run the following command to create a new Laravel application:

1$ laravel new trello-clone

To give your application a test run, cd into the trello-clone directory and then run the command:

1$ php artisan serve

This runs your Laravel application on 127.0.0.1:8000. To kill the server press ctrl+c on your machine. If you have Laravel valet installed and configured on your machine then you can cd to the trello-clone directory, run the command below:

1$ valet link trello-clone

Then head over to http://trello-clone.test. You should see the Laravel welcome page as seen below:

laravel homepage

Building the models

For our simple trello clone, we are going to have the following models:

  • User.
  • Task.
  • Category.

To build a model, run the following below. Since we already have the User model, we need models for the Task and Category resources. You can go ahead and create the models now.

1$ php artisan make:model ModelName -mr

? The -mr flag creates an accompanying migration file and resource controller for the model.

The User model

Laravel comes bundled with a default User model so you do not need to create one. The User model will have the following fields:

  • id – the unique auto-incrementing ID of the user.
  • name – the name of the user.
  • email – the email of the user.
  • password – used in authentication.

Open the User model which is in the app directory and update it as below:

1<?php
2
3    namespace App;
4
5    use Illuminate\Notifications\Notifiable;
6    use Illuminate\Foundation\Auth\User as Authenticatable;
7    use Illuminate\Database\Eloquent\SoftDeletes;
8
9    class User extends Authenticatable
10    {
11        use SoftDeletes, Notifiable;
12
13        protected $fillable = ['name', 'email', 'password'];
14
15        protected $hidden = [
16            'password', 'remember_token',
17        ];
18
19        public function tasks(){
20            return $this->hasMany(Task::class);
21        }
22    }

? A fillable column in Laravel is a database column that is mass assignable. This means you can just pass an array to the create function of the model with the data you want to get assigned.

? SoftDeletes is a way of deleting resources without actually deleting the data from the database. What happens is that when the table is created, there will be a field called ‘deleted_at’ and when a user tries to delete a task, the ‘deleted_at’ field will be populated with the current date time. And so when fetches are made for resources, the ‘deleted’ resource will not be part of the response

Task model

The task model will have the following fields:

  • id – a unique identifier for the task.
  • name – the name of the task.
  • category_id – ID of the category the task belongs to.
  • user_id – ID of the user the task belongs to.
  • order – the order of the task in its respective category.

Create a Task model using the artisan command. Then open it from the app directory and replace the contents with the following:

1<?php
2
3    namespace App;
4
5    use Illuminate\Database\Eloquent\Model;
6    use Illuminate\Database\Eloquent\SoftDeletes;
7
8    class Task extends Model
9    {
10        use SoftDeletes;
11
12        protected $fillable = ['name', 'category_id', 'user_id', 'order'];
13
14        public function category() {
15            return $this->hasOne(Category::class);
16        }
17
18        public function user() {
19            return $this->belongsTo(User::class);
20        }
21    }

Category model

The category model will have the following fields:

  • id – this will uniquely identify every category.
  • name – represents the name of the category.

Create a Category model using the artisan command. Then open it from the app directory and replace the contents with the following:

1<?php
2
3    namespace App;
4
5    use Illuminate\Database\Eloquent\Model;
6    use Illuminate\Database\Eloquent\SoftDeletes;
7
8    class Category extends Model
9    {
10        use SoftDeletes;
11
12        protected $fillable = ['name'];
13
14        public function tasks() {
15            return $this->hasMany(Task::class);
16        }
17    }

Here, the tasks() function is to help define relationships between the Category model and the Task model as a one-to-many relationship. What this means is one category has many tasks.

Writing our migrations

For this application to work, we need to create the database. To keep track of changes going on in our database, we make use of migrations, which is an inbuilt feature of Laravel.

As part of the prerequisites mentioned in the first part of this series, you need SQLite installed on your machine. To create an SQLite database that Laravel can connect to create an empty new file in the database directory called database.sqlite.

Next, open your .env file in the root of your project and replace the following lines:

1DB_CONNECTION=mysql
2    DB_DATABASE=homestead
3    DB_USERNAME=username
4    DB_PASSWORD=password

with

1DB_CONNECTION=sqlite
2    DB_DATABASE=/full/path/to/database/database.sqlite

That is all for our database setup. However, while you’re in the .env file, change the APP_URL value from http://localhost to http://127.0.0.1:8000 as this will be the application URL.

If you wanted to create migrations, the artisan command is:

1$ php artisan make:migration create_tablename_table

You can name your migration file whatever you like, but it is always good to name it like verb_tablename_table as shown above. The file will be created in the database/migrations directory.

However since we have already used the -mr flag earlier while creating our models, the migrations should have been created for us already.

⚠️ Migrations are runtime-based. So you need to consider this when making migrations for tables that are dependent on each other.

Updating our user migration

Open the create users migration file in the database/migrations directory and replace the content with the code below:

1<?php
2
3    use Illuminate\Support\Facades\Schema;
4    use Illuminate\Database\Schema\Blueprint;
5    use Illuminate\Database\Migrations\Migration;
6
7    class CreateUsersTable extends Migration
8    {
9        public function up()
10        {
11            Schema::create('users', function (Blueprint $table) {
12                $table->increments('id');
13                $table->string('name');
14                $table->string('email')->unique();
15                $table->string('password');
16                $table->rememberToken();
17                $table->timestamps();
18                $table->softDeletes();
19            });
20        }
21
22        public function down()
23        {
24            Schema::dropIfExists('users');
25        }
26    }

Updating our category migration

Since we had created the category migration earlier, open the file and replace the content with the code below:

1<?php
2
3    use Illuminate\Support\Facades\Schema;
4    use Illuminate\Database\Schema\Blueprint;
5    use Illuminate\Database\Migrations\Migration;
6
7    class CreateCategoriesTable extends Migration
8    {
9        public function up()
10        {
11            Schema::create('categories', function (Blueprint $table) {
12                $table->increments('id');
13                $table->string('name');
14                $table->timestamps();
15                $table->softDeletes();
16            });
17        }
18
19        public function down()
20        {
21            Schema::dropIfExists('categories');
22        }
23    }

Creating our task migration

Since we created the task migration file earlier, open the file and replace the content with the code below:

1<?php
2
3    use Illuminate\Support\Facades\Schema;
4    use Illuminate\Database\Schema\Blueprint;
5    use Illuminate\Database\Migrations\Migration;
6
7    class CreateTasksTable extends Migration
8    {
9        public function up()
10        {
11            Schema::create('tasks', function (Blueprint $table) {
12                $table->increments('id');
13                $table->string('name');
14                $table->unsignedInteger('category_id');
15                $table->unsignedInteger('user_id');
16                $table->integer('order');
17                $table->timestamps();
18                $table->softDeletes();
19
20                $table->foreign('user_id')->references('id')->on('users');
21                $table->foreign('category_id')->references('id')->on('categories');
22            });
23        }
24
25        public function down()
26        {
27            Schema::dropIfExists('tasks');
28        }
29    }

Now that we have our migration files, let’s run the artisan command to execute the migrations and write to the database:

1$ php artisan migrate

? Migrations are like version control for your database. It allows you to create, modify or tear down your database as your application evolves, without having to manually write SQL queries (or whatever queries your database of choice uses). It also makes it easy for you and your team to easily modify and share the application’s database schema. Learn more.

Database seeders

Now that we have created our database migrations, let’s see how to put in dummy data for when we are testing our applications. In Laravel, we have something called seeders.

? Seeders allow you automatically insert dummy data into your database.

This is the command to make a seeder:

1$ php artisan make:seeder TableNameSeeder

Creating our users table seeder

To create our database seeder type the following command:

1$ php artisan make:seeder UsersTableSeeder

This creates a UsersTableSeeder.php file in the database/seeds directory. Open the file and replace the contents with the following code:

1<?php
2
3    use App\User;
4    use Illuminate\Database\Seeder;
5
6    class UsersTableSeeder extends Seeder
7    {
8        public function run()
9        {
10            User::create([
11                'name' => 'John Doe',
12                'email' => 'demo@demo.com',
13                'password' => bcrypt('secret'),
14            ]);
15        }
16    }

The run function contains the database query we want to be executed when the seeders are run.

? You can use model factories to create better-seeded data.

? We use the bcrypt to hash the password before storing it because this is the default hashing algorithm Laravel uses to hash passwords.

Creating our categories table seeder

To create our database seeder type the following command:

1$ php artisan make:seeder CategoriesTableSeeder

This creates a CategoriesTableSeeder.php file in the database/seeds directory. Open the file and replace the contents with the following code:

1<?php
2
3    use App\Category;
4    use Illuminate\Database\Seeder;
5
6    class CategoriesTableSeeder extends Seeder
7    {
8        public function run()
9        {
10            $categories = ['Ideas', 'On Going', 'Completed'];
11
12            foreach ($categories as $category) {
13                Category::create(['name' => $category]);
14            }
15        }
16    }

Running our database seeders

To run the database seeders, open the database/DatabaseSeeder.php file and replace the run method with the code below:

1public function run()
2    {
3        $this->call([
4            UsersTableSeeder::class,
5            CategoriesTableSeeder::class,
6        ]);
7    }

Next, run the command below on your terminal:

1$ php artisan db:seed

This should update the databases with data. If at some point you want to refresh and seed your data again, run the command below:

1$ php artisan migrate:fresh --seed

This will delete the database tables, add them back and run the seeder.

REST in a nutshell

In technical terms, REST stands for REpresentational State Transfer (elsewhere, it just means “to relax”). In order for you get a good grasp of this article, there are a couple of terms that need to be broken down into bite-sized nuggets.

Clients, statelessness, resources – what’s the relationship?

Clients are the devices that interact with your application. For any given application, the number of clients that interact with it can range from one to billions. When you go to a website (e.g. https://pusher.com) your client sends a request to the server. The server then processes your request and then sends a response back to your client for interaction.

Statelessness in the simplest of terms means building your application in such a way that the client has all it needs to complete every request. When the client makes subsequent requests, the server won’t store or retrieve any data relating to the client. When your application starts having more active concurrent users, it will be an unnecessary burden on your server managing states for the client. Being stateless also simplifies your application design so unless you have specific reasons not to be, then why not?

Resources are a representation of real-life instances in your code. Take for example you are building an application that allows students to check their grades, a good example of resources in such an application will be your students, courses etc. These resources are linked with the data that will be stored in the database.

Now when we are building RESTful applications, our server gives the client access to the resources. The client is then able to make requests to fetch, change, or delete resources. Resources are usually represented in JSON or XML formats but there are many more formats and it’s up to you during your implementation to decide the format.

Creating your first few REST endpoints

Before we start creating the endpoints, make sure you get familiar with best practices for naming REST resources.

We currently have the following HTTP Verbs which we are going to apply:

  • GET – this is usually used to fetch a resource
  • POST – this is used to create a new resource
  • PUT/PATCH – used to replace/update an existing resource
  • DELETE – used to delete a resource

Here is a tabular representation of how our REST endpoints for our tasks resource will look:

METHODROUTEFUNCTION
POST/api/taskCreates a new task
GET/api/taskFetches all tasks
GET/api/task/{task_id}Fetches a specific task
PUTPATCH/api/task/{task_id} | Update a specific task
DELETE/api/task/{task_id}Delete a specific task

Let’s start creating the routes in our application. Open the routes/api.php file and updated:

1<?php
2
3    Route::resource('/task', 'TaskController');
4    Route::get('/category/{category}/tasks', 'CategoryController@tasks');
5    Route::resource('/category', 'CategoryController');

Above, we defined our routes. We have two route resources that register all the other routes for us without having to create them manually. Read about resource controllers and routes here.

Formatting responses and handling API errors

Earlier in the article, we spoke about making requests from the client. Now let’s look at how to create and format our responses when a request has been handled.

Creating our controllers

Now that we have our routes, we need to add some controller logic that will handle all our requests. To create a controller, you need to run the following command on the terminal:

1$ php artisan make:controller NameController

However since we have created our requests when we used the -mr earlier, let’s edit them.

Open the controller file TaskController.php in the app/Http/Controller/ directory. In there, we will define a few basic methods that’ll handle the routes we created above.

In the file update the store method as seen below:

1public function store(Request $request)
2    {
3        $task = Task::create([
4            'name' => $request->name,
5            'category_id' => $request->category_id,
6            'user_id' => $request->user_id,
7            'order' => $request->order
8        ]);
9
10        $data = [
11            'data' => $task,
12            'status' => (bool) $task,
13            'message' => $task ? 'Task Created!' : 'Error Creating Task',
14        ];
15
16        return response()->json($data);
17    }

? On line 16 we can see that the response is set to be in the JSON format. You can specify what response format you want the data to be returned in, but we will be using JSON.

⚠️ We are not focusing on creating the meat of the application just yet. We are explaining how you can create RESTful endpoints. In later parts, we will create the controllers fully.

Securing our endpoints with Passport

Now that we have our routes, we have to secure them. As they are now, anyone can access them without having to verify that they should be able to.

Laravel, by default, has support for web and api routes. Web routes handle routing for dynamically generated pages accessed from a web browser, while, API routes handle requests from clients that require a response in either JSON or XML format.

Authentication and authorization (JWT) to secure the APIs

In the first part of this series, we talked about API authentication using Laravel Passport. If you read this guide, you would already have the idea of how to make this work. For that reason, we would go over a lot of things fairly quickly in this section.

First, install Laravel Passport:

1$ composer require laravel/passport

Laravel Passport comes with the migration files for the database table it needs to work, so you just need to run them:

1$ php artisan migrate

Next, you should run the passport installation command so it can create the necessary keys for securing your application:

1php artisan passport:install

The command will create encryption keys needed to generate secure access tokens plus “personal access” and “password grant” clients which will be used to generate access tokens.

After the installation, you need to use the Laravel Passport HasApiToken trait in the User model. This trait will provide a few helper methods to your model which allow you to inspect the authenticated user’s token and scopes.

File: app/User.php

1<?php
2
3    [...]
4
5    use Laravel\Passport\HasApiTokens;
6
7    class User extends Authenticatable
8    {
9        use HasApiTokens, SoftDeletes, Notifiable;
10
11        [...]
12    }

Next, call the Passport::routes method within the boot method of your AuthServiceProvider. This method will register the routes necessary to issue the tokens your app will need:

File: app/Providers/AuthServiceProvider.php

1<?php
2
3    [...]
4
5    use Laravel\Passport\Passport;
6
7    class AuthServiceProvider extends ServiceProvider
8    {
9        [...]
10
11        public function boot()
12        {
13            $this->registerPolicies();
14
15            Passport::routes();
16        }
17
18        [...]
19    }

Finally, in your config/auth.php configuration file, you should set the driver option of the api authentication guard to passport.

File: config/auth.php

1[...]
2
3    'guards' => [
4        [...]
5
6        'api' => [
7            'driver' => 'passport',
8            'provider' => 'users',
9        ],
10    ],
11
12    [...]

Log in and register using the API

Now that you have set up the API authentication for this application using Laravel Passport, we will need to make the login and registration endpoints.

Add the following routes in routes/api.php file:

1Route::post('login', 'UserController@login');
2    Route::post('register', 'UserController@register');

You also need to create the UserController to handle authentication requests for the API. Create a new file UserController.php in app/Http/Controllers and place the following code in it:

1<?php
2
3    namespace App\Http\Controllers;
4
5    use App\User;
6    use Validator;
7    use Illuminate\Http\Request;
8    use App\Http\Controllers\Controller;
9    use Illuminate\Support\Facades\Auth;
10
11    class UserController extends Controller
12    {
13        public function login()
14        {
15            $credentials = [
16                'email' => request('email'), 
17                'password' => request('password')
18            ];
19
20            if (Auth::attempt($credentials)) {
21                $success['token'] = Auth::user()->createToken('MyApp')->accessToken;
22
23                return response()->json(['success' => $success]);
24            }
25
26            return response()->json(['error' => 'Unauthorised'], 401);
27        }
28
29        public function register(Request $request)
30        {
31            $validator = Validator::make($request->all(), [
32                'name' => 'required',
33                'email' => 'required|email',
34                'password' => 'required',
35            ]);
36
37            if ($validator->fails()) {
38                return response()->json(['error' => $validator->errors()], 401);
39            }
40
41            $input = $request->all();
42            $input['password'] = bcrypt($input['password']);
43
44            $user = User::create($input);
45            $success['token'] = $user->createToken('MyApp')->accessToken;
46            $success['name'] = $user->name;
47
48            return response()->json(['success' => $success]);
49        }
50
51        public function getDetails()
52        {
53            return response()->json(['success' => Auth::user()]);
54        }
55    }

In the code above we have the:

Login Method: in here we call Auth::attempt with the credentials the user supplied. If authentication is successful, we create access tokens and return them to the user. This access token is what the user would always send along with all API calls to have access to the APIs.

Register Method: like the login method, we validated the user information, created an account for the user and generated an access token for the user.

Grouping routes under a common middleware

For our routes, we can group the routes we need authentication for under common middleware. Laravel comes with the auth:api middleware in-built and we can just use that to secure some routes as seen below in the routes/api.php file:

1<?php
2
3    Route::post('login', 'UserController@login');
4    Route::post('register', 'UserController@register');
5
6    Route::group(['middleware' => 'auth:api'], function(){
7        Route::resource('/task', 'TasksController');
8        Route::resource('/category', 'CategoryController');
9        Route::get('/category/{category}/tasks', 'CategoryController@tasks');
10    });

Handling API errors

In the event that our server encountered errors while serving or manipulating our resources, we have to implement a way to communicate to the client that something went wrong. For this, we have to serve the responses with specific HTTP status codes.

If you look at the UserControlle``r.php file you can see us implementing HTTP status code 401 which signifies that the client is not authorized to view the resource:

1public function login(Request $request)
2    {
3        $status = 401;
4        $response = ['error' => 'Unauthorised'];
5
6        [...]
7
8        return response()->json($response, $status);
9    }

Conclusion

In this part of the series, we have considered how you can create RESTful endpoints for your application. We also considered how you can handle errors and serve the correct HTTP status code to the client.

In the next chapter, we will address how to test your API endpoints using Postman. We will set up some unit tests, which will be useful for our own testing from the command-line.