February 28, 2023

How to Build a REST API with Laravel 13: Complete Guide with Sanctum and API Resources

Photo of Marco Orta Marco Orta | 7 mins read
Compartir
Code editor showing a Laravel API route returning JSON

Building a REST API with Laravel is one of the most in-demand skills in 2026: any mobile app, SPA frontend, or AI agent that connects to your system will do so through an API. In this guide we’ll set up a fully functional REST API with Laravel 13, including routes, controllers, Form Requests for validation, API Resources for serialization, and Sanctum for token-based authentication.

What is a REST API?

A REST API (Representational State Transfer) is an interface that exposes resources accessible over HTTP, almost always returning JSON. The key principles: each resource has a unique URL, operations use standard HTTP verbs (GET, POST, PUT/PATCH, DELETE), responses are stateless, and the structure follows predictable conventions.

1. Create a New Laravel 13 Application

If you don’t have a project yet, check out How to Install Laravel 13 first. In short:

laravel new my-api
cd my-api

In Laravel 11+, the routes/api.php file is not created by default. To enable it, run:

php artisan install:api

This command creates routes/api.php, automatically registers the /api prefix, and installs Laravel Sanctum for token-based authentication.

2. Configure the Database

Edit the .env file with your credentials:

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=my_api
DB_USERNAME=root
DB_PASSWORD=

To get started quickly, you can also use SQLite:

DB_CONNECTION=sqlite

Then run the migrations:

php artisan migrate

3. Create the Model, Migration, and Factory

We’ll build a “products” CRUD. Generate everything in one command:

php artisan make:model Product -mfr

The -mfr flags create a migration, factory, and resource controller. Edit the generated migration:

public function up(): void
{
    Schema::create('products', function (Blueprint $table) {
        $table->id();
        $table->string('name');
        $table->text('description')->nullable();
        $table->decimal('price', 10, 2);
        $table->integer('stock')->default(0);
        $table->timestamps();
    });
}

And in the app/Models/Product.php model, declare the $fillable fields:

class Product extends Model
{
    use HasFactory;

    protected $fillable = ['name', 'description', 'price', 'stock'];
}

Run the migration:

php artisan migrate

4. Create the API Resource

API Resources are Laravel’s idiomatic way to control exactly which fields your API exposes:

php artisan make:resource ProductResource

Edit app/Http/Resources/ProductResource.php:

public function toArray(Request $request): array
{
    return [
        'id'          => $this->id,
        'name'        => $this->name,
        'description' => $this->description,
        'price'       => (float) $this->price,
        'stock'       => $this->stock,
        'created_at'  => $this->created_at->toIso8601String(),
    ];
}

5. Create Form Requests for Validation

Instead of validating inside the controller, the idiomatic approach in 2026 is to use dedicated Form Requests:

php artisan make:request StoreProductRequest
php artisan make:request UpdateProductRequest

Edit StoreProductRequest:

public function authorize(): bool
{
    return true;
}

public function rules(): array
{
    return [
        'name'        => ['required', 'string', 'max:255'],
        'description' => ['nullable', 'string'],
        'price'       => ['required', 'numeric', 'min:0'],
        'stock'       => ['required', 'integer', 'min:0'],
    ];
}

6. Implement the Controller

app/Http/Controllers/ProductController.php already has the skeleton. Fill it in like this:

use App\Http\Requests\StoreProductRequest;
use App\Http\Requests\UpdateProductRequest;
use App\Http\Resources\ProductResource;
use App\Models\Product;

class ProductController extends Controller
{
    public function index()
    {
        return ProductResource::collection(Product::latest()->paginate(20));
    }

    public function store(StoreProductRequest $request)
    {
        $product = Product::create($request->validated());
        return new ProductResource($product);
    }

    public function show(Product $product)
    {
        return new ProductResource($product);
    }

    public function update(UpdateProductRequest $request, Product $product)
    {
        $product->update($request->validated());
        return new ProductResource($product);
    }

    public function destroy(Product $product)
    {
        $product->delete();
        return response()->noContent();
    }
}

Notice two key patterns of modern Laravel: route model binding (Product $product is already resolved from the URL) and response()->noContent() (correctly returns 204 No Content).

7. Define the Routes

Edit routes/api.php:

use App\Http\Controllers\ProductController;
use Illuminate\Support\Facades\Route;

Route::apiResource('products', ProductController::class);

A single line registers all five REST routes: GET /api/products, POST /api/products, GET /api/products/{id}, PUT/PATCH /api/products/{id}, and DELETE /api/products/{id}.

Verify with:

php artisan route:list --path=api

8. Authentication with Laravel Sanctum

When you ran php artisan install:api, Sanctum was installed. To issue tokens, add the HasApiTokens trait to App\Models\User:

use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
    use HasApiTokens, HasFactory, Notifiable;
    // ...
}

Create a login endpoint that returns the token:

use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;

Route::post('/login', function (Request $request) {
    $request->validate([
        'email'    => ['required', 'email'],
        'password' => ['required'],
    ]);

    $user = User::where('email', $request->email)->first();

    if (! $user || ! Hash::check($request->password, $user->password)) {
        return response()->json(['message' => 'Invalid credentials'], 401);
    }

    return response()->json([
        'token' => $user->createToken('api')->plainTextToken,
        'user'  => $user,
    ]);
});

And protect the product routes:

Route::middleware('auth:sanctum')->group(function () {
    Route::apiResource('products', ProductController::class);
});

Now every request to /api/products requires the header:

Authorization: Bearer {token}

Sanctum issues opaque tokens, but if your API uses JSON Web Tokens (JWT) you can inspect their header and payload and verify the signature with this tool:

9. Testing the API

Use Bruno, Thunder Client, Postman, or curl directly:

# Login
curl -X POST http://localhost:8000/api/login \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]","password":"password"}'

# Create a product (replace TOKEN with the one returned above)
curl -X POST http://localhost:8000/api/products \
  -H "Authorization: Bearer TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"Laptop","price":1500,"stock":10}'

10. Tests with PHPUnit (or Pest)

Laravel 13 ships with Pest integrated by default. A basic test:

use App\Models\User;
use Laravel\Sanctum\Sanctum;

it('lists products for authenticated users', function () {
    Sanctum::actingAs(User::factory()->create());

    $this->getJson('/api/products')
        ->assertOk()
        ->assertJsonStructure(['data', 'meta', 'links']);
});

it('rejects requests without a token', function () {
    $this->getJson('/api/products')->assertStatus(401);
});

Run the tests:

php artisan test

Conclusion

In 2026, building a REST API with Laravel is practically “run install:api, create a resource controller, and you’re done.” What separates a toy API from a professional one is the details: API Resources for clean serialization, Form Requests for validation, Sanctum for token-based authentication, and tests to avoid breaking anything with each release.

Once your API is up and running, two natural next steps are: optimizing performance with How to Optimize Your Laravel Project’s Performance and, if you want to expose your logic to AI agents, reading How to Build an AI Agent with Laravel and MCP.

If this guide was useful, don’t forget to share it. Cheers!

Compartir

Search

Tags