Advanced Backend Integration With Inertia.js, React and Laravel
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
andTaskEdit
.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.
- Task Management:
- Simply enter
http://localhost:8000/tasks
to test the task management route.
2. Weather App:
- Simply enter
http://localhost:8000/weather
to test the weather route.
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 ♥
Subscribe to my newsletter
Read articles from Favour Orukpe directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Favour Orukpe
Favour Orukpe
Favour Orukpe is a Full-Stack Software Engineer proficient in React, React Native, TypeScript, PHP, Laravel, and Django. As a key member of the $mart Earners Team™, Favour focuses on innovative solutions. With a passion for seamless user experiences, Favour excels in both frontend and backend development, contributing to diverse projects with efficiency and creativity.