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.

How to Build a REST API in Laravel 12: Step-by-Step Guide with Examples

Table Of Content

1 Prerequisites for Building Your Laravel 12 REST API

Before diving into the code, make sure your development environment meets the following requirements:
  • PHP 8.2 or higher — Laravel 12 requires PHP 8.2 as a minimum. Run php -v in 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

Throughout this tutorial, we will build a Task Management REST API that supports full CRUD operations — creating tasks, retrieving task lists, updating task details, and deleting tasks. We will also secure all endpoints using Laravel Sanctum, which provides lightweight token-based authentication perfect for SPAs and mobile applications.

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

Setting up a new Laravel 12 project is your first step toward building the REST API. Laravel provides a streamlined installation process through Composer. Once installed, you will configure your database connection and verify that everything is running correctly before writing any API code.

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

First, ensure Composer is installed on your system. Use the following command to install a new 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)

Open the .env file and input the necessary database credentials:

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

Migrations in Laravel are version-controlled database schema definitions. They allow your team to share and replicate the database structure consistently. Let's create a migration for the tasks table.

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

Laravel 12 simplifies API routing with a dedicated routes/api.php file. All routes defined here are automatically prefixed with /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/tasksindex() — List all tasks
  • POST /api/tasksstore() — 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

The controller is where your API logic lives. Laravel provides a convenient --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 Task model instance when you type-hint Task $task in 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 Created for new resources and 204 No Content for 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

Laravel 12 ships with Laravel Sanctum pre-installed, so you don't need to install it separately. Sanctum provides a lightweight authentication system for issuing API tokens — perfect for SPAs, mobile apps, and simple token-based APIs.

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.
If you found this REST API guide helpful, here are more Laravel 12 tutorials from our site to level up your development skills:

8 Testing Your REST API

Start the Laravel development server:

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 header
5. View single task: GET /api/tasks/1 with Bearer token header
6. Update a task: PUT /api/tasks/1 with updated JSON body
7. Delete a task: DELETE /api/tasks/1 — returns 204 No Content

Common 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 .env database credentials and run php artisan migrate.

9 Conclusion

You have now built a complete REST API in Laravel 12 with full CRUD operations and Sanctum token authentication. Throughout this guide, we covered project setup, database migrations, Eloquent model configuration, RESTful route definitions, controller logic with validation, secure authentication using Sanctum, and thorough API testing with Postman.

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 of Task::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.
Revathi M - PHP and CodeIgniter Developer

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.