Crafting a Modular Laravel API for Effective SPA Interaction
Tags: laravel, api, backend, php
Published: 2024-03-17 11:19:17

#### 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](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:
```php

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);  
    }  
}
```

To make the SPA works, you need to add one more route at the end of the `web.php` file.
```php
Route::view('/{any}', 'spa')->where('any', '^(?!api).*'); 
```

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

```php
<!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>
```

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](https://dev.to/yurich84/my-point-of-view-on-spa-modularity-using-laravel-and-vue-52g0), 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
<?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');  
    }  
};
```

**Model**:
```php
<?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];  
}
```

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
<?php  
  
use Illuminate\Support\Facades\Route;  
  
Route::apiResource('pages', 'PageController');
```

**Controllers/PageController.php** 
```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',  
        ]);  
    }  
}
```

It would be convenient to add constants in `App\Http\Controllers\Controller` so they can be used for response status.
```php
const RESPONSE_TYPE_SUCCESS = 'success';
const RESPONSE_TYPE_INFO = 'info';
const RESPONSE_TYPE_WARNING = 'warning';
const RESPONSE_TYPE_ERROR = 'error';
```

**Requests/PageRequest.php**
```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',  
        ];  
    }  
}
```

**Resources/PageResource.php**
```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);  
    }  
}
```
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.