Laravel catch-all route for Vue Single Page Applications

Reading time ~3 minutes

Introduction

Building a complex VueJS SPA implies a lot more challenges than an ordinary application. At my job we are creating a VueJS powered SAAS platform, which shows us how powerful this library is, but also how easy it is to misunderstand things.

The Problem

This application is built upon a set of APIs provided by Laravel. We also have a few routes with static regular pages. Our RouteServiceProvider.php looks like the following:

<?php

public function map(Router $router)
{
    $this->mapApiRoutes($router);
    $this->mapClientRoutes($router);
}

/**
 * Define the "client" routes for the application.
 * These also includes static pages
 *
 * @param  \Illuminate\Routing\Router  $router
 * @return void
 */
protected function mapClientRoutes(Router $router)
{
    $router->group([
        'namespace' => $this->namespace,
        'middleware' => ['session'],
    ], function ($router) {
        require base_path('routes/client.php');
    });
}

/**
 * Define the "api" routes for the application.
 *
 * @param  \Illuminate\Routing\Router  $router
 * @return void
 */
protected function mapApiRoutes(Router $router)
{
    $router->group([
        'namespace' => $this->namespace,
        'middleware' => 'api',
        'prefix' => 'api',
    ], function ($router) {
        require base_path('routes/api.php');
    });
}

As you can see APIs routes are mapped before client routes. This is a specific requirement of Vue-Router, because we need to add a catch-all fallback route in order to serve the Vue application using “History mode”. Otherwise users will get 404 errors if they access a specific URL (that is defined in the routes file of Vue-Router, but not in Server’s configuration).

In Laravel this catch-all route can be defined in the following way, at the end of the last route file:

<?php

Route::get('/{catchall?}', 'AppController@show');

Everything that is not matched in previous routes will be served by the AppController, which returns the naked view that contains the files for bootstrapping the VueJS application.

However there is a little caveat, our server will “no longer report 404 errors as all not-found paths now serve up your index.html file”. To get around the issue, the documentation suggests to implement a catch-all route within the Vue app to show a 404 page:

const router = new VueRouter({
  mode: 'history',
  routes: [
    { path: '*', component: NotFoundComponent }
  ]
});

This works great for “regular” Vue pages, but what about the APIs? If we try to fetch a route that doesn’t exist, for example because we misspelled it, we notice that the status code of the response is 200 and the content is the HTML for the VueJS application. Not really what we want.

The Solution

At first we were really confused by this behaviour, however the solution is really easy and elegant. Simply we need to configure the catch-all route in order to ignore routes that start with the api segment. We can define it using a regex constraint:

<?php

Route::get('/{catchall?}', 'AppController@show')->where('catchall', '^(?!api).*$')->name('administration');

Bonus: match only numeric routes

Do you need a regex constraint to match only numeric routes? Here we go:

<?php

Route::get('/{route?}', 'RouteController@show')->where('route', '([0-9]+(\/){0,1})*');
comments powered by Disqus

Building a basic reserved area with ProcessWire

Introduction

In this tutorial we will learn how to add a simple reserved area to an existing ProcessWire website, using two handy …

Split char-separated values in MySQL