Building Single Page Application with Laravel and VueJS

vue-spa-with-laravel
Reading Time: 9 minutes

What is a Single Page Application?

A single page application (SPA) is a website development approach that enables a user to visit different parts/pages of a website without loading new HTML pages from the server. A single page application gives the feel of a desktop or mobile app

In this tutorial, we will be building a To-do list app (Single Page Application) that will enable us to add, delete, update and view all pending tasks using Laravel for its backend and VueJS for its frontend

Laravel is a php framework developed by Taylor Otwell is a PHP based web-framework with a lot of out of the box functionalities developed to ease the process in building medium to large web applications.

VueJS is a javascript framework developed by Evan You is a progressive Javascript framework that allows you to create a beautiful user interface and experience easily.

Let’s get started 🙂

Laravel Installation

  1. Open your terminal and go to the directory where you want to store your project.
  2. Before installation ensure you meet the laravel installation requirements here: https://laravel.com/docs/6.x#server-requirements
  3. Install laravel by typing the following command in your terminal
    composer create-project --prefer-dist laravel/laravel todo_list_app
  4. Once the installation is complete, enter into the project directory and start the app
    cd todo_list_app
    php artisan serve
  5. Laravel will assign a web address to you on your terminal as seen below;
    $ php artisan serve
    Laravel development server started: localhost:8000

    copy the web address from your terminal to your preferred web browser to confirm your installation was successful by seeing the image below.

Laravel

Laravel .env file

The .env file located at the root directory of the laravel project contains configuration variables for our application as seen below. Before we continue this tutorial ensure you put the right variables for the following keys DB_DATABASEDB_USERNAMEDB_PASSWORD

APP_NAME=Laravel
APP_ENV=local
APP_KEY=base64:MGFGyZ4T8XFCY1fkxqRJw1f6WQiL+odkmu05B9kmcGM=
APP_DEBUG=true
APP_URL=http://localhost

LOG_CHANNEL=stack

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=todo_list_app
DB_USERNAME=root
DB_PASSWORD=

BROADCAST_DRIVER=log
CACHE_DRIVER=file
QUEUE_CONNECTION=sync
SESSION_DRIVER=cookie
SESSION_LIFETIME=120

REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379

MAIL_DRIVER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null

AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=

PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_APP_CLUSTER=mt1

MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"

Note: It is expected that your development server has been setup

 

Create Migrations for database

According to the laravel documentation: Migrations are like version control for your database, allowing your team to easily modify and share the application’s database schema. Migrations are typically paired with Laravel’s schema builder to easily build your application’s database schema. If you have ever had to tell a teammate to manually add a column to their local database schema, you’ve faced the problem that database migrations solve.

In our project, our database will have only one table and we will name it tasks. Delete all the sample files in the database > migrations directory, we will not be using them in this tutorial. Let’s create our migration file.

  1. Create migration for tasks table. Open your terminal and navigate to your project directory then run the command
    php artisan make:migration create_tasks_table
  2. A migration file will be created in database > migrations directory, open the latest file created and copy the code below into it
    <?php
    
    use Illuminate\Database\Migrations\Migration;
    use Illuminate\Database\Schema\Blueprint;
    use Illuminate\Support\Facades\Schema;
    
    class CreateTasksTable extends Migration
    {
        /**
         * Run the migrations.
         *
         * @return void
         */
        public function up()
        {
            Schema::create('tasks', function (Blueprint $table) {
                $table->bigIncrements('id');
                $table->string('title', 191);
                $table->text('description');
                $table->timestamps();
            });
        }
    
        /**
         * Reverse the migrations.
         *
         * @return void
         */
        public function down()
        {
            Schema::dropIfExists('tasks');
        }
    }
    

    3. Then run the command below to create the tasks table in our database

    php artisan migrate

Create a Model for tasks Table

Models allow us to query for data in our tables, as well as insert new records into the table. Models are stored in the app directory

  1. Open your terminal and navigate into the project directory, then run the command below
    php artisan make:model Task

    Task.php file will be created in the app directory

 

Create a Task Controller

Controllers group related request handling/business logic into a single class. Controllers are stored in the app > Http > Controllers directory.

  1. Open your terminal and navigate into the project directory, then the run command below
    php artisan make:controller TaskController --resource
  2. Then copy the code below into your TaskController.php file in app > Http > Controllers directory
    <?php
    
    namespace App\Http\Controllers;
    
    use App\Task;
    use Illuminate\Http\Request;
    
    class TaskController extends Controller
    {
        //all tasks
        public function index()
        {
            $tasks = Task::all();
    
            return response()->json($tasks);
        }
        
        //create new task 
        public function create(Request $request)
        {
            
            $task = new Task;
            $task->title = $request->get('title');
            $task->description = $request->get('description');
            $task->save();
    
            return response()->json('The task was successfully added');
    
        }
    
        //edit Task
        public function edit($id)
        {
    
            $task = Task::find($id);
            return response()->json($task);
            
        }
    
        //update task
        public function update(Request $request, $id)
        {
            $task = Task::find($id);
            $task->title = $request->get('title');
            $task->description = $request->get('description');
            $task->save();
    
            return response()->json('The task was updated successfully');
        }
    
        //delete task
        public function delete($id)
        {
            $task = Task::find($id);
            $task->delete();
    
            return response()->json('The task was deleted successfully');
        }
        
    }
    

Define API routes

The API routes would be consumed by our frontend to send and receive data based on the business logic in our TaskController.php

Copy the code below to api.php in the routes directory

<?php

/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| is assigned the "api" middleware group. Enjoy building your API!
|
*/


Route::get('tasks', 'TaskController@index');
Route::post('create', 'TaskController@create');
Route::get('edit/{id}', 'TaskController@edit');
Route::post('update/{id}', 'TaskController@update');
Route::delete('delete/{id}', 'TaskController@delete');

***We are done with the Laravel backend implementation, to test what we have done so far, you can use Postman to test our APIs. Ensure you have run the php artisan serve command in your terminal. For example to create a new task you will send a post request to http://localhost:8000/api/create.

**http://localhost:8000 could be different depending on your machine

 

VueJS Installation

Next, we implement the frontend integration for our APIs created using VueJS

  1. Open your terminal and navigate into the project directory, then install the frontend node modules dependencies
    npm install
  2.  Next, we will install Vue;
    npm install --save vue
  3. We will then install Vue-router; Vue-router is the official router package for Vue. It allows us to navigate to different parts of our website without the web page reloading
    npm install vue-router
  4. We will also install Axios; Axios is a Javascript library used to make HTTP requests. It allows us to send and receive data from our laravel API endpoints that we will create later
    npm install axios
  5. Then run the command below to ensure everything is working fine.
    npm run watch

    Note: npm run watch ensures that your Vue (frontend) files re-compile whenever a change is made on any of the files.

 

Linking VueJS with Laravel

  1. Define laravel route in web.php under the routes directory.
    <?php
    
    //web.php
    Route::get('{any}', function () {
        return view('app');
    })->where('any', '.*');

    Note: This ensures that Laravel is no longer in charge of routing to different parts of the website but VueJS

  2. Next, we will create an app.blade.php file in resources > views directory, which will contain the app component that Vue will render along with rendering appropriate content based on the web URL.
    <!doctype html>
    <html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
    
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <meta name="csrf-token" value="{{ csrf_token() }}" />
        <title>To-Do List App Tutorial - Tolustar</title>
        <link href="https://fonts.googleapis.com/css?family=Nunito:200,300,400,500,600" rel="stylesheet" type="text/css">
        <link href="{{ mix('css/app.css') }}" type="text/css" rel="stylesheet" />
    
        <!-- Bootstrap CSS -->
        <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css">
    </head>
    
    <body>
        <div id="app">
        </div>
        <script src="{{ mix('js/app.js') }}" type="text/javascript"></script>
    
        <!-- Bootstrap JS and dependencies -->
        <script src="https://code.jquery.com/jquery-3.4.1.slim.min.js"></script>
        <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js"></script>
        <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js"></script>
    </body>
    
    </html>

    Note: We also installed Bootstrap framework via CDN by adding its respective CSS links and JS scripts in our app.blade.php file

  3. Create a components directory in resources > js directory
  4. Create an App.vue file in the components directory resources > js > components. This file serves as the outermost component of the application that will enable all other components to be displayed
    <template>
        <div class="container">
            <div class="text-center" style="margin: 20px 0px 20px 0px;">
                <h3 class="text-secondary">Laravel & Vue CRUD Single Page Application (SPA) Tutorial</h3>
            </div>
    
            <nav class="navbar navbar-expand-lg navbar-light bg-light">
                <div class="collapse navbar-collapse">
                    <div class="navbar-nav">
                        <router-link to="/" class="nav-item nav-link">Home</router-link>
                        <router-link to="/create" class="nav-item nav-link">Create Task</router-link>
                    </div>
                </div>
            </nav>
            <br/>
            <router-view></router-view>
        </div>
    </template>
    
    <script>
        export default {}
    </script>

 

Create Frontend Pages

  1. Create pages directory in  resources > js > components
  2. In the pages directory resources > js > components > pages create the following files AllTasks.vue, NewTask.vue, EditTask.vue
  3. Open the AllTasks.vue file and copy the code below
    <template>
        <div>
            <h3 class="text-center">All Tasks</h3><br/>
    
            <table class="table table-bordered">
                <thead>
                <tr>
                    <th>ID</th>
                    <th>Title</th>
                    <th>Description</th>
                    <th>Created At</th>
                    <th>Updated At</th>
                    <th>Actions</th>
                </tr>
                </thead>
                <tbody>
                <tr v-for="task in tasks" :key="task.id">
                    <td>{{ task.id }}</td>
                    <td>{{ task.title }}</td>
                    <td>{{ task.description }}</td>
                    <td>{{ task.created_at }}</td>
                    <td>{{ task.updated_at }}</td>
                    <td>
                        <div class="btn-group" role="group">
                            <router-link :to="{name: 'edit', params: { id: task.id }}" class="btn btn-primary">Edit
                            </router-link>
                            <button class="btn btn-danger" @click="deleteTask(task.id)">Delete</button>
                        </div>
                    </td>
                </tr>
                </tbody>
            </table>
        </div>
    </template>
    
    <script>
        export default {
            data() {
                return {
                    tasks: []
                }
            },
            created() {
                this.axios
                    .get('http://localhost:8000/api/tasks')
                    .then(response => {
                        this.tasks = response.data;
                    });
            },
            methods: {
                deleteTask(id) {
                    this.axios
                        .delete(`http://localhost:8000/api/delete/${id}`)
                        .then(response => {
                            let i = this.tasks.map(item => item.id).indexOf(id); // find index of your object
                            this.tasks.splice(i, 1)
                        });
                }
            }
        }
    </script>
  4. Open the NewTask.vue file and copy the code below
    <template>
        <div>
            <h3 class="text-center">New Task</h3>
            <div class="row">
                <div class="col-md-12">
                    <form @submit.prevent="newTask">
                        <div class="form-group">
                            <label>Title</label>
                            <input type="text" class="form-control" v-model="task.title">
                        </div>
                        <div class="form-group">
                            <label>Description</label>
                            <input type="text" class="form-control" v-model="task.description">
                        </div>
                        <button type="submit" class="btn btn-primary">Add Task</button>
                    </form>
                </div>
            </div>
        </div>
    </template>
    
    <script>
        export default {
            data() {
                return {
                    task: {}
                }
            },
            methods: {
                newTask() {
    
                    this.axios
                        .post('http://localhost:8000/api/create', this.task)
                        .then(response => (
                            this.$router.push({name: 'home'})
                        ))
                        .catch(error => console.log(error))
                        .finally(() => this.loading = false)
                }
            }
        }
    </script>
  5. Open the EditTask.vue file and copy the code below
    <template>
        <div>
            <h3 class="text-center">Edit Task</h3>
            <div class="row">
                <div class="col-md-12">
                    <form @submit.prevent="updateTask">
                        <div class="form-group">
                            <label>Title</label>
                            <input type="text" class="form-control" v-model="task.title">
                        </div>
                        <div class="form-group">
                            <label>Description</label>
                            <input type="text" class="form-control" v-model="task.description">
                        </div>
                        <button type="submit" class="btn btn-primary">Update Task</button>
                    </form>
                </div>
            </div>
        </div>
    </template>
    
    <script>
        export default {
            data() {
                return {
                    task: {}
                }
            },
            created() {
                this.axios
                    .get(`http://localhost:8000/api/edit/${this.$route.params.id}`)
                    .then((response) => {
                        this.task = response.data;
                    });
            },
            methods: {
                updateTask() {
                    this.axios
                        .post(`http://localhost:8000/api/update/${this.$route.params.id}`, this.task)
                        .then((response) => {
                            this.$router.push({name: 'home'});
                        });
                }
            }
        }
    </script>

    **Note: In the three files AllTasks.vue, NewTask.vue, EditTask.vue we used Axios to consume the laravel APIs

 

Define Vue Routes

We define the Vue routes in a route.js file in resources > js directory. Defining the vue routes ensures that we can navigate to different parts of our web app without encountering an error.

import AllTasks from './components/pages/AllTasks.vue';
import NewTask from './components/pages/NewTask.vue';
import EditTask from './components/pages/EditTask.vue';

export const routes = [
    {
        name: 'home',
        path: '/',
        component: AllTasks
    },
    {
        name: 'create',
        path: '/create',
        component: NewTask
    },
    {
        name: 'edit',
        path: '/edit/:id',
        component: EditTask
    }
];

 

Importing All to App.js

We bring everything together by importing all the necessary dependencies that we have used so far such as Axios, Vue-router, routes, and also ensure our app is mounted to the DOM in the app.js file found in resources > js directory.

import Vue from 'vue'

import App from './components/App.vue';
import VueRouter from 'vue-router';
import axios from 'axios';
import {routes} from './routes';

Vue.use(VueRouter);

Vue.prototype.axios = axios;

const router = new VueRouter({
    mode: 'history',
    routes: routes
});

const app = new Vue({
    el: '#app',
    router: router,
    render: h => h(App),
});

Final Output

We have completed the tutorial for today, let us run our project and see how it looks like

All TasksNew TaskEdit Task

Conclusion

In today’s tutorial, we have learnt how to build a single page app with Laravel and VueJS. In future tutorials, I will explain how to secure certain API endpoints from unauthorized users and authenticate users from accessing restricted data based on user roles.

Do let me know if you have any question on this tutorial with the comment box below and don’t forget to share and like this tutorial. Cheers

Leave a comment

%d bloggers like this: