Building a Laravel-React Todo App: A Comprehensive Beginner's Guide
Creating a full-stack application can seem daunting, especially for beginners. However, with the right guidance, it's entirely achievable. In this blog post, we'll walk you through building a Todo App using Laravel for the backend and React for the frontend. This step-by-step guide is designed to be beginner-friendly, ensuring you grasp each concept as you progress.
Link to complete source code : https://github.com/JC-Coder/laravel-react-todo-app
Prerequisites
Before diving into the development process, ensure you have the following installed on your machine:
PHP (version 8.0 or higher)
Composer: Dependency Manager for PHP
Node.js and npm: For managing frontend dependencies
Laravel Installer (optional but recommended)
Git: For version control
For detailed installation guides, refer to the Laravel Documentation and Node.js Installation Guide.
Setting Up the Laravel Backend
1. Install Laravel
First, you'll need to set up a new Laravel project. You can either install a fresh Laravel application or clone the starter repository.
# Create a new Laravel project
composer create-project laravel/laravel todo-app
# Or clone the starter repo
git clone https://github.com/JC-Coder/laravel-react-todo-app.git
cd laravel-react-todo-app
2. Install Sanctum for Authentication
Laravel Sanctum provides a robust authentication system for single-page applications (SPA) and simple APIs.
# Install Sanctum
php artisan install:api
3. Update the User Model
To enable API token management, incorporate the HasApiTokens
trait into your User
model.
<?php
// File: app/Models/User.php
namespace App\Models;
use Laravel\Sanctum\HasApiTokens; // Import HasApiTokens
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable
{
use HasFactory, HasApiTokens, Notifiable;
// Rest of the model...
}
4. Add a Health Route
A health check route ensures your API is operational.
<?php
// File: routes/api.php
use Illuminate\Support\Facades\Route;
Route::get('/health', function () {
return response()->json(['status' => 'API is working'], 200);
});
5. Create Controllers
We'll need two controllers: BaseController
for standardized responses and AuthController
for handling authentication.
# Create BaseController and AuthController
php artisan make:controller BaseController
php artisan make:controller AuthController
6. Implement BaseController
The BaseController
standardizes API responses.
<?php
// File: app/Http/Controllers/BaseController.php
namespace App\Http\Controllers;
use Illuminate\Http\JsonResponse;
class BaseController extends Controller
{
/**
* Send a successful response.
*
* @param mixed $result
* @param string $message
* @return JsonResponse
*/
public function sendResponse($result, $message): JsonResponse
{
return response()->json([
'success' => true,
'data' => $result,
'message' => $message,
], 200);
}
/**
* Send an error response.
*
* @param string $error
* @param array $errorMessages
* @param int $code
* @return JsonResponse
*/
public function sendError($error, $errorMessages = [], $code = 404): JsonResponse
{
$response = [
'success' => false,
'message' => $error,
];
if (!empty($errorMessages)) {
$response['data'] = $errorMessages;
}
return response()->json($response, $code);
}
}
7. Implement AuthController
The AuthController
manages user registration, login, and fetching authenticated user details.
<?php
// File: app/Http/Controllers/AuthController.php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
class AuthController extends BaseController
{
/**
* Register a new user.
*
* @param Request $request
* @return JsonResponse
*/
public function register(Request $request)
{
try {
// Validate incoming request
$validatedData = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|string|email|max:255|unique:users',
'password' => 'required|string|min:8',
]);
// Create new user
$user = User::create([
'name' => $validatedData['name'],
'email' => $validatedData['email'],
'password' => Hash::make($validatedData['password']),
]);
// Generate token
$token = $user->createToken('auth_token')->plainTextToken;
// Respond with user data and token
return $this->sendResponse([
'user' => $user,
'access_token' => $token,
'token_type' => 'Bearer',
], 'User created successfully');
} catch (\Exception $e) {
return $this->sendError($e->getMessage(), [], 422);
}
}
/**
* Login user and create token.
*
* @param Request $request
* @return JsonResponse
*/
public function login(Request $request)
{
try {
// Attempt to authenticate
if (!Auth::attempt($request->only('email', 'password'))) {
return $this->sendError('Invalid login details', [], 401);
}
// Fetch user
$user = User::where('email', $request['email'])->firstOrFail();
// Generate token
$token = $user->createToken('auth_token')->plainTextToken;
// Respond with user data and token
return $this->sendResponse([
'user' => $user,
'access_token' => $token,
'token_type' => 'Bearer',
], 'User logged in successfully');
} catch (\Exception $e) {
return $this->sendError($e->getMessage(), [], 422);
}
}
/**
* Fetch authenticated user.
*
* @param Request $request
* @return JsonResponse
*/
public function user(Request $request)
{
if ($request->user()) {
return $this->sendResponse($request->user(), 'User fetched successfully');
} else {
return response()->json([
'message' => 'User not authenticated',
], 401);
}
}
}
Defining Authentication Routes
Let's define the routes for registration and login in routes/api.php
.
<?php
// File: routes/api.php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\AuthController;
// Authentication Routes
Route::post('/register', [AuthController::class, 'register']);
Route::post('/login', [AuthController::class, 'login']);
Testing Authentication
With the authentication setup in place, it's time to test the API endpoints using tools like Postman or cURL.
Registration Endpoint
URL:
POST
http://localhost:8000/api/register
Body:
{ "name": "John Doe", "email": "johndoe@example.com", "password": "password123" }
Expected Response:
{ "success": true, "data": { "user": { /* User Details */ }, "access_token": "token_here", "token_type": "Bearer" }, "message": "User created successfully" }
Login Endpoint
URL:
POST
http://localhost:8000/api/login
Body:
{ "email": "johndoe@example.com", "password": "password123" }
Expected Response:
{ "success": true, "data": { "user": { /* User Details */ }, "access_token": "token_here", "token_type": "Bearer" }, "message": "User logged in successfully" }
Fetch Authenticated User
URL:
GET
http://localhost:8000/api/me
Headers:
Authorization: Bearer <access_token>
Expected Response:
{ "success": true, "data": { /* Authenticated User Details */ }, "message": "User fetched successfully" }
Implementing Todo Features
Now that authentication is set up, let's implement the core Todo functionalities.
10. Create Migration, Model, and Controller
Generate the necessary files for the Todo feature.
# Create migration for todos table
php artisan make:migration create_todos_table
# Create Todo model
php artisan make:model Todo
# Create TodoController
php artisan make:controller TodoController
11. Update Migration File
Define the structure of the todos
table.
<?php
// File: database/migrations/xxxx_xx_xx_create_todo_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateTodoTable extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('todos', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade'); // References users table
$table->string('title');
$table->text('description')->nullable();
$table->boolean('completed')->default(false);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('todos');
}
}
12. Run Migrations
Apply the migrations to create the todos
table.
php artisan migrate
13. Update Todo Model
Specify the fillable fields in the Todo
model.
<?php
// File: app/Models/Todo.php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Todo extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'title',
'description',
'completed',
'user_id',
];
}
14. Implement TodoController
Handle CRUD operations for todos.
<?php
// File: app/Http/Controllers/TodoController.php
namespace App\Http\Controllers;
use App\Models\Todo;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class TodoController extends BaseController
{
/**
* Retrieve all todos for the authenticated user.
*
* @param Request $request
* @return JsonResponse
*/
public function index(Request $request)
{
$userTodos = Todo::where('user_id', Auth::id())->get();
return $this->sendResponse($userTodos, 'Todos fetched successfully');
}
/**
* Store a new todo.
*
* @param Request $request
* @return JsonResponse
*/
public function store(Request $request)
{
// Validate incoming request
$request->validate([
'title' => 'required|string|max:255',
'description' => 'nullable|string',
'completed' => 'nullable|boolean',
]);
// Create new todo
$todo = Todo::create([
'title' => $request->title,
'description' => $request->description,
'completed' => $request->completed ?? false,
'user_id' => Auth::id(),
]);
return $this->sendResponse($todo, 'Todo created successfully');
}
/**
* Update an existing todo.
*
* @param Request $request
* @param int $id
* @return JsonResponse
*/
public function update(Request $request, $id)
{
// Validate incoming request
$request->validate([
'title' => 'nullable|string|max:255',
'description' => 'nullable|string',
'completed' => 'nullable|boolean',
]);
$userId = Auth::id();
$todo = Todo::where('id', $id)->where('user_id', $userId)->first();
if (!$todo) {
return $this->sendError('Todo not found', [], 404);
}
// Update todo with validated data
$todo->update($request->all());
return $this->sendResponse($todo, 'Todo updated successfully');
}
/**
* Delete a todo.
*
* @param int $id
* @return JsonResponse
*/
public function destroy($id)
{
$userId = Auth::id();
$todo = Todo::where('id', $id)->where('user_id', $userId)->first();
if (!$todo) {
return $this->sendError('Todo not found', [], 404);
}
// Delete the todo
$todo->delete();
return $this->sendResponse([], 'Todo deleted successfully');
}
}
Defining Todo Routes
Define routes for managing todos in routes/api.php
.
<?php
// File: routes/api.php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\TodoController;
// Todo Routes
Route::get('/todos', [TodoController::class, 'index']);
Route::post('/todos', [TodoController::class, 'store']);
Route::put('/todos/{id}', [TodoController::class, 'update']);
Route::delete('/todos/{id}', [TodoController::class, 'destroy']);
Securing Routes with Sanctum Middleware
Ensure that only authenticated users can access certain routes by applying Sanctum's authentication middleware.
<?php
// File: routes/api.php
use Illuminate\Support\Facades\Route;
// Group routes that require authentication
Route::middleware(['auth:sanctum'])->group(function () {
// User Routes
Route::get('/me', [AuthController::class, 'user']);
// Todo Routes
Route::get('/todos', [TodoController::class, 'index']);
Route::post('/todos', [TodoController::class, 'store']);
Route::put('/todos/{id}', [TodoController::class, 'update']);
Route::delete('/todos/{id}', [TodoController::class, 'destroy']);
});
Handling Exceptions
To ensure consistent error responses, handle exceptions globally in app/Exceptions/Handler.php
.
<?php
// File: bootstrap/app.php
// add it inside the block
// ->withExceptions(function (Exceptions $exceptions) {
// })->create();
$exceptions->renderable(function (Exception $e) {
return response()->json([
'message' => 'An error occurred',
'error' => $e->getMessage(),
], 500);
});
Handling Exceptions
Start up your backend server using the php magic wand
php artisan serve
# you should get this message in your terminal if server start
# successfully
# INFO Server running on [http://127.0.0.1:8000].
Setting Up the Frontend
After setting up the backend, clone the frontend repository and configure it.
# Clone the frontend repo
git clone https://github.com/JC-Coder/laravel-react-todo-app.git
cd laravel-react-todo-app/client
# Install dependencies
npm install
# Configure Base URL
# Open the axios config file located at : src/api/axios.js
# and set the API base URL to your server URL,
# typically http://localhost:8000/api
npm start
Note: Ensure that the Laravel backend is running to allow the frontend to communicate with the API.
Conclusion
Congratulations! You've successfully built a full-stack Todo application using Laravel and React. This project not only demonstrates essential CRUD operations but also integrates user authentication using Laravel Sanctum. By following this guide, you've gained valuable insights into setting up a robust backend, managing API routes, handling authentication, and securing your application.
Feel free to explore the GitHub repository for the complete source code and further enhancements.
Resources
Laravel Documentation: https://laravel.com/docs
Laravel Sanctum: https://laravel.com/docs/sanctum
React Documentation: https://reactjs.org/docs/getting-started.html
Tailwind CSS: https://tailwindcss.com/docs
Postman API Testing: https://www.postman.com/
Feel free to reach out or contribute to the repository for any enhancements or queries!
Subscribe to my newsletter
Read articles from Joseph Chimezie directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Joseph Chimezie
Joseph Chimezie
Hi, I'm Joseph, a backend engineer with expertise in JavaScript, TypeScript, Node.js, Express.js, and Nest.js. I specialize in developing scalable backend solutions using databases like MongoDB, PostgreSQL, and MySQL. I have extensive experience in designing efficient database schemas, optimizing query performance, and ensuring data integrity. I excel in building RESTful APIs with a focus on authentication, authorization, and data validation. I'm skilled in Docker for containerization, enabling seamless deployment and scalability. I've worked on various projects, from startups to enterprise-level applications, collaborating with cross-functional teams to deliver high-quality software solutions. I stay updated with the latest backend technologies and enjoy tackling complex challenges. If you're looking for a dedicated backend engineer who can contribute to your team's success, feel free to reach out. Let's connect and discuss how we can create exceptional backend solutions together. Personal interests: In my free time, I enjoy listening to music and am always looking for ways to apply my technical skills to personal projects. I am also an active member of the Google Developers Group (GDG) Uyo Community.