Wiring Laravel and Svelte

Wiring Laravel and Svelte
Photo by Frames For Your Heart / Unsplash

I had the totally insane idea to assemble a simple project with Svelte, but in SPA mode using Laravel for the backend, and keeping all together in the same repository. Which seems to be the less popular setup, as I had to scavenge many websites and blogs and StackOverflow pages to obtain a bare minimum result.

Most of documentation around is about Svelte + Laravel + InertiaJS, but my main target was to acquire some confidence in building a standalone client-rendered application able to be eventually packed into Tauri for mobile deployment. So, the heavily server-driven approach of InertiaJS was not suitable.

The first heachache came figuring out how to organize code in folders. Laravel has his own folders hierarchy, SvelteKit has his own folders hierarchy, the (apparently) simple goal was to put all together and just execute npm run build for everything. Only diving into the Svelte code I realized that do not exists a configuration to be put in svelte.config.js to instruct the compiler about the fact that the code is not in the canonical src folder but somewhere else (resources/frontend, in my case); I had to create a new SvelteKit project, manually move files in the proper folders, and populate every paths-related options (also the ones I do not actually use, just in case) to obtain the result.

import adapter from '@sveltejs/adapter-static';

const config = {
  kit: {
    adapter: adapter({
      fallback: 'index.html',
    }),

    files: {
      assets: 'resources/frontend/static',
      hooks: {
        client: 'resources/frontend/src/hooks.client',
        server: 'resources/frontend/src/hooks.server',
        universal: 'resources/frontend/src/hooks',
      },
      lib: 'resources/frontend/src/lib',
      params: 'resources/frontend/src/params',
      routes: 'resources/frontend/src/routes',
      serviceWorker: 'resources/frontend/src/service-worker',
      appTemplate: 'resources/frontend/src/app.html',
      errorTemplate: 'resources/frontend/src/error.html',
    },
  }
};

export default config;

I do not define a pages parameter, so the build ends up - by default - in the build folder. Eventually I may decide to move it in public/app, keeping in mind that opting for just public will wipe the whole public folder at every build (including the index.php required to route API calls for Laravel... Yes, I did it...).

This implies that the webserver has to be configured to handle both types of requests: those directed to the SPA application, and those directed to the API backend (identified by the /api prefix).

server {
  listen 80;

  server_name example.local.it;

  location / {
    root /var/www/example/build/;
    index index.php index.html index.htm;
    try_files $uri $uri/ $uri.html /index.html;
  }

  location ^~ /api/ {
    alias /var/www/example/public/;
    try_files $uri $uri/ @nested;

    location ~ \.php$ {
      try_files $uri /index.php =404;
      fastcgi_pass unix:/run/php/php8.4-fpm.sock;
      fastcgi_param SCRIPT_FILENAME $request_filename;
      fastcgi_read_timeout 300;
      include fastcgi_params;
    }
  }

  location @nested {
    rewrite /api/(.*)$ /api/index.php?/ last;
  }
}

An interesting note is that, in this case, Laravel internal routing is not aware of the /api prefix, so routes have to be defined without that part of path.

<?php

use Illuminate\Support\Facades\Route;

use App\Http\Controllers\UserController;
use App\Http\Controllers\PlayController;

Route::middleware('auth:sanctum')->group(function() {
  Route::get('/user', [UserController::class, 'index'])->name('user.index');
  Route::get('/data', [PlayController::class, 'data'])->name('play.data');
});

It is important to remind that .env variables are accessible from import.meta.env, so it can be defined once the base URL for all API endpoints.

# in .env
VITE_BASE_API="${APP_URL}/api"

# in JS
const base = import.meta.env.VITE_BASE_API;

The next large pain in the ass is authentication. No one is going to explain you how to properly handle sessions in the client (even in the most obvious combination of Fortify + Sanctum), nor I'm able to do it as I'm still connecting things with the same awareness of a drunk monkey. But at least I can share some working portion of code.

I've found that the code presented in this blog post actually works to obtain a XSRF token from Laravel Sanctum, to be then used in subsequent requests.

let session = null;

function getCookie(cookieString, name) {
  const cookies = cookieString.split(";").map((cookieDeclaration) => {
    const [key, value] = cookieDeclaration.trim().split("=");
    return {
      key,
      value: decodeURIComponent(value)
    };
  });

  return cookies.find((cookie) => cookie.key === name);
}

async function getSession()
{
  if (session == null) {
    let fullurl = `${base}/sanctum/csrf-cookie`;

    await fetch(fullurl, {
      credentials: 'include',
    });

    const xsrfToken = getCookie(document.cookie, "XSRF-TOKEN");
    if (!xsrfToken) {
      throw new Error("No XSRF token found");
    }

    session = xsrfToken.value;
  }

  return session;
}

Once you obtain a token you can attach it to all other requests (in the X-XSRF-TOKEN header), as to POST your login request to the Fortify endpoint and then retrieve the current user with an authenticated endpoint.

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class UserController extends Controller
{
  public function index(Request $request)
  {
    return $request->user();
  }
}

Collected those informations, which I naively expected to find in some for-dummies tutorial, I've been able to start building my actual app. Which still is a total mess, but sooner or later I will understand the Svelte mindset...