Advanced Backend Integration With Inertia.js, React and Laravel

Crafting a Task Management System


In this comprehensive guide, we’ll embark on a journey to build a sophisticated task management system and a weather application using Laravel as the backend, React as the frontend, and Inertia.js as the bridge between them. This article will guide you through advanced Laravel features, robust authentication and authorization, and seamless API integration, all brought to life with real code examples.

The Concept: A Dynamic Task Management Application

Our application will allow users to create, edit, and delete tasks. Admins will have additional privileges, like viewing all user tasks. We’ll also integrate a weather API to display the weather forecast on the application, adding a unique feature to our task management system.

Laravel as a Powerful Backend

Laravel excels in handling complex server-side operations. We’ll leverage its capabilities to manage tasks, process data, and ensure secure operations.

Task CRUD Operations

Every task management system requires basic CRUD (Create, Read, Update, Delete) operations. Laravel makes this straightforward.

Task Model & Migration: First, we create the Task model and its associated migration file with the following command:

php artisan make:model Task -m

In our User model, enter the following:

// existing code in the user model

/**
     * Get the tasks associated with the user.
     */
    public function tasks()
    {
        return $this->hasMany(Task::class);
    }

In your User model, the tasks() method is used to define a one-to-many relationship between User and Task. This method establishes that a user can have many tasks.

In our Task model, enter the following:

<?php

namespace App\Models;

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

class Task extends Model
{
    use HasFactory;

    protected $fillable = ['title', 'description', 'user_id'];

    /**
     * Get the user that owns the task.
     */
    public function user()
    {
        return $this->belongsTo(User::class);
    }
}

In our Task model, the user() method uses the belongsTo relationship method to indicate that each task belongs to a user.

Modifying the Migration File:

In the migration file generated by Laravel (located in database/migrations/), we define the structure of our tasks table:

<?php

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

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('tasks', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->text('description')->nullable();
            $table->foreignId('user_id')->constrained()->onDelete('cascade');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('tasks');
    }
};

Here, we have a title for the task, an optional description, and a user_id that references the users table, establishing a relationship between a task and a user.

Running Migrations:

After defining the migration, run it to create the tasks table in your database:

php artisan migrate

Crafting the TaskController for CRUD Operations

Creating the TaskController: Generate the controller using the Artisan command:

php artisan make:controller TaskController

Implementing CRUD Methods in TaskController:

In TaskController, we'll implement methods to handle various task-related operations.

<?php

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use App\Models\Task;
use Illuminate\Http\Request;
use Inertia\Inertia;

class TaskController extends Controller
{
    public function index()
    {
        if (auth()->check()) {
            $tasks = auth()->user()->tasks;
            $userId = auth()->user()->id;
            return Inertia::render('Tasks', [
                'tasks' => $tasks,
                'userId' => $userId
            ]);
        }

        return response()->json(['message' => 'Not authenticated'], 401);
    }

    // Store a new task
    public function store(Request $request)
    {
        try {
            $validatedData = $request->validate([
                'title' => 'required|max:255',
                'description' => 'nullable',
                'user_id' => 'required|exists:users,id'
            ]);

            // Create a new task with the validated data
            Task::create($validatedData);

            return to_route('tasks.index');
        } catch (\Illuminate\Validation\ValidationException $e) {
            return to_route(
                'tasks.index',
                [
                    'message' => 'Task not created'
                ]
            );
        }
    }

    // Update the specified task
    public function update(Request $request, Task $task)
    {
        $validatedData = $request->validate([
            'title' => 'required|max:255',
            'description' => 'nullable',
        ]);

        $task->update($validatedData);

        return to_route('tasks.index');
    }

    // Remove the specified task
    public function destroy(Task $task)
    {
        $task->delete();
        return to_route('tasks.index', [
            'message' => 'Task deleted successfully'
        ]);
    }
}

In this controller, we’ve defined methods for:

  • Listing all tasks for the logged-in user.

  • Storing a new task.

  • Updating an existing task.

  • Deleting a task.


Authentication and Authorization

Laravel and Inertia.js provide robust solutions for authentication and role-based access control.

Implementing Authentication

Using Laravel’s built-in authentication, we’ll ensure secure user login.

User Authentication: Laravel Breeze or Laravel Jetstream simplifies this process. Install Breeze and set up the authentication scaffolding:

composer require laravel/breeze --dev
php artisan breeze:install
npm install
npm run dev

Implementing Role-Based Access Control (RBAC)

For our task management system, we’ll differentiate between regular users and administrators.

Add a Role Field to Users Table:

Before creating the middleware, you need to add a role field to your users table to distinguish between different user types.

Create a new migration:

php artisan make:migration add_role_to_users_table --table=users

In the migration file, add the role field:

<?php

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

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::table('users', function (Blueprint $table) {
            $table->string('role')->default('user'); // Default role is 'user'
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::table('users', function (Blueprint $table) {
            $table->dropColumn('role');
        });
    }
};

Use Laravel’s seeding mechanism to create your first admin user.

This is a more automated approach and is especially useful for development environments:

This is important as our application would find it difficult to start up properly without an admin user set up.

Generate a new seeder file:

php artisan make:seeder AdminUserSeeder

Open the generated seeder file in database/seeders/AdminUserSeeder.php and add the logic to create an admin user:

<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;
use App\Models\User;

class AdminUserSeeder extends Seeder
{
    public function run()
    {
        User::create([
            'name' => 'Admin User',
            'email' => 'admin@gmail.com',
            'password' => bcrypt('12345'), // Replace with a secure password
            'role' => 'admin',
        ]);
    }
}

Before running our migration, seeders need to be called to run on our database, so lets edit our database/seeders/DatabaseSeeder.php to call our AdminUserSeeder class:

<?php

namespace Database\Seeders;

// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     */
    public function run(): void
    {
        // \App\Models\User::factory(10)->create();

        // \App\Models\User::factory()->create([
        //     'name' => 'Test User',
        //     'email' => 'test@example.com',
        // ]);
        $this->call([
            AdminUserSeeder::class,
            // other seeders...
        ]);
    }
}

Now we run the migration with the seed option flag:

php artisan migrate --seed

Creating Middleware for Admin Role:

Generate a new middleware to check if a user is an admin:

php artisan make:middleware EnsureUserIsAdmin

In the generated middleware (app/Http/Middleware/EnsureUserIsAdmin.php), add the role check logic:

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class EnsureUserIsAdmin
{
    /**
     * Handle an incoming request.
     *
     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
     */
    public function handle(Request $request, Closure $next): Response
    {
        if (auth()->check() && auth()->user()->role === 'admin') {
            return $next($request);
        }

        return redirect('/')->with('error', 'You do not have access to this resource.');
    }
}

Registering the Middleware:

Register your new middleware in app/Http/Kernel.php within the $routeMiddleware array:

// other protected route or middleware groups

/**
     * The application's route custom middleware.
     *
     * These middleware may be assigned to groups or used individually.
     *
     * @var array<string, class-string|string>
     */
    protected $routeMiddleware = [
        // ... existing route middleware ...

        'admin' => \App\Http\Middleware\EnsureUserIsAdmin::class, // Your custom middleware
    ];

Using the Middleware in Routes:

Apply the middleware to any routes or route groups that should be restricted to admin users:

Route::middleware(['auth', 'admin'])->group(function () {
    // Admin routes here
});

Setting Up Routes

Finally, you’ll need to set up routes in routes/api.php or routes/web.php to handle requests for these operations. Preferably using routes/web.php is best as we are rendering web content with Inertia.js:

use App\Http\Controllers\TaskController;

Route::middleware(['auth', 'admin'])->group(function () {
    // Admin routes here
    Route::get('/tasks', [TaskController::class, 'index'])->name('tasks.index');
    Route::post('/tasks', [TaskController::class, 'store'])->name('tasks.store');
    Route::put('/tasks/{task}', [TaskController::class, 'update'])->name('tasks.update');
    Route::delete('/tasks/{task}', [TaskController::class, 'destroy'])->name('tasks.destroy');
});

This sets up a full suite of CRUD routes for your tasks, mapping to the methods in TaskController.


React Frontend with Inertia.js

Inertia.js allows us to build a seamless frontend using React.

Our application will include components for listing, creating, and editing tasks.

Task Listing Component (TaskIndex)

TaskIndex displays a list of tasks. Each task shows a title, description, and a delete button to remove the task.

// TaskIndex.tsx

import { Task } from '@/Pages/Tasks';
import { useForm } from '@inertiajs/react';
import React from 'react';

interface TaskIndexProps {
    tasks: Task[];
}

const TaskIndex: React.FC<TaskIndexProps> = ({ tasks }) => {

    // console.log(param);
    const formMethods = useForm();

    const onDelete = (id: number) => {
        // Confirm before deleting
        if (window.confirm('Are you sure you want to delete this task?')) {
            formMethods.delete(route('tasks.destroy', id), {
                onSuccess: () => {
                    // Handle success response, e.g., show a success message or refresh data
                    console.log("Task deleted successfully!")
                },
                onError: () => {
                    // Handle error response, e.g., show an error message
                    console.error('An error occurred while deleting the task.');
                }
            });
        }
    };

    return (
        <div className="container p-4 mx-auto">
            {tasks.map(task => (
                <div key={task.id} className="pb-4 mb-4 border-b border-gray-200">
                    <h2 className="text-xl font-semibold">{task.title}</h2>
                    <p className="mb-2 text-gray-600">{task.description}</p>
                    <button
                        onClick={() => onDelete(task.id)}
                        className="px-4 py-2 font-bold text-white bg-red-500 rounded hover:bg-red-700"
                    >
                        Delete
                    </button>
                </div>
            ))}
        </div>
    );
};

export default TaskIndex;

Key Features:

  • Maps over the tasks array to display each task.

  • Includes a delete button for each task, which calls the onDelete function.

Creating New Tasks (TaskCreate)

TaskCreate allows users to add new tasks. It features input fields for the task's title and description, and a submit button.

// TaskCreate.tsx

import { useForm, usePage } from '@inertiajs/react';
import React from 'react';

const TaskCreate: React.FC = () => {
    const {userId, /* errors */} = usePage().props;
    // console.log(userId);
    // console.log(errors);

    const { data, setData, post } = useForm({
        title: '',
        description: '',
        user_id: userId || '',
    });

    const handleAddTask = () => {
        post(route('tasks.store', data));
        setData({ title: '', description: '', user_id: '' });
    };

    return (
        <div className="max-w-md p-6 my-8 bg-white rounded-lg shadow-md">
            <h2 className="mb-4 text-lg font-semibold">Add New Task</h2>
            <div className="mb-4">
                <label htmlFor="title" className="block text-sm font-medium text-gray-700">Title</label>
                <input
                    id="title"
                    type="text"
                    value={data.title}
                    onChange={(e) => setData({ ...data, title: e.target.value })}
                    className="block w-full mt-1 border border-gray-300 rounded-md shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
                />
            </div>
            <div className="mb-4">
                <label htmlFor="description" className="block text-sm font-medium text-gray-700">Description</label>
                <textarea
                    id="description"
                    value={data.description}
                    onChange={(e) => setData({ ...data, description: e.target.value })}
                    className="block w-full mt-1 border border-gray-300 rounded-md shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
                />
            </div>
            <button
                onClick={handleAddTask}
                className="w-full px-4 py-2 font-bold text-white bg-indigo-600 rounded hover:bg-indigo-700 focus:outline-none focus:shadow-outline"
            >
                Add Task
            </button>
        </div>
    );
};

export default TaskCreate;

Key Features:

  • Uses the useForm hook from Inertia.js to manage form data.

  • Submits the form data to the server when the add button is clicked.

Editing Tasks (TaskEdit)

TaskEdit enables users to select a task from a dropdown and edit its title and description.

// TaskEdit.tsx

import React, { useState, useEffect } from 'react';
import { Task } from '@/Pages/Tasks';
import { useForm } from '@inertiajs/react';

interface TaskEditProps {
    tasks: Task[];
}

const TaskEdit: React.FC<TaskEditProps> = ({ tasks }) => {
    // State to hold the currently selected task ID
    const [selectedTaskId, setSelectedTaskId] = useState<number | null>(tasks[0]?.id || null);

    const { data, setData, put } = useForm({
        title: '',
        description: '',
    });

    // Update form data when the selected task ID changes
    useEffect(() => {
        const selectedTask = tasks.find(task => task.id === selectedTaskId);
        if (selectedTask) {
            setData({
                title: selectedTask.title,
                description: selectedTask.description,
            });
        }
    }, [selectedTaskId, tasks]);

    // Handle the update task action
    const handleUpdateTask = () => {
        if (selectedTaskId) {
            put(route('tasks.update', selectedTaskId));
        }
    };

    return (
        <div className="max-w-md p-6 my-8 bg-white rounded-lg shadow-md">
            <h2 className="mb-4 text-lg font-semibold">Edit Task</h2>

            <div className="mb-4">
                <label htmlFor="taskSelect" className="block text-sm font-medium text-gray-700">
                    Select Task
                </label>
                <select
                    id="taskSelect"
                    value={selectedTaskId || ''}
                    onChange={(e) => setSelectedTaskId(Number(e.target.value))}
                    className="block w-full mt-1 border border-gray-300 rounded-md shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
                >
                    {tasks.map((task) => (
                        <option key={task.id} value={task.id}>
                            {task.title}
                        </option>
                    ))}
                </select>
            </div>

            <div className="mb-4">
                <label htmlFor="title" className="block text-sm font-medium text-gray-700">
                    Title
                </label>
                <input
                    id="title"
                    type="text"
                    value={data.title}
                    onChange={(e) => setData({ ...data, title: e.target.value })}
                    className="block w-full mt-1 border border-gray-300 rounded-md shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
                />
            </div>

            <div className="mb-4">
                <label htmlFor="description" className="block text-sm font-medium text-gray-700">
                    Description
                </label>
                <textarea
                    id="description"
                    value={data.description}
                    onChange={(e) => setData({ ...data, description: e.target.value })}
                    className="block w-full mt-1 border border-gray-300 rounded-md shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
                />
            </div>

            <button
                onClick={handleUpdateTask}
                className="w-full px-4 py-2 font-bold text-white bg-indigo-600 rounded hover:bg-indigo-700 focus:outline-none focus:shadow-outline"
            >
                Update Task
            </button>
        </div>
    );
};

export default TaskEdit;

Key Features:

  • Dropdown to select a task to edit.

  • useEffect hook updates form data when the selected task changes.

  • Submits updated data to the server.

The Main Page Component (Tasks)

The Tasks page component integrates TaskIndex, TaskCreate, and TaskEdit components.

// Tasks.tsx Page
import TaskCreate from '@/Components/Tasks/TaskCreate';
import TaskEdit from '@/Components/Tasks/TaskEdit';
import TaskIndex from '@/Components/Tasks/TaskIndex';
import { usePage } from '@inertiajs/react'

export interface Task {
    id: number;
    title: string;
    description: string;
}

const Tasks = () => {
    const { tasks } = usePage<{ tasks: Task[] }>().props;
    // console.log(tasks);
    return (
        <div className="container p-4 mx-auto">
            <div className="mb-6">
                <h2 className="mb-3 text-lg font-semibold">Tasks List</h2>
                <div className="mb-6 border-b-2 border-gray-200"></div> {/* Divider */}
                <TaskIndex tasks={tasks} />
            </div>

            <div className="mb-6">
                <h2 className="mb-3 text-lg font-semibold">Create New Task</h2>
                <div className="mb-6 border-b-2 border-gray-200"></div> {/* Divider */}
                <TaskCreate />
            </div>

            {tasks.length > 0 && (
                <div>
                    <h2 className="mb-3 text-lg font-semibold">Edit Task</h2>
                    <div className="mb-6 border-b-2 border-gray-200"></div> {/* Divider */}
                    <TaskEdit tasks={tasks} />
                </div>
            )}
        </div>
    )
}

export default Tasks

Key Features:

  • Fetches and passes tasks to TaskIndex and TaskEdit.

  • Renders TaskCreate to add new tasks.

  • Styling with Tailwind CSS for a clean UI.

This setup demonstrates the power of Inertia.js in creating seamless user experiences with React in a Laravel environment. By breaking down the application into distinct components, we maintain clean code organization and efficient functionality, making our task management application both user-friendly and scalable.


API Integration and Microservices

Integrating external APIs adds significant value to our application. We’ll use a weather API for a daily forecast.

Integrating Weather API: Create a route and controller method to fetch weather data.

Weather Controller:

php artisan make:controller WeatherController

In WeatherController, add an index() method to view the Weather Component and a getWeather() method to call the weather API:

<?php

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use Inertia\Inertia;

class WeatherController extends Controller
{
    public function index() {
        return Inertia::render('Weather');
    }

    public function getWeather(Request $request)
    {
        $location = $request->input('location', 'Lagos Nigeria'); // Default to 'Lagos Nigeria' if not provided

        $response = Http::get('https://api.weatherapi.com/v1/current.json', [
            'key' => env('WEATHERAPI_KEY'),
            'q' => $location
        ]);

        return Inertia::render('Weather', ['weatherData' => $response->json()]);
    }
}
  • $request->input('location', 'Lagos Nigeria') retrieves the 'location' parameter from the incoming HTTP request.

  • If the ‘location’ parameter is not provided in the request, it defaults to 'Lagos Nigeria'. This default value is a fallback to ensure that the function always has a location to work with.

  • Inertia::render('Weather', ['weatherData' => $response->json()]) sends the response back to the client-side.

  • We used the env("WEATHER_API") to get the user weather_api key from the env file, which looks like this:

WEATHERAPI_KEY=<Your_API_Key>

The .env file is used to store environment-specific variables. This includes sensitive information like API keys, database credentials, and other configurations that may vary between development, staging, and production environments.

You can get your own API key directly from https://www.weatherapi.com/, sign up, and you’ll find your key right there at the top of your dashboard page.

Replace <Your API Key> with the actual key provided by the weather service API you are using.

Setting Up Our Weather Component:

Now, let’s create a component for displaying weather information.

// Weather.tsx Page
import { useForm, usePage } from '@inertiajs/react';

interface WeatherData {
    location: {
        name: string;
        region: string;
        country: string;
        lat: number;
        lon: number;
        tz_id: string;
        localtime_epoch: number;
        localtime: string;
    };
    current: {
        last_updated_epoch: number;
        last_updated: string;
        temp_c: number;
        temp_f: number;
        is_day: number;
        condition: {
            text: string;
            icon: string;
            code: number;
        };
        wind_mph: number;
        wind_kph: number;
        wind_degree: number;
        wind_dir: string;
        pressure_mb: number;
        pressure_in: number;
        precip_mm: number;
        precip_in: number;
        humidity: number;
        cloud: number;
        feelslike_c: number;
        feelslike_f: number;
        vis_km: number;
        vis_miles: number;
        uv: number;
        gust_mph: number;
        gust_kph: number;
    };
}

interface WeatherPageProps {
    [key: string]: any;
    weatherData: WeatherData;
}

const Weather: React.FC = () => {

    const { props } = usePage<WeatherPageProps>();
    const { weatherData } = props;

    // console.log(weatherData);

    const { get, data, setData, reset } = useForm({
        location: ""
    })

    const getWeatherInfo = () => {
        get(route('get-weather-info', data));
        reset();
    };

    return (
        <div className="container grid p-4 mx-auto">
            <div className="flex flex-col items-center justify-center space-y-4">
                <input
                    type="text"
                    value={data.location}
                    onChange={(e) => setData({ location: e.target.value })}
                    className="px-4 py-2 border border-gray-300 rounded shadow-sm focus:outline-none focus:border-indigo-500"
                    placeholder='Enter location...'
                />
                <button
                    onClick={getWeatherInfo}
                    className="px-4 py-2 text-white bg-blue-500 rounded hover:bg-blue-600 focus:outline-none focus:shadow-outline"
                >
                    Get Weather
                </button>

                <div className='py-3'>
                    <p className="text-gray-500 divide-y divide-gray-800">Enter a valid location and click the button to load weather data.</p>
                </div>

                {weatherData.location?.name !== undefined && (
                    <div className='grid gap-1 py-3'>
                        <h3 className="text-xl font-semibold underline underline-offset-2">{weatherData.location?.name}</h3>
                        <p className="text-gray-700">Weather condition: {weatherData.current?.condition.text}</p>
                        <div className="grid grid-cols-2 gap-4">
                            <p>Region: {weatherData.location?.region}</p>
                            <p>Country: {weatherData.location?.country}</p>
                            <p>Latitude: {weatherData.location?.lat}</p>
                            <p>Longitude: {weatherData.location?.lon}</p>
                            <p>Local Time: {weatherData.location?.localtime}</p>
                            <p>Temperature: {weatherData.current?.temp_c}°C / {weatherData.current?.temp_f}°F</p>
                            <p>Wind: {weatherData.current?.wind_kph} kph ({weatherData.current?.wind_dir})</p>
                            <p>Pressure: {weatherData.current?.pressure_mb} mb</p>
                            <p>Humidity: {weatherData.current?.humidity}%</p>
                            <p>Precipitation: {weatherData.current?.precip_mm} mm</p>
                            <p>Visibility: {weatherData.current?.vis_km} km</p>
                            <p>UV Index: {weatherData.current?.uv}</p>
                            <p>Feels Like: {weatherData.current?.feelslike_c}°C / {weatherData.current?.feelslike_f}°F</p>
                        </div>
                    </div>
                )}
            </div>
        </div>
    );
};

export default Weather;

Now, we add the routes in our routes/web.php file:

use App\Http\Controllers\WeatherController;

Route::controller(WeatherController::class)->group(function () {
    Route::get('/weather', 'index')->name('weather.index');
    Route::get('/weather', 'getWeather')->name('get-weather-info');
});

Testing Our Entire Application:

To run our Laravel server and test the routes we’ve defined so far, let’s follow these steps:

Running the Laravel Server

Starting the Laravel Server:

  • Open your terminal or command prompt.

  • Navigate to the root directory of your Laravel project.

  • Run the command php artisan serve.

  • This command will start a development server at http://localhost:8000 by default.

Accessing Your React Application Via Inertia.js:

  • Run the command npm run dev.

  • You should see your React application running live with the Laravel Server.

Testing Your Routes

Testing routes in Laravel using a browser.

  1. Task Management:

2. Weather App:


Conclusion

Pheww! We’re done!

Through this task management system and weather API integration, we’ve demonstrated the prowess of Laravel as a backend, handling complex operations and security with finesse. Inertia.js, in harmony with React, provides an intuitive user interface, enhancing the overall user experience. This application is not just functional but also scalable, setting the stage for integrating more advanced features and external services. With this foundation, you’re well-equipped to take on more complex projects, pushing the boundaries of modern web development.

Happy coding! 🚀👨‍💻👩‍💻

Preview Source Code