Laravel Gates and Policies in an API Driven SPA

Part 42 of 48 in API Driven Development With Laravel and VueJS
Dan Pastori avatar
Dan Pastori August 21st, 2018
⚡️ Updated content is available We learned a lot since we originally wrote this article. We now have this updated for Laravel 10, Vue 3/NuxtJS 3, and Capacitor 3.

Now that we have our plan for our permissions and security planned out it’s time to implement it. For now we will be focusing only on cafe CRUD. More like just the creating, updating and deleting since everyone can read cafe data.

To implement this security we will be using a Laravel Policy: Authorization – Laravel – The PHP Framework For Web Artisans. Policies, according to the Laravel docs are: “Policies are classes that organize authorization logic around a particular model or resource.”. There are also gates. Gates block off certain methods individually. Since we have a specific resource, a cafe, we will be using a policy.

Now there are many other forms of security that we use in this app, and I’m sure in most of your applications such as validators and middleware. The way I incorporate all of these forms of security is more like a shell of an app. I use middleware (Middleware – Laravel – The PHP Framework For Web Artisans) first to make sure that certain requests are done by an authenticated user. This simply makes sure that a user is authenticated. ANY authenticated user can submit a new cafe, but only an authorized user can add it directly. When an authenticated user submits data for a new cafe, we validate the data to make sure it’s accurate. This is the next level of security. We know any authenticated user can submit data, so we use validators to make sure the data is accurate, otherwise we return an error.

Now we get into authorization, where we send the data. We check if the user has the right permissions to add a cafe, if they do, we add it directly and make an action stating the cafe has been added and processed. If they do not, we create an action that has not been processed and has to be approved by an admin. The cafe does not get added right away. Don’t worry, we will dive in and give some code examples and it will make a lot more sense!

Preparing our Users for Permissions

Before we even create a policy to check if the user has permissions to perform a method, we must set up a quick field on our user’s table to organize users by level.

To do this, I made a quick migration to our user’s table like this:

public function up()
{
    Schema::table('users', function( Blueprint $table ){
      $table->integer('permission')->after('id')->default(0);
    });
}

What this does, is after the id field, we add a permission field. The way this will work is as follows:

  • 3 – Super Admin
  • 2 – Admin
  • 1 – Shop Owner
  • 0 – General User

As you can see in the migration, we default every user to be a general user. This allows admins and super admins to promote users and not give any one permissions right off of the bat. We will use this column to determine permissions in our Cafe policy.

Now, the user with an ID of 1 is what we refer to as a coffee shop owner. This user type was mentioned when we were planning out our user structure. It’s the most unique of the 4 types as they can only add cafes to companies they own, edit cafes that belong to the company they own, and delete cafes for the companies they own.

Because of this, we need some relationship between users and companies they own, so we built a many to many relationship through a migration like this:

public function up()
{
    Schema::create('companies_owners', function( Blueprint $table ){
      $table->integer('user_id')->unsigned();
      $table->foreign('user_id')->references('id')->on('users');
      $table->integer('company_id')->unsigned();
      $table->foreign('company_id')->references('id')->on('companies');
    });
}

For more information on many to many relationships, check out: Many To Many Relationships With Laravel – Server Side Up.

Now we can have users who own a company. Don’t worry we will develop a process for users to reach out to an admin to own a company or not. For now, we just need this for our user permissions level. We will discuss this more in our next section when we create our policy.

To create these relationships on the models, open up app/Models/User.php and add the following relationship:

public function companiesOwned(){
  return $this->belongsToMany( 'App\Models\Company', 'companies_owners', 'user_id', 'company_id' );
}

Also open up the app/Models/Company.php and add the following relationship:

public function ownedBy(){
    return $this->belongsToMany( 'App\Models\User', 'companies_owners', 'company_id', 'user_id' );
    }

Now the company can be owned by many users and the users can own many companies.

Creating our Cafe Policy

This is where the magic begins to happen. I have to admit, as I was writing these policies for the first time I was thinking it was a little confusing. Before Laravel had these authorization tools, I always wrote things in a huge class with static methods and crazy checks. It was hard to break away and do things the Laravel way, but like usual with Laravel, once it clicked I could never go back. I’ll explain everything from the ground up. If any questions come up, for sure reach out and I will answer them as best as I can.

Like other Laravel implementations, there are many ways you can go about implementing authorization security. You can even use Gates which block unauthorized access on a “per-method” basis. A policy simply organizes these blocks into a nice easy to read class.

Let’s get started!

Laravel offers a slick artisan command to create a policy. It will auto generate the class necessary to create the policy and place it in the app/Policies directory. If you do not already have it created, it will create it for you. To create our CafePolicy run php artisan make:policy CafePolicy. If you want even more auto generated magic, you can append --model=Cafe to the end of the command and all of the CRUD will automatically be added.

Before we start writing our policy, let’s do some pseudo-coding to make it clearer. To get started, there are 3 methods we will need to focus on and 4 user groups. The pseudo code should look like this:

Creating A Cafe
To create a cafe, you have to be an admin, super admin, or the owner of the company the cafe you are creating belongs to.

Editing A Cafe
To edit a cafe, you have to be an admin, super admin, or owner of the company the cafe you are editing belongs to.

Deleting A Cafe
To delete a cafe, you have to be an admin, super admin, or owner of the company the cafe you are deleting belongs to.

Like I mentioned before, we don’t want to just block these routes off for users who don’t own the cafe or are just regular users. We want to create an action (discussed in the next section) so an admin can review the data and approve or deny the action.

Now that we have our policy created, let’s register it really quick with the app/Providers/AuthServiceProvider.php so open up that file.

At the top of the file, make sure you include the proper namespaces that will be used:

use App\Models\Cafe;
use App\Policies\CafePolicy;

Near the top of the file, there’s a protected $policies array that accepts a mapping from Eloquent model to policy. Add the following record to that array:

protected $policies = [
    Cafe::class => CafePolicy::class
];

Now when we are checking a method for a class, we bind it to the policy. So when we check the create method on a Cafe object, it’s bound to the CafePolicy class to check.

Writing Our Policy

Now it’s time to write our policy. Let’s open up our app/Policies/CafePolicy.php file.

It should look like:

<?php

namespace App\Policies;

use App\Models\User;

use Illuminate\Auth\Access\HandlesAuthorization;

class CafePolicy
{
    use HandlesAuthorization;

    /**
     * Create a new policy instance.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }
}

The first thing we want to do is make sure that the Cafe and Company models are ready for use, so add the following to the top of your class:

…
use App\Models\Cafe;
use App\Models\Company;
…

Now, let’s begin with the first method that will handle the permission to create a cafe. Add the following code to your CafePolicy:

/**
 * If a user is an admin or a super admin they can create
 * a cafe.
 *
 * @param \App\Models\User  $user
 * @param \App\Models\Company $company
 */
public function create( User $user, Company $company ){
  if( $user->permission == 2 || $user->permission == 3 ){
    return true;
  }else if( $company != null && $user->companiesOwned->contains( $company->id ) ){
    return true;
  }else{
    return false;
  }
}

First, this accepts two parameters. The first one, the User object, will be the user we are testing the policy against. It’s type hinted and only accepts a valid User object. When we implement our policy, you will see that this injected and we don’t directly pass this to the method. The second parameter is a Company object. We directly pass that as you will see when we implement.

The first check we have is if the user has a permission of 2 or 3 which is an admin or super admin. If so, we return true. Next, we check to see if the companiesOwned() relationship contains the companyID if that’s the case, then we allow the user to add the cafe because they own the company. Lastly, if both of these fail, we return false which will deny access to the functionality.

The next two methods are very similar so add them to the policy like this:

/**
 * If a user is an admin or super admin OR they own the cafe
 * company then can edit the cafe.
 *
 * @param \App\Models\User  $user
 * @param \App\Models\Cafe  $cafe
 */
public function update( User $user, Cafe $cafe ){
  if( $user->permission == 2 || $user->permission == 3 ){
    return true;
  }else if( $user->companiesOwned->contains( $cafe->company_id ) ){
    return true;
  }else{
    return false;
  }
}

/**
 * If a user is an admin or super admin OR they own the cafe company
 * then they can delete the cafe.
 *
 * @param \App\Models\User  $user
 * @param \App\Models\Cafe  $cafe
 */
public function delete( User $user, Cafe $cafe ){
  if( $user->permission == 2 || $user->permission == 3 ){
    return true;
  }else if( $user->companiesOwned->contains( $cafe->company_id ) ){
    return true;
  }else{
    return false;
  }
}

The only difference is the second parameter passed to each of the methods is an instance of the Cafe object. When we check to see if the user can update or delete a cafe, we check the company id in the relationship for the cafe not the company itself. That’s because for each of these scenarios, the cafe being edited already exists.

Now it’s time to implement our policy.

Implementing our Policy

There are many ways to implement a policy. You can add it via middleware, or via controller helpers. We are going to do it via the User model. For all the implementation methods check out: Laravel Policy Implementation. Since we are doing this via the user model, we will be checking if the user can or cannot perform certain functions. We are only checking right now for adding, updating, or deleting cafes. All of our code will be in the app\Http\Controllers\API\CafesController.php file so might as well open that up!

The first method we will look at is public function postNewCafe( StoreCafeRequest $request ). This method is validated by the StoreCafeRequest validator located in app\Http\Requests\StoreCafeRequest.php. The validator simply makes sure we have proper data coming in for the request. Sometimes it’d make sense to authorize the request BEFORE validating it, but in our case it doesn’t. Reason being, we want users to submit data whether they can or cannot but we want the data to be valid. If the user is not authorized to submit the data, we will create an action that allows the admin or owner of the cafe to approve the request.

Before we get in too deep on implementing our policy, let me explain how we will implement our actions. Each cafe method will have a corresponding action that keeps track of the history of the cafe essentially. We will create an action that is already approved if the user is authorized, and one that needs approval if the user is not authorized. In a later tutorial, we will be creating the routes to approve and deny additions, deletions, and edits to cafes.

First, let’s run a migration to create our actions table:

public function up()
{
    Schema::create('actions', function( Blueprint $table ){
      $table->increments('id');
      $table->integer('user_id')->unsigned();
      $table->foreign('user_id')->references('id')->on('users');
        $table->integer('company_id')->unsigned()->nullable();
      $table->foreign('company_id')->references('id')->on('companies');
      $table->integer('cafe_id')->unsigned()->nullable();
      $table->foreign('cafe_id')->references('id')->on('cafes');
      $table->integer('status');
      $table->integer('processed_by')->unsigned()->nullable();
      $table->foreign('processed_by')->references('id')->on('users');
      $table->dateTime('processed_on')->nullable();
      $table->string('type');
      $table->text('content');
      $table->timestamps();
    });
}

A few notes on this table. The user_id field will be the user submitting the action. The processed_by field will be the user who approved the action. If an action is added and the user is authorized, then these will be the same value. This just keeps track of the history of the cafe.

The content field will be different for each type of request. Essentially it will be the data up for review in a JSON format.

The type contains the type of action needing review. The three types are cafe-added, cafe-deleted, and cafe-updated.

Lastly, the status column can be either 0, 1, or 2. 0 means the action has not been processed (pending). 1 means the action has been approved, and 2 means the action has been denied.

We need to next add a model for these actions so add a file: app\Http\Models\Action.php and add the following code:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Action extends Model
{
    protected $table = 'actions';

  public function cafes(){
    return $this->belongsTo( 'App\Models\Cafe', 'cafe_id', 'id' );
  }

  public function by(){
    return $this->belongsTo( 'App\Models\User', 'user_id', 'id' );
  }

  public function processedBy(){
    return $this->belongsTo( 'App\Models\User', 'processed_by', 'id' );
  }
}

This sets up all of the necessary relationships. We should add a few relationships to our User model as well:

public function actions(){
  return $this->hasMany( 'App\Models\Action', 'id', 'user_id' );
}

public function actionsProcessed(){
  return $this->hasMany( 'App\Models\Action', 'id', 'processed_by' );
}

These relate actions to the user who processed and submitted them. Now it’s time to dive back into implementing our policy so back to public function postNewCafe( StoreCafeRequest $request ).

If you look at the method the way it is now, we need to wrap all of that functionality in an if to confirm if the user can add a cafe. Our policy states we need a User and a Company. Since we are adding a cafe, the company might not exist, so on top of the code for this method add:

/*
  Gets company that's adding the cafe
*/
$companyID = $request->get('company_id');

/*
  Get the company. If its null, create a new company otherwise
  set to the company that exists.
*/
$company = Company::where('id', '=', $companyID)->first();
$company = $company == null ? new Company() : $company;

What this does is grab the request for the company ID and loads the company. If the company doesn’t exist, just do a new company object, otherwise use the company. We have it like this because the owner could be adding a cafe to a company that exists. The Cafe is guaranteed to be new, we can’t pass a cafe to the policy method, we can pass a company though. If the company is new too, and the user is not an admin or super admin, then an action will be created.

Let’s implement our first policy!

After the code above, add the following code, wrapping the existing functionality:

if( Auth::user()->can('create', [ Cafe::class, $company ] ) ){
  //..EXISTING CODE
}else{
  //..CODE TO CREATE ACTION
}

Like I mentioned, we are calling the authorization method on the User model. This time we are using the Auth::user() as this model and checking if the user can do something. In this case create a Cafe. Now look at the second parameter: [ Cafe::class, $company ]. We have a Cafe::class before our $company that we loaded. This is because we are stating we are checking the CafePolicy for the Cafe::class defined in our AuthServiceProvider. This is EXTREMELY important because you could have create methods on other entities. This determines which policy will be used and injected if needed. Our other methods we will actually be using an instantiated Cafe::class but since we don’t have one at this point, we won’t be. We will be passing the $company variable we loaded right off of the bat though.

Honestly, that’s it! If the user can create a cafe class, the code will run that we had existing, otherwise an un processed action will be created and persisted to the database for an admin to approve later.

If the user created the Cafe, we also added an action that is already approved which looks like:

/*
  Create an already processed and approved action for the
  user since they have permission.
*/
$action                         = new Action();

$action->company_id             = $company->id
$action->user_id                = Auth::user()->id;
$action->status                 = 1;
$action->type                   = 'cafe-added';
$action->content                = json_encode( $request->all() );
$action->processed_by       = Auth::user()->id;
$action->processed_on   = date('Y-m-d H:i:s', time() );

$action->save();

The un-approved version looks like:

/*
  Create a new cafe action and save all of the data
  that the user has provided
*/
$action                         = new Action();

$action->company_id     = $request->get('company_id');
$action->user_id        = Auth::user()->id;
$action->status         = 0;
$action->type           = 'cafe-added';
$action->content        = json_encode( $request->all() );

$action->save();

Now let’s wrap our public function putEditCafe( $slug, EditCafeRequest $request ) and public function deleteCafe( $slug ) methods. They are very similar except each of them accept an actual instantiation of the Cafe class.

For the putEditCafe method, wrap the code like this:

/*
  Grab the cafe to be edited.
*/
$cafe = Cafe::where('slug', '=', $slug)->first();

/*
  Confirms user can edit the cafe through the Cafes Policy
*/
if( Auth::user()->can('update', $cafe ) ){
  //..Update cafe
}else{
  //..CREATE cafe-updated action
}

This time we load the cafe being edited, and pass it as the second parameter to the can method. This suffices as our Cafe::class and uses the update method on the CafePolicy.

The only real difference with the action is we make an array that saves the cafe before edits and the information passed to edit the cafe:

/*
  Set the before cafe to the data that was existing,
  and the after to what was set.
*/
$content['before']          = $cafe;
$content['after']           = $request->all();

This is so on the admin side we will be able to see the edits side by side. The type of this action is a cafe-updated action.

For our public function deleteCafe( $slug ) we simply need to wrap the existing code like:

/*
  Grabs the Cafe to be deleted
*/
$cafe = Cafe::where('slug', '=', $slug)->first();

/*
  Checks if the user can delete the cafe through
  our CafePolicy.
*/
if( Auth::user()->can('delete', $cafe ) ){
  //..Delete cafe
}else{
  //..Create cafe-deleted action
}

Once again, the cafe being deleted is loaded and passed to suffice for our Cafe::class object. The type action being created is a cafe-deleted action.

One thing to note, is when we send back our response from any of the methods where the data hasn’t been committed ( creating an action to be approved ), the proper response code is 202. This means that there is a process pending. The process being that an admin has to approve it.

That’s about all we need to do on the server side for protecting actions performed by the user for this tutorial. The next section I will add a little bit of code to display to the user the state of the action and we will wrap up! In the next couple tutorials, we will be building an admin side to our SPA and building the corresponding routes and providing security based on the user to these routes.

Refactoring Large Controllers

Planning ahead, there will now be multiple places where we add a cafe and create an action. All of this logic is in the controller. For simple apps you can get away with it, but the second you do something more than once, it’s a good idea to create a single point for the method. There’s lots of coding methodologies such as the Single Responsibility Principle or DRY (Don’t Repeat Yourself) that re-iterate this. Honestly, it leads to much more developer friendly code.

Let’s take a look at adding a cafe. There is one place right now and that’s being an admin and submitting a new cafe request. All of the logic for doing this is done in the controller. Now that we have actions, we are about ready to add an “approve” action route that can approve a cafe addition. A DIFFERENT route to add a cafe. We don’t want to add this logic in the controller as well so what I did was make a service. This is a special name for a class and essentially it contains a static method that creates a cafe that I can re-use in my code.

First, I created a directory: app\Services and added the following file: app\Services\CafeService.php. I added the namespace to the top of the file as well as a static create method like this:

<?php

namespace App\Services;

use App\Models\Cafe;
use App\Models\Company;

use App\Utilities\GoogleMaps;

use \Cviebrock\EloquentSluggable\Services\SlugService;

use Auth;

class CafeService{
  public static function addCafe( $data, $addedBy ){

  }
}

I then included all of the classes needed to create a cafe. In the method signature there are two parameters. The first is $data and that is an array of data for the cafe. The second is $addedBy and that will be the ID of the user who added the cafe.

I then abstracted all of my code from the controller into this method and called:

$cafe = CafeService::addCafe( $request->all(), Auth::user()->id );

when I needed to create a cafe (an admin adding a cafe).

Of course, make sure to use the CafeService on top of your CafesController. The first parameter is all of the request data and the second is the ID of the authenticated user. This way I can call this method from any where in our app and we don’t over load our controllers with business logic.

I also added a service for cafe actions as well that adds pre-approved and un-approved actions. The service is app\Services\ActionService.php and the methods look like this:

<?php

namespace App\Services;

use App\Models\Action;

use Auth;

class ActionService{
  public static function createPendingAction( $cafeID, $companyID, $type, $content ){
    $action                         = new Action();

    $action->cafe_id        = $cafeID;
    $action->company_id = $companyID;
    $action->user_id        = Auth::user()->id;
    $action->status         = 0;
    $action->type           = $type;
    $action->content        = json_encode( $content );

    $action->save();
  }

  public static function createApprovedAction( $cafeID, $companyID, $type, $content ){
    $action = new CafeAction();

    $action->cafe_id                = $cafeID;
    $action->company_id         = $companyID;
    $action->user_id                = Auth::user()->id;
    $action->status                 = 1;
    $action->type                   = $type;
    $action->content                = json_encode( $content );
    $action->processed_by       = Auth::user()->id;
    $action->processed_on   = date('Y-m-d H:i:s', time() );

    $action->save();
  }
}
?>

Now we can pass the appropriate content to the action and create the action accordingly which will allow us to do this in as many places that come up.

For adding a cafe, our postNewCafe() method in our app/Http/Controllers/API/CafesController.php should look like:

/*
|-------------------------------------------------------------------------------
| Adds a New Cafe
|-------------------------------------------------------------------------------
| URL:            /api/v1/cafes
| Method:         POST
| Description:    Adds a new cafe to the application
*/
public function postNewCafe( StoreCafeRequest $request ){
  /*
    Gets company that's adding the cafe
  */
  $companyID = $request->get('company_id');

  /*
    Get the company. If its null, create a new company otherwise
    set to the company that exists.
  */
  $company = Company::where('id', '=', $companyID)->first();
  $company = $company == null ? new Company() : $company;

  /*
    Determines if the user can create a cafe or not.
    If the user can create a cafe, then we let them otherwise
    we create an add cafe action.
  */
  if( Auth::user()->can('create', [ Cafe::class, $company ] ) ){
    $cafe = CafeService::addCafe( $request->all(), Auth::user()->id );

    /*
      Create an already processed and approved action for the
      user since they have permission.
    */
    ActionService::createApprovedAction( null, $cafe->company_id, 'cafe-added', $request->all() );

    /*
      Grab the company to return
    */
    $company = Company::where('id', '=', $cafe->company_id)
                      ->with('cafes')
                      ->first();

    /*
      Return the added cafes as JSON
    */
    return response()->json( $company, 201);
  }else{
    /*
      Create a new cafe action and save all of the data
      that the user has provided
    */
    ActionService::createPendingAction( null, $request->get('company_id'), 'cafe-added', $request->all() );

    /*
      Return the flag that the cafe addition is pending
    */
    return response()->json( ['cafe_add_pending' => $request->get('company_name') ], 202 );
  }
}

Much cleaner! We will see our return on investment in the next tutorial when we run the same add method on approving a cafe. We can just call this method and everything is created ready to rock!

I also did this for the update cafe method since it was a lot of code and should be cleaned up as well. It’s the same process and you can view the code here: https://github.com/serversideup/roastandbrew. Of course if you have any questions on how you can clean up controllers, ask in the comments below.

We now just have to show the status of the action to the authenticated user in the front end!

Showing action status to the user

Since we switched to actions, the three methods from the APIs will return different things if the user is not authorized to perform an action. We should alert the user of this on the SPA Vue side. If the user cannot automatically create a cafe, we pass back: cafe_add_pending with the name of the cafe. With edits, it’d be cafe_edit_pending and with deletions, cafe_delete_pending with the name of the cafe.

Let’s handle these in Vue so we can notify the user the status of their submission.

The first thing I did for all of the actions is created a text state in my /resources/assets/js/modules/cafes.js file that held onto the text from each response. For example, adding a cafe I added the state cafeAddText, editing a cafe cafeEditText and deleting a cafe was cafeDeleteText.

Then in each action, I checked to see if the cafe_{action}_pending was set. If it was, I used the cafe name and set the text to be according to the action. If we look specifically at the response from the addCafe() action, it looks like this:

addCafe( { commit, state, dispatch }, data ){
  commit( 'setCafeAddedStatus', 1 );
  CafeAPI.postAddNewCafe( data.company_name, data.company_id, data.company_type, data.website, data.location_name, data.address, data.city, data.state, data.zip, data.lat, data.lng, data.brew_methods, data.matcha, data.tea )
      .then( function( response ){
        if( typeof response.data.cafe_add_pending !== 'undefined' ){
          commit( 'setCafeAddedText', response.data.cafe_add_pending +' is pending approval!');
        }else{
          commit( 'setCafeAddedText', response.data.name +' has been added!');
        }

        commit( 'setCafeAddedStatus', 2 );
        commit( 'setCafeAdded', response.data );
        dispatch( 'loadCafes' );
      })
      .catch( function(){
        commit( 'setCafeAddedStatus', 3 );
      });
},

I added it before we set the status of the method, so the text is available in our components when the status is updated so we can display it.

If the action run successfully, I set the text accordingly as well. I also added the getters and setters for these actions in the /resources/assets/js/modules/cafes.js file.

Then whenever the method was called from the component and I listen for the response, I also emit a notification to the user for the action. All of these are done very similarly so to avoid redundancy, I’ll show you how I did it for adding a cafe.

I first opened up the /resources/assets/js/pages/NewCafe.vue file. In the computed() method on the component, I added:

addCafeText(){
  return this.$store.getters.getCafeAddText;
}

This gets the cafe added text from the state when needed.

Next, in the watcher we already have in place for the addCafeStatus, I added the following code when addCafeStatus == 2:

EventBus.$emit('show-success', {
  notification: this.addCafeText
});

What this will do is emit the appropriate text to our notification for the user. Now the user will know if the cafe has been added or is pending!

To see all of the other examples, check out the GitHub here: GitHub – serversideup/roastandbrew: Roast helps coffee enthusiasts find their next cup of coffee. It’s also open source to help aspiring developers build a single page app on web and mobile. All tutorials can be found at https://srvrsi.de/roast.

That’s it for now! We have our policies in place and our data locked down. Next up, we will be building the admin side in our SPA and shutting down the admin routes through Vue and then building admin routes only for processing actions. If there are any questions, make sure to leave a comment below! Also, if you want a deeper dive into API Driven development, be sure to sign up for our book when we launch: Server Side Up General List.

Keep Reading
View the course View the Course API Driven Development With Laravel and VueJS
Up Next → Laravel Admin Routes and Security in a SPA

Support future content

The Ultimate Guide to Building APIs and Single-Page Applications with Laravel + VueJS + Capacitor book cover.

Psst... any earnings that we make off of our book is being reinvested to bringing you more content. If you like what you read, consider getting our book or get sweet perks by becoming a sponsor.

Written By Dan

Dan Pastori avatar Dan Pastori

Builder, creator, and maker. Dan Pastori is a Laravel certified developer with over 10 years experience in full stack development. When you aren't finding Dan exploring new techniques in programming, catch him at the beach or hiking in the National Parks.

Like this? Subscribe

We're privacy advocates. We will never spam you and we only want to send you emails that you actually want to receive. One-click unsubscribes are instantly honored.