Crafting a Modular Laravel API for Effective SPA Interaction

yurich84

Yurich

Posted on March 17, 2024

Crafting a Modular Laravel API for Effective SPA Interaction

Let's create Laravel app

composer create-project laravel/laravel example-app

or laravel new example-app

go to app folder cd example-app

Now you can run php artisan serve and visit http://127.0.0.1:8000 to verify that the website is working.

Remember that we need to adjust the .env file, specifically to configure the database connection and specify the correct URL APP_URL=http://127.0.0.1:8000.

If we don't have API routes it can be easily fixed by running the command php artisan install:api.

Let's proceed to create a modular structure. The modules will be located in the Modules folder, so let's create them in the app directory.

First of all, let's dynamically include all routes. To do this, go to app.php and add the following code:


use Illuminate\Support\Facades\Route;

if (!defined('API_PREFIX')) define('API_PREFIX', 'api/v1');  

$modules_folder = app_path('Modules');  
$modules = array_values(  
    array_filter(  
        scandir($modules_folder),  
        function ($item) use ($modules_folder) {  
            return is_dir($modules_folder.DIRECTORY_SEPARATOR.$item) && ! in_array($item, ['.', '..']);  
        }  
    )  
);  

foreach ($modules as $module) {  
    $routesPath = $modules_folder.DIRECTORY_SEPARATOR.$module.DIRECTORY_SEPARATOR.'routes_api.php';  

    if (file_exists($routesPath)) {  
        Route::prefix(API_PREFIX)  
            ->middleware(['auth:sanctum'])  
            ->namespace("\\App\\Modules\\$module\Controllers")  
            ->group($routesPath);  
    }  
}
Enter fullscreen mode Exit fullscreen mode

To make the SPA works, you need to add one more route at the end of the web.php file.

Route::view('/{any}', 'spa')->where('any', '^(?!api).*'); 
Enter fullscreen mode Exit fullscreen mode

We also need to create a Blade template spa.blade.php for the main route where Vue will be connected with libraries.

<!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" content="{{ csrf_token() }}">  
        <title>{{ config('app.name') }}</title>  
    </head>  
    <body>
        <div id="app"></div>  
    </body>
</html>
Enter fullscreen mode Exit fullscreen mode

Now, all routes that do not start with /api will be directed to this template. This will enable front-end routing.

Building a Restful API

Following the modular system laid out in the article My point of view on SPA modularity using Laravel and Vue.js, let's create the first module.

To begin, let's create a folder app\Modules.

Let's assume our CRM will have a system for managing static pages, meaning we need CRUD functionality for this.

Execute the command to create a model and migration

php artisan make:model Page -m

Edit the created files

Add the name field to both the migration file and the model. The code for the files will look accordingly.

Migration:

<?php  

use Illuminate\Database\Migrations\Migration;  
use Illuminate\Database\Schema\Blueprint;  
use Illuminate\Support\Facades\Schema;  

return new class extends Migration  
{  
    /**  
     * Run the migrations.     *     * @return void  
     */  
    public function up()  
    {  
        Schema::create('pages', function (Blueprint $table) {  
            $table->id();  
            $table->string('name');  
            $table->timestamps();  
        });  
    }  

    /**  
     * Reverse the migrations.     
     *     
     * @return void  
     */  
    public function down()  
    {  
        Schema::dropIfExists('pages');  
    }  
};
Enter fullscreen mode Exit fullscreen mode

Model:

<?php  

namespace App\Models;  

use Illuminate\Database\Eloquent\Factories\HasFactory;  
use Illuminate\Database\Eloquent\Model;  

class Page extends Model  
{  
    use HasFactory;  

    const COLUMN_ID = 'id';  
    const COLUMN_NAME = 'name';  

    protected $guarded = [self::COLUMN_ID];  
}
Enter fullscreen mode Exit fullscreen mode

Let's run the migrations: php artisan migrate.

In the Modules directory, create a folder named Page, which will contain the following files:

routes_api.php

<?php  

use Illuminate\Support\Facades\Route;  

Route::apiResource('pages', 'PageController');
Enter fullscreen mode Exit fullscreen mode

Controllers/PageController.php

<?php  

namespace App\Modules\Page\Controllers;  

use App\Http\Controllers\Controller;  
use App\Models\Page;  
use App\Modules\Page\Requests\PageRequest;  
use App\Modules\Page\Resources\PageResource;  
use Exception;  
use Illuminate\Database\Eloquent\Builder;  
use Illuminate\Http\JsonResponse;  
use Illuminate\Http\Request;  
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;  

class PageController extends Controller  
{  
    /**  
     * Display list of resources     
     *     
     * @param  Request  $request  
     * @return AnonymousResourceCollection  
     */  
    public function index(Request $request)  
    {  
        [$column, $order] = explode(',', $request->input('sortBy', 'id,asc'));  
        $pageSize = (int) $request->input('pageSize', 10);  

        $resource = Page::query()  
            ->when($request->filled('search'), function (Builder $q) use ($request) {  
                $q->where(Page::COLUMN_NAME, 'like', '%'.$request->search.'%');  
            })  
            ->orderBy($column, $order)->paginate($pageSize);  

        return PageResource::collection($resource);  
    }  

    /**  
     * Store a newly created resource in storage.     
     *
     * @param  PageRequest  $request  
     * @param  Page  $page  
     * @return JsonResponse  
     */  
    public function store(PageRequest $request, Page $page)  
    {  
        $data = $request->validated();  
        $page->fill($data)->save();  

        return response()->json([  
            'type' => self::RESPONSE_TYPE_SUCCESS,  
            'message' => 'Successfully created',  
        ]);  
    }  

    /**  
     * Display the specified resource.     
     *
     * @param  Page  $page  
     * @return PageResource  
     */  
    public function show(Page $page)  
    {  
        return new PageResource($page);  
    }  

    /**  
     * Update the specified resource in storage.     
     *
     * @param  PageRequest  $request  
     * @param  Page  $page  
     * @return JsonResponse  
     */  
    public function update(PageRequest $request, Page $page)  
    {  
        $data = $request->validated();  
        $page->fill($data)->save();  

        return response()->json([  
            'type' => self::RESPONSE_TYPE_SUCCESS,  
            'message' => 'Successfully updated',  
        ]);  
    }  

    /**  
     * Delete the specified resource.     
     *     
     * @param  Page  $page  
     * @return JsonResponse  
     *  
     * @throws Exception  
     */  
    public function destroy(Page $page)  
    {  
        $page->delete();  

        return response()->json([  
            'type' => self::RESPONSE_TYPE_SUCCESS,  
            'message' => 'Successfully deleted',  
        ]);  
    }  
}
Enter fullscreen mode Exit fullscreen mode

It would be convenient to add constants in App\Http\Controllers\Controller so they can be used for response status.

const RESPONSE_TYPE_SUCCESS = 'success';
const RESPONSE_TYPE_INFO = 'info';
const RESPONSE_TYPE_WARNING = 'warning';
const RESPONSE_TYPE_ERROR = 'error';
Enter fullscreen mode Exit fullscreen mode

Requests/PageRequest.php

<?php  

namespace App\Modules\Page\Requests;  

use App\Models\Page;  
use Illuminate\Foundation\Http\FormRequest;  

class PageRequest extends FormRequest  
{  
    /**  
     * Determine if the user is authorized to make this request.     
     *     
     * @return bool  
     */  
    public function authorize()  
    {  
        return true;  
    }  

    /**  
     * Get the validation rules that apply to the request.     
     *     
     * @return array  
     */  
    public function rules()  
    {  
        return [  
            Page::COLUMN_NAME => 'required|string',  
        ];  
    }  
}
Enter fullscreen mode Exit fullscreen mode

Resources/PageResource.php

<?php  

namespace App\Modules\Page\Resources;  

use Illuminate\Http\Request;  
use Illuminate\Http\Resources\Json\JsonResource;  

class PageResource extends JsonResource  
{  
    /**  
     * Transform the resource into an array.     
     *     
     * @param  Request  $request  
     * @return array  
     */  
    public function toArray($request)  
    {  
        return parent::toArray($request);  
    }  
}
Enter fullscreen mode Exit fullscreen mode

Currently, this class doesn't provide much assistance, but it will set a standard response and unload the controller in the future.

Our Restful API for managing the Page entity is ready.
The list of routes for this module you can find running this command

php artisan route:list --path=pages

If you comment out the line ->middleware(['auth:sanctum']) in the api.php file, you can easily test this functionality using Postman.

💖 💪 🙅 🚩
yurich84
Yurich

Posted on March 17, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related