Build a Complete REST API in Laravel 12 with Sanctum Authentication
REST (Representational State Transfer) is an architectural style that defines a set of constraints for building web services. A REST API allows client applications — such as mobile apps, frontend frameworks like React or Vue, and third-party services — to communicate with your server using standard HTTP methods: GET, POST, PUT, and DELETE.Laravel has long been a favorite PHP framework for API development, and Laravel 12 raises the bar further. With improvements in route handling performance, tighter security defaults, and a refined developer experience, building APIs in Laravel 12 is faster and more enjoyable than ever. In this tutorial, we will build a complete task management API from scratch, covering database design, RESTful routing, controller logic, request validation, and token-based authentication using Sanctum.

Table Of Content
1 Prerequisites for Building Your Laravel 12 REST API
- PHP 8.2 or higher — Laravel 12 requires PHP 8.2 as a minimum. Run
php -vin your terminal to check your version. - Composer 2.x — The PHP dependency manager. Install it from getcomposer.org if you haven't already.
- MySQL 8.0+ or PostgreSQL 13+ — Any of these databases will work. This tutorial uses MySQL.
- Postman or cURL — For testing your API endpoints. Download Postman for a GUI-based testing experience.
- A code editor — VS Code, PHPStorm, or Sublime Text are recommended.
If you are upgrading from an older Laravel version, check out our guide on How to Upgrade from Laravel 11 to Laravel 12 Without Breaking Changes before starting a new project.
2 Introduction
Here is what the finished API will support:
- POST /api/register — Register a new user and receive an API token
- POST /api/login — Authenticate and get a Bearer token
- GET /api/tasks — List all tasks (authenticated)
- POST /api/tasks — Create a new task (authenticated)
- GET /api/tasks/{id} — View a single task (authenticated)
- PUT /api/tasks/{id} — Update a task (authenticated)
- DELETE /api/tasks/{id} — Delete a task (authenticated)
Each step includes complete, copy-paste-ready code. Let's get started!
3 Create a Fresh Laravel 12 Project
If you want to containerize your Laravel setup, you can also follow our Docker Compose Setup for Laravel & MySQL guide as an alternative approach.
3.1 Install Laravel Project
composer create-project laravel/laravel:^12.0 laravel-rest-api
Navigate to your project directory:
cd laravel-rest-api
3.2 Configure Your Database (.env)
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel_rest_api
DB_USERNAME=root
DB_PASSWORD=
4 Setting Up the Database and Model
Generate the migration using Artisan:
php artisan make:migration create_tasks_table
Open the newly created migration file inside database/migrations/ and define the table schema:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('tasks', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('description')->nullable();
$table->boolean('completed')->default(false);
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('tasks');
}
};
The up() method creates the table with an auto-incrementing ID, a title, an optional description, a completed flag that defaults to false, and automatic created_at/updated_at timestamps. The down() method reverses the migration by dropping the table.Run the migration to create the table in your database:
php artisan migrate
Next, generate the Eloquent model for the Task:
php artisan make:model Task
Edit app/Models/Task.php and define the $fillable array. This property specifies which columns can be mass-assigned when using methods like Task::create():
<?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', 'completed'];
}
Without the $fillable property, Laravel will throw a MassAssignmentException when you try to create or update records using request data. This is a security feature that prevents unintended column modifications.For deeper Eloquent tips, check out our article on Laravel 12 Eloquent Tips and Tricks for Beginners.
5 Creating the API Routes
/api. Open the file and define your resource routes:
<?php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\TaskController;
use App\Http\Controllers\AuthController;
// Public routes
Route::post('/register', [AuthController::class, 'register']);
Route::post('/login', [AuthController::class, 'login']);
// Protected routes
Route::middleware('auth:sanctum')->group(function () {
Route::apiResource('tasks', TaskController::class);
});
The apiResource method automatically generates the following routes for your TaskController:
- GET /api/tasks →
index()— List all tasks - POST /api/tasks →
store()— Create a new task - GET /api/tasks/{task} →
show()— Display a specific task - PUT /api/tasks/{task} →
update()— Update a specific task - DELETE /api/tasks/{task} →
destroy()— Delete a specific task
The
auth:sanctum middleware ensures only authenticated users with a valid token can access the task endpoints. The register and login routes remain public so new users can obtain tokens.To learn more about route definitions and middleware groups, read our detailed guide on Laravel 12 Routing and Middleware.
6 Building the Controller
--api flag that generates a controller with methods for all standard RESTful actions, minus the create and edit methods (which are only needed for web forms).Generate the resource controller:
php artisan make:controller TaskController --api
Now edit app/Http/Controllers/TaskController.php with the full CRUD logic:
<?php
namespace App\Http\Controllers;
use App\Models\Task;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
class TaskController extends Controller
{
// GET /api/tasks — Return all tasks
public function index(): JsonResponse
{
$tasks = Task::all();
return response()->json($tasks);
}
// POST /api/tasks — Create a new task
public function store(Request $request): JsonResponse
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'description' => 'nullable|string',
'completed' => 'boolean',
]);
$task = Task::create($validated);
return response()->json($task, 201);
}
// GET /api/tasks/{task} — Show a single task
public function show(Task $task): JsonResponse
{
return response()->json($task);
}
// PUT/PATCH /api/tasks/{task} — Update a task
public function update(Request $request, Task $task): JsonResponse
{
$validated = $request->validate([
'title' => 'sometimes|required|string|max:255',
'description' => 'nullable|string',
'completed' => 'boolean',
]);
$task->update($validated);
return response()->json($task);
}
// DELETE /api/tasks/{task} — Delete a task
public function destroy(Task $task): JsonResponse
{
$task->delete();
return response()->json(null, 204);
}
}
Key points about this controller:
- Route Model Binding — Laravel automatically injects the
Taskmodel instance when you type-hintTask $taskin the method signature. If the task ID does not exist, Laravel returns a 404 response automatically. - Validation — The
$request->validate()method returns only the validated fields. If validation fails, Laravel sends a 422 JSON response with error details. - HTTP Status Codes — We return
201 Createdfor new resources and204 No Contentfor successful deletions, following REST conventions. - No closing
?>tag — PHP files containing only PHP code should omit the closing tag to prevent accidental output.
7 Adding Authentication with Sanctum
Step 1: Publish the Sanctum configuration and run migrations (if not already done):
php artisan install:api
This command publishes the Sanctum configuration, creates the personal_access_tokens migration, and adds the API route file if it doesn't exist.Step 2: Configure Sanctum middleware. In Laravel 12, middleware is registered in bootstrap/app.php (not
Kernel.php, which was removed in Laravel 11):
<?php
// bootstrap/app.php
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
api: __DIR__.'/../routes/api.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware) {
$middleware->api(prepend: [
\Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
]);
})
->withExceptions(function (Exceptions $exceptions) {
//
})->create();
Step 3: Create the AuthController to handle registration and login:
php artisan make:controller AuthController
Step 4: Implement the authentication logic in app/Http/Controllers/AuthController.php:
<?php
namespace App\Http\Controllers;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Hash;
class AuthController extends Controller
{
public function register(Request $request): JsonResponse
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|string|email|unique:users',
'password' => 'required|string|min:8|confirmed',
]);
$user = User::create([
'name' => $validated['name'],
'email' => $validated['email'],
'password' => Hash::make($validated['password']),
]);
$token = $user->createToken('auth_token')->plainTextToken;
return response()->json([
'message' => 'Registration successful',
'access_token' => $token,
'token_type' => 'Bearer',
], 201);
}
public function login(Request $request): JsonResponse
{
$validated = $request->validate([
'email' => 'required|string|email',
'password' => 'required|string',
]);
if (!auth()->attempt($validated)) {
return response()->json([
'message' => 'The provided credentials are incorrect.'
], 401);
}
$token = auth()->user()->createToken('auth_token')->plainTextToken;
return response()->json([
'message' => 'Login successful',
'access_token' => $token,
'token_type' => 'Bearer',
]);
}
}
Step 5: Ensure the User model uses the HasApiTokens trait. Open app/Models/User.php:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
use HasApiTokens, HasFactory, Notifiable;
protected $fillable = ['name', 'email', 'password'];
protected $hidden = ['password', 'remember_token'];
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
}
}
Your task routes are already protected with auth:sanctum middleware from the routes we defined earlier. Any request without a valid Bearer token will receive a 401 Unauthorized response.For a deeper comparison of authentication approaches in Laravel 12, see our guide on Best Authentication Options in Laravel 12.
Related Laravel 12 Tutorials
- Laravel 12 Routing and Middleware: Complete Guide — Master route groups, named routes, middleware parameters, and rate limiting in Laravel 12.
- Laravel 12 Eloquent Tips and Tricks for Beginners — Learn advanced query techniques, relationships, scopes, and accessors to write cleaner database code.
- Best Authentication Options in Laravel 12 Using WorkOS AuthKit — Compare Sanctum, Passport, Breeze, and WorkOS AuthKit for different authentication scenarios.
- Laravel 12 Queue and Jobs Tutorial — Offload heavy processing to background jobs for faster API responses.
- Laravel 12 Performance Optimization Tips — Speed up your API with caching, eager loading, query optimization, and deployment best practices.
- Common Laravel 12 Errors and How to Fix Them — Quick solutions to the most frequent issues developers face with Laravel 12.
- Upgrade from Laravel 11 to 12 Without Breaking Changes — A safe migration path if you are moving an existing project to Laravel 12.
- Docker Compose for Laravel & MySQL — Containerize your Laravel API for consistent development and deployment environments.
8 Testing Your REST API
php artisan serve
Your API is now running at http://127.0.0.1:8000. Open Postman (or use cURL) and test each endpoint:1. Register a new user:
POST http://127.0.0.1:8000/api/register
Content-Type: application/json
{
"name": "John Doe",
"email": "john@example.com",
"password": "password123",
"password_confirmation": "password123"
}
Expected response (201 Created):
{
"message": "Registration successful",
"access_token": "1|abc123xyz...",
"token_type": "Bearer"
}
2. Login:
POST http://127.0.0.1:8000/api/login
Content-Type: application/json
{
"email": "john@example.com",
"password": "password123"
}
3. Create a task (use the token from login/register):
POST http://127.0.0.1:8000/api/tasks
Authorization: Bearer 1|abc123xyz...
Content-Type: application/json
{
"title": "Complete REST API tutorial",
"description": "Build and test the full CRUD API",
"completed": false
}
4. List all tasks: GET /api/tasks with Bearer token header5. View single task:
GET /api/tasks/1 with Bearer token header6. Update a task:
PUT /api/tasks/1 with updated JSON body7. Delete a task:
DELETE /api/tasks/1 — returns 204 No ContentCommon errors to watch for:
- 401 Unauthorized — Token is missing or invalid. Ensure the
Authorization: Bearer {token}header is set correctly. - 422 Unprocessable Entity — Validation failed. Check the response body for specific field errors.
- 404 Not Found — The task ID does not exist in the database.
- 500 Server Error — Check your
.envdatabase credentials and runphp artisan migrate.
9 Conclusion
Laravel 12's refined developer experience — from the streamlined
bootstrap/app.php middleware configuration to the built-in Sanctum support — makes it one of the most efficient frameworks for API development in 2026.Next steps to improve your API:
- Add pagination using
Task::paginate(15)instead ofTask::all()for production-ready list endpoints. - Implement API Resources (
php artisan make:resource TaskResource) for consistent JSON response formatting. - Add rate limiting to protect your API from abuse.
- Write feature tests using
php artisan make:test TaskApiTest. - Explore background job processing for heavy operations — see our Laravel 12 Queue and Jobs Tutorial.
- Optimize your app performance by following our Laravel 12 Performance Optimization Tips.
If you run into any issues, refer to our Common Laravel 12 Errors and How to Fix Them guide for troubleshooting help.
Written by Revathi M
PHP Developer & Technical Writer · 10+ years building web applications with CodeIgniter and Laravel
Revathi specializes in PHP backend development, authentication systems, and REST API design. She writes practical, production-tested tutorials at Get Sample Code to help developers build secure applications faster.
Frequently Asked Questions
Laravel 12 is the latest major release of the Laravel PHP framework, launched in early 2025. It introduces performance improvements including faster query execution, an optimized service container, improved job batching, and reduced boot time. The middleware configuration has also moved from Kernel.php to bootstrap/app.php, and Sanctum now ships pre-installed for API authentication.
To install Laravel 12, ensure you have PHP 8.2+ and Composer installed. Run the command: composer create-project laravel/laravel:^12.0 your-project-name. After installation, navigate to the project folder, configure your .env file with your database credentials, and run php artisan migrate to set up the default tables.
Laravel Sanctum is a lightweight authentication package for issuing API tokens. It is ideal for single-page applications (SPAs), mobile apps, and simple token-based APIs. In Laravel 12, Sanctum comes pre-installed. You can enable it by running php artisan install:api, which creates the personal_access_tokens migration and configures the API routes.
In Laravel API controllers, use the $request->validate() method to define validation rules for incoming data. If validation fails, Laravel automatically returns a 422 JSON response with detailed error messages. For example: $request->validate(["title" => "required|string|max:255"]). For complex validation, you can also create dedicated Form Request classes using php artisan make:request.
Yes, Laravel makes pagination simple. Instead of Task::all(), use Task::paginate(15) in your controller's index method. This returns paginated JSON with metadata including current_page, last_page, per_page, and total count. The client can request specific pages using the ?page=2 query parameter.
Common errors include: (1) CORS errors — solve by installing the fruitcake/laravel-cors package or configuring config/cors.php. (2) 401 Unauthorized — ensure the Bearer token is included in request headers. (3) Database connection failures — verify your .env credentials match your database setup. (4) MassAssignmentException — add columns to the $fillable array in your model. (5) 404 on API routes — ensure you ran php artisan install:api and routes are defined in routes/api.php.
