Laravel is a powerful PHP framework that allows developers to easily build web applications and APIs.
In this guide, we'll walk through the process of creating a CRUD (Create, Read, Update, Delete) API using Laravel, and we'll implement authentication using Laravel Sanctum.
By the end of this tutorial, you'll have a fully functional API that allows authenticated users to perform CRUD operations on a resource. We'll use a 'Task' model as our example resource and implement the necessary endpoints to manage tasks.
Prerequisites
Before we begin, ensure you have the following:
- PHP 8.1 or higher installed on your system
- Composer for managing PHP dependencies
- A Neon account for database hosting
- Basic knowledge of Laravel and RESTful API principles
Setting up the Project
Let's start by creating a new Laravel project and setting up the necessary components.
Creating a New Laravel Project
Open your terminal and run the following command to create a new Laravel project:
composer create-project laravel/laravel laravel-crud-api
cd laravel-crud-api
This will create a new Laravel project in a directory named laravel-crud-api
and install all the necessary dependencies.
Setting up the Database
Update your .env
file with your Neon database credentials:
DB_CONNECTION=pgsql
DB_HOST=your-neon-hostname.neon.tech
DB_PORT=5432
DB_DATABASE=your_database_name
DB_USERNAME=your_username
DB_PASSWORD=your_password
Make sure to replace your-neon-hostname
, your_database_name
, your_username
, and your_password
with your actual database credentials.
Installing Laravel Sanctum
Laravel Sanctum provides a featherweight authentication system for SPAs and simple APIs. To install it, all you need to do is use the following command:
php artisan install:api
If you get asked to run all pending migrations, type yes
and press Enter. Else, run the migrations to create the necessary tables:
php artisan migrate
This will create the necessary tables for Sanctum to work.
Creating the Task Model and Migration
As mentioned earlier, for our CRUD API, we'll use a 'Task' model as our example resource. This model will have fields such as title, description, status, due date, and priority.
Let's create it along with its migration file:
php artisan make:model Task -m
Once created, open the newly created migration file in database/migrations
and update it to include the necessary columns for the 'tasks' table:
public function up()
{
Schema::create('tasks', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->string('title');
$table->text('description')->nullable();
$table->enum('status', ['pending', 'in_progress', 'completed'])->default('pending');
$table->date('due_date')->nullable();
$table->integer('priority')->default(1);
$table->timestamps();
});
}
We've defined a foreign key user_id
to associate each task with a user. This allows us to implement user-specific tasks and ensure that each task belongs to a specific user. The constrained()
method creates a foreign key constraint that references the id
column of the users
table. The onDelete('cascade')
method ensures that when a user is deleted, all associated tasks are also deleted.
Run the migration to create the 'tasks' table:
php artisan migrate
After that, update the app/Models/Task.php
model file to include the necessary fields in the $fillable
array:
<?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', 'status', 'due_date', 'priority'];
public function user()
{
return $this->belongsTo(User::class);
}
}
As a reference, the fillable
property specifies which fields can be mass-assigned when creating or updating a model. This helps protect against mass assignment vulnerabilities and ensures that only the specified fields can be modified.
Implementing API Authentication
Before we create our CRUD endpoints, let's set up authentication using Laravel Sanctum. This will allow users to register, log in, and access protected routes in our API.
Creating the User Registration and Login Controllers
By default, Laravel comes with a few route groups like web
, api
, and auth
. We'll use the api
group for our API routes which will be protected by Sanctum.
Start by creating a controller called AuthController
to handle user registration and login using the artisan command:
php artisan make:controller Api/AuthController
Then, update the app/Http/Controllers/Api/AuthController.php
file and add the necessary methods:
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;
class AuthController extends Controller
{
public function register(Request $request)
{
$request->validate([
'name' => 'required|string|max:255',
'email' => 'required|string|email|max:255|unique:users',
'password' => 'required|string|min:8|confirmed',
]);
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
]);
$token = $user->createToken('auth_token')->plainTextToken;
return response()->json([
'access_token' => $token,
'token_type' => 'Bearer',
]);
}
public function login(Request $request)
{
$request->validate([
'email' => 'required|email',
'password' => 'required',
]);
$user = User::where('email', $request->email)->first();
if (! $user || ! Hash::check($request->password, $user->password)) {
throw ValidationException::withMessages([
'email' => ['The provided credentials are incorrect.'],
]);
}
$token = $user->createToken('auth_token')->plainTextToken;
return response()->json([
'access_token' => $token,
'token_type' => 'Bearer',
]);
}
public function logout(Request $request)
{
$request->user()->currentAccessToken()->delete();
return response()->json(['message' => 'Logged out successfully']);
}
}
Rundown of the methods in the AuthController
:
-
register
: Handles user registration. Validates the request data, creates a new user, and returns an access token. -
login
: Handles user login. Validates the request data, checks the user credentials, and returns an access token. -
logout
: Logs out the authenticated user by deleting the current access token.
Setting up Authentication Routes
Update routes/api.php
to include the authentication routes within the api
route group:
<?php
use App\Http\Controllers\Api\AuthController;
use Illuminate\Support\Facades\Route;
Route::post('/register', [AuthController::class, 'register']);
Route::post('/login', [AuthController::class, 'login']);
Route::middleware('auth:sanctum')->group(function () {
Route::post('/logout', [AuthController::class, 'logout']);
// We'll add our task routes here later
});
The auth:sanctum
middleware protects the routes by requiring a valid access token. This ensures that only authenticated users can access the protected routes.
The /register
and /login
routes are public and do not require authentication. Users can register and log in to obtain an access token.
Issuing API Tokens
To issue API tokens, we need to update the User
model to use the HasApiTokens
trait. Laravel ships with a default User
model located at app/Models/User.php
.
Let's update the app/Models/User.php
file and add the HasApiTokens
trait:
<?php
// Existing imports
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
// Add the HasApiTokens trait here
use HasApiTokens, HasFactory, Notifiable;
// Existing code
}
After adding the HasApiTokens
trait, you can use the createToken
method to generate an access token for a user. We've used this method in the AuthController
to issue tokens during registration and login.
While we're here, let's also update the User
model to include a relationship with the Task
model:
public function tasks()
{
return $this->hasMany(Task::class);
}
This relationship allows us to retrieve all tasks associated with a user and create new tasks for a user, simplifying the task management process.
Testing the Authentication Endpoints
To test the authentication endpoints, you can use a tool like Postman or Insomnia.
For the sake of simplicity, you can use the curl
command in your terminal.
Let's start by testing the registration route and try to register a new user:
curl -X POST http://laravel-crud-api.test/api/register \
-H 'Content-Type: application/json' \
-d '{
"name": "John Doe",
"email": "john@example.com",
"password": "password",
"password_confirmation": "password"
}'
Note: Replace
laravel-crud-api.test
with your Laravel project URL.
The response should include an access token like this:
{
"access_token": "1|eyJ...your_access_token_here",
"token_type": "Bearer"
}
To log in with the registered user:
curl -X POST http://laravel-crud-api.test/api/login \
-H 'Content-Type: application/json' \
-d '{
"email": "john@example.com",
"password": "password"
}'
This will return another access token:
{
"access_token": "1|eyJ...your_new_access_token_here",
"token_type": "Bearer"
}
With the access token, you can now access the protected routes. To log out, use the /logout
route:
curl -X POST http://laravel-crud-api.test/api/logout \
-H 'Authorization: Bearer <your_access_token_here>'
Replace <your_access_token_here>
with the access token you received during login. This will log out the user and delete the access token and you will get a response like:
{
"message": "Logged out successfully"
}
Implementing CRUD Operations
Now that we have authentication set up, let's create our CRUD operations for the Task model.
Creating the TaskController
Generate a new controller for handling task operations:
php artisan make:controller Api/TaskController --api
The --api
flag generates a controller with the necessary methods for a RESTful API. This will create a new controller file at app/Http/Controllers/Api/TaskController.php
and will include methods like index
, store
, show
, update
, and destroy
.
After that, update app/Http/Controllers/Api/TaskController.php
and populate those methods with the necessary logic:
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Task;
use Illuminate\Http\Request;
class TaskController extends Controller
{
public function index()
{
$tasks = Task::all();
return response()->json($tasks);
}
public function store(Request $request)
{
$request->validate([
'title' => 'required|string|max:255',
'description' => 'nullable|string',
'status' => 'required|in:pending,in_progress,completed',
'due_date' => 'nullable|date',
'priority' => 'required|integer|min:1|max:5',
]);
$task = $request->user()->tasks()->create($request->all());
return new TaskResource($task);
}
public function show(Task $task)
{
return response()->json($task);
}
public function update(Request $request, Task $task)
{
$this->authorize('update', $task);
$validated = $request->validate([
'title' => 'sometimes|required|string|max:255',
'description' => 'nullable|string',
'status' => 'sometimes|required|in:pending,in_progress,completed',
'due_date' => 'nullable|date',
'priority' => 'sometimes|required|integer|min:1|max:5',
]);
$task->update($validated);
return new TaskResource($task);
}
public function destroy(Task $task)
{
$task->delete();
return response()->json(null, 204);
}
}
Note: We'll create the
TaskResource
class later to transform the task model into a JSON response.
Rundown of the methods in the TaskController
:
-
index
: Fetches all tasks. Returns a JSON response with all tasks. We'll update this method to use API Resources later. -
store
: Creates a new task. Validates the request data, creates a new task, and returns the task as JSON. -
show
: Fetches a single task. Returns a JSON response with the specified task. -
update
: Updates a task. Validates the request data, updates the task, and returns the updated task as JSON. -
destroy
: Deletes a task. Deletes the specified task and returns a 204 No Content response.
Adding Task Routes
Once we have the TaskController
set up, let's add the task routes to routes/api.php
to include the task routes within the authenticated group:
// Include the TaskController at the top:
use App\Http\Controllers\Api\TaskController;
Route::middleware('auth:sanctum')->group(function () {
Route::post('/logout', [AuthController::class, 'logout']);
// Add the task routes here:
Route::apiResource('tasks', TaskController::class);
});
The Route::apiResource
method automatically generates the necessary routes for a RESTful resource. This will create routes for tasks
with the appropriate HTTP verbs and route names.
You can use the php artisan route:list
command to see a list of all registered routes.
Implementing API Resources
To provide a consistent and customizable way of transforming our models into JSON responses, let's use Laravel's API Resources.
A resource class represents a single model that needs to be transformed into a JSON structure. It allows you to customize the data that is returned when a model is converted to JSON rather than returning the entire model instance.
Generate a new resource for the Task model:
php artisan make:resource TaskResource
Open the app/Http/Resources/TaskResource.php
file and update it as follows:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class TaskResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'title' => $this->title,
'description' => $this->description,
'status' => $this->status,
'due_date' => $this->due_date,
'priority' => $this->priority,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
}
Here, we've defined the fields that should be included in the JSON response for a task and how they should be formatted. For more complex transformations, you can customize the toArray
method as needed.
Now, update the TaskController
to use this resource when returning task data instead of returning the raw model:
<?php
namespace App\Http\Controllers\Api;
use App\Http\Resources\TaskResource;
use App\Http\Controllers\Controller;
use App\Models\Task;
use Illuminate\Http\Request;
class TaskController extends Controller
{
public function index()
{
$tasks = Task::all();
// Change this line to use the TaskResource:
return TaskResource::collection($tasks);
}
public function store(Request $request)
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'description' => 'nullable|string',
'status' => 'required|in:pending,in_progress,completed',
'due_date' => 'nullable|date',
'priority' => 'required|integer|min:1|max:5',
]);
$task = $request->user()->tasks()->create($validated);
return new TaskResource($task);
}
public function show(Task $task)
{
return new TaskResource($task);
}
public function update(Request $request, Task $task)
{
$request->validate([
'title' => 'required|string|max:255',
'description' => 'nullable|string',
'status' => 'required|in:pending,in_progress,completed',
'due_date' => 'nullable|date',
'priority' => 'required|integer|min:1|max:5',
]);
$task->update($request->all());
return new TaskResource($task);
}
public function destroy(Task $task)
{
$task->delete();
return response()->json(null, 204);
}
}
Now, when you fetch tasks, create a new task, or update an existing task, the response will be formatted according to the TaskResource
class.
Testing the API Endpoints
To test the new Task API, you can again use curl
or a tool like Postman or Insomnia.
Let's first create a new task:
curl -X POST http://laravel-crud-api.test/api/tasks \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <your_access_token_here>' \
-d '{
"title": "New Task",
"description": "Task description",
"status": "pending",
"due_date": "2024-12-31",
"priority": 2
}'
As a response, you should see the newly created task:
{
"id": 1,
"title": "New Task",
"description": "Task description",
"status": "pending",
"due_date": "2024-12-31",
"priority": 2,
"created_at": "2024-07-01T00:00:00.000000Z",
"updated_at": "2024-07-01T00:00:00.000000Z"
}
You can then fetch all tasks:
curl -X GET http://laravel-crud-api.test/api/tasks \
-H 'Authorization: Bearer <your_access_token_here>'
This will return a list of tasks in JSON format.
Adding Pagination
To improve performance and reduce payload size, we can add pagination to the task list. Laravel provides a simple way to paginate query results using the paginate
method when fetching data. That way the response will include only a subset of tasks per page instead of the entire collection which can be large depending on the number of entries in the database.
To do that, update the index
method in TaskController
and change the all
method to paginate
followed by the number of items per page:
public function index()
{
$tasks = Task::paginate(15);
return TaskResource::collection($tasks);
}
This pagination method will limit the number of tasks returned to 15 per page, significantly reducing the payload size for large datasets.
The response will now include additional pagination metadata such as the total number of items, the number of pages, and links to the next and previous pages, allowing for easy navigation through the entire collection of tasks.
Implementing Request Classes
Request classes allow you to encapsulate request validation logic within dedicated classes. This helps keep your controller clean and improves reusability.
To keep our controller clean and improve reusability, let's create dedicated request classes for validation for creating and updating tasks:
php artisan make:request StoreTaskRequest
php artisan make:request UpdateTaskRequest
Update app/Http/Requests/StoreTaskRequest.php
to include the validation rules:
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreTaskRequest extends FormRequest
{
public function authorize()
{
return true;
}
public function rules()
{
return [
'title' => 'required|string|max:255',
'description' => 'nullable|string',
'status' => 'required|in:pending,in_progress,completed',
'due_date' => 'nullable|date',
'priority' => 'required|integer|min:1|max:5',
];
}
}
Update app/Http/Requests/UpdateTaskRequest.php
the same way:
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UpdateTaskRequest extends FormRequest
{
public function authorize()
{
return true;
}
public function rules()
{
return [
'title' => 'required|string|max:255',
'description' => 'nullable|string',
'status' => 'required|in:pending,in_progress,completed',
'due_date' => 'nullable|date',
'priority' => 'required|integer|min:1|max:5',
];
}
}
Now, we are ready to update the TaskController
to use these request classes instead of validating the request directly in the controller methods:
// Include the request classes at the top:
use App\Http\Requests\StoreTaskRequest;
use App\Http\Requests\UpdateTaskRequest;
class TaskController extends Controller
{
// ... other methods ...
public function store(StoreTaskRequest $request)
{
$task = $request->user()->tasks()->create($request->validated());
return new TaskResource($task);
}
public function update(UpdateTaskRequest $request, Task $task)
{
$task->update($request->validated());
return new TaskResource($task);
}
// ... other methods ...
}
The end-user will still receive the same JSON response, but the validation logic is now encapsulated within the request classes. This makes the controller cleaner and easier to maintain.
Testing the API
To ensure our API works as expected, Laravel provides a powerful testing suite out of the box.
To learn more about testing in Laravel along with Neon branding, check out the Testing Laravel Applications with Neon's Database Branching.
Adding API Documentation
For better developer experience, it's important to have good API documentation.
You can use a third-party package called Scribe to generate your API documentation.
To install Scribe, run the following command:
composer require --dev knuckleswtf/scribe
Publish the configuration file:
php artisan vendor:publish --tag=scribe-config
Then update the config/scribe.php
file to configure the documentation settings according to your preferences.
Generate the documentation:
php artisan scribe:generate
This will create a public/docs
directory with the generated API documentation. You can access it by visiting http://laravel-crud-api.test/docs
. The generated format will also include Postman collections and OpenAPI specifications.
Implementing API Versioning
As your API evolves, you might need to introduce breaking changes. API versioning allows you to do this without affecting existing clients.
To implement a simple versioning strategy, you can prefix your API routes with a version number. This way, you can maintain backward compatibility while introducing new features in future versions.
To do that, create a new directory for v1 of your API:
mkdir app/Http/Controllers/Api/V1
Move your TaskController.php
to this new directory and update its namespace:
namespace App\Http\Controllers\Api\V1;
Then update your routes/api.php
file to include versioning and the new namespace:
// Update the TaskController import at the top:
use App\Http\Controllers\Api\V1\TaskController;
// Update the routes to include the version prefix:
Route::prefix('v1')->group(function () {
Route::post('/register', [AuthController::class, 'register']);
Route::post('/login', [AuthController::class, 'login']);
Route::middleware('auth:sanctum')->group(function () {
Route::post('/logout', [AuthController::class, 'logout']);
Route::apiResource('tasks', \App\Http\Controllers\Api\V1\TaskController::class);
});
});
Now, your API endpoints will be prefixed with /api/v1
. This allows you to introduce breaking changes in future versions without affecting existing clients.
Later on, you can create a new version (e.g., v2
) and update the routes accordingly to maintain backward compatibility.
Implementing Caching
To improve performance, especially for frequently accessed and rarely changing data, it is a good idea to implement caching for our task list.
This can significantly reduce the response time and server load and reduce the number of database queries putting less pressure on the database.
As an example, let's implement that for the index
method in TaskController
:
// Include the Cache facade at the top:
use Illuminate\Support\Facades\Cache;
public function index()
{
// Use the Cache facade to store the tasks for one hour
$tasks = Cache::remember('tasks', 3600, function () {
return Task::paginate(15);
});
return TaskResource::collection($tasks);
}
This caches the task list for one hour. Remember to clear the cache when tasks are updated, created, or deleted.
Conclusion
In this guide, we've walked through the process of building a simple CRUD API with Laravel, secured with Laravel Sanctum for authentication.
We've covered setting up the project, configuring the database with Neon, and implementing CRUD operations for a Task model. We also added essential features such as API versioning, API documentation, and caching to improve the API's performance, security, and maintainability.
By following these steps, you now have a fully functional API that allows authenticated users to manage tasks effectively. This can be used as the foundation for more complex applications and extended with additional features as needed.
As next steps you can think about adding more features to the API, such as search, filtering, sorting, and more advanced authentication and authorization mechanisms.
Additionally, it is a good idea to implement throttling to protect your API from abuse and to ensure fair usage.