How to Build REST API with Laravel 11: A Complete Guide

We earn commissions when you shop through the links below.

If you’ve been wondering how to build REST API with Laravel 11, you’re in the right place. Laravel has always been a strong choice for backend APIs, and version 11 made things even cleaner by slimming down the default application structure, removing the kernel files, and introducing a leaner routing setup. In this guide I’ll walk you through building a fully functional REST API from scratch — routing, controllers, Eloquent resources, authentication, and deployment.

What Changed in Laravel 11 for API Development

Before diving into code, it’s worth noting what version 11 changed. The most impactful shift is the removal of Http/Kernel.php and the consolidation of middleware registration into bootstrap/app.php. Route files also changed slightly — api.php is no longer auto-loaded by default. You need to opt in:

php artisan install:api

This command adds the api.php route file, installs Laravel Sanctum, and publishes the necessary config. It’s a one-liner that sets up everything you need to start building your API.

Project Setup

Start by creating a new Laravel 11 project:

composer create-project laravel/laravel myapi
cd myapi
php artisan install:api

Update your .env with a database connection, then run migrations:

php artisan migrate

You’re ready to build.

Creating the Model, Migration, and Controller

Let’s build a simple Posts API. Generate everything in one command:

php artisan make:model Post -mcr --api

The --api flag creates a resource controller without the create and edit methods (those are for web forms, not APIs). Open the generated migration and define the 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('posts', function (Blueprint $table) {
            $table->id();
            $table->foreignId('user_id')->constrained()->cascadeOnDelete();
            $table->string('title');
            $table->text('body');
            $table->boolean('published')->default(false);
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('posts');
    }
};

Run the migration:

php artisan migrate

Defining API Routes

Open routes/api.php and register your resource routes:

<?php

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

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

Route::post('/register', [App\Http\Controllers\AuthController::class, 'register']);
Route::post('/login', [App\Http\Controllers\AuthController::class, 'login']);

apiResource maps the standard RESTful HTTP verbs to controller methods automatically: GET /posts → index, POST /posts → store, GET /posts/{post} → show, PUT/PATCH /posts/{post} → update, DELETE /posts/{post} → destroy.

Building the Controller

Here’s a clean implementation of the PostController:

<?php

namespace App\Http\Controllers;

use App\Http\Resources\PostResource;
use App\Models\Post;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Illuminate\Http\Response;

class PostController extends Controller
{
    public function index(): AnonymousResourceCollection
    {
        $posts = Post::where('user_id', auth()->id())
            ->latest()
            ->paginate(15);

        return PostResource::collection($posts);
    }

    public function store(Request $request): PostResource
    {
        $validated = $request->validate([
            'title'     => 'required|string|max:255',
            'body'      => 'required|string',
            'published' => 'boolean',
        ]);

        $post = $request->user()->posts()->create($validated);

        return new PostResource($post);
    }

    public function show(Post $post): PostResource
    {
        $this->authorize('view', $post);

        return new PostResource($post);
    }

    public function update(Request $request, Post $post): PostResource
    {
        $this->authorize('update', $post);

        $validated = $request->validate([
            'title'     => 'sometimes|string|max:255',
            'body'      => 'sometimes|string',
            'published' => 'sometimes|boolean',
        ]);

        $post->update($validated);

        return new PostResource($post);
    }

    public function destroy(Post $post): Response
    {
        $this->authorize('delete', $post);
        $post->delete();

        return response()->noContent();
    }
}

API Resources for Consistent JSON Output

Never return raw Eloquent models from your API. Use resources to control the shape of your responses:

php artisan make:resource PostResource
<?php

namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class PostResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id'        => $this->id,
            'title'     => $this->title,
            'body'      => $this->body,
            'published' => $this->published,
            'author'    => [
                'id'   => $this->user->id,
                'name' => $this->user->name,
            ],
            'created_at' => $this->created_at->toISOString(),
            'updated_at' => $this->updated_at->toISOString(),
        ];
    }
}

This approach gives you full control. You can hide sensitive fields, rename keys, add computed properties, and transform relationships without touching the model.

Authentication with Sanctum

Since install:api already set up Sanctum, create an AuthController:

<?php

namespace App\Http\Controllers;

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

class AuthController extends Controller
{
    public function register(Request $request): JsonResponse
    {
        $validated = $request->validate([
            'name'     => 'required|string|max:255',
            'email'    => 'required|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('api-token')->plainTextToken;

        return response()->json(['token' => $token], 201);
    }

    public function login(Request $request): JsonResponse
    {
        $request->validate([
            'email'    => 'required|email',
            'password' => 'required|string',
        ]);

        $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('api-token')->plainTextToken;

        return response()->json(['token' => $token]);
    }
}

Clients send this token as a Bearer token in the Authorization header. Sanctum handles the rest.

Error Handling

In Laravel 11, exception handling is registered in bootstrap/app.php rather than a kernel. Add API-friendly error responses like this:

->withExceptions(function (Exceptions $exceptions) {
    $exceptions->render(function (\Illuminate\Auth\Access\AuthorizationException $e, $request) {
        if ($request->expectsJson()) {
            return response()->json(['message' => 'Forbidden.'], 403);
        }
    });

    $exceptions->render(function (\Illuminate\Database\Eloquent\ModelNotFoundException $e, $request) {
        if ($request->expectsJson()) {
            return response()->json(['message' => 'Resource not found.'], 404);
        }
    });
})

This ensures your API always returns clean JSON errors instead of HTML stack traces.

Testing Your API

Laravel’s built-in HTTP testing is excellent. Here’s a quick feature test:

<?php

namespace Tests\Feature;

use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class PostApiTest extends TestCase
{
    use RefreshDatabase;

    public function test_authenticated_user_can_create_post(): void
    {
        $user = User::factory()->create();

        $response = $this->actingAs($user)
            ->postJson('/api/posts', [
                'title' => 'My First Post',
                'body'  => 'Hello world from the API.',
            ]);

        $response->assertCreated()
            ->assertJsonPath('data.title', 'My First Post');

        $this->assertDatabaseHas('posts', ['title' => 'My First Post']);
    }
}

Run tests with php artisan test. Keep your test coverage high — API contracts break silently when you don’t.

Deploying Your Laravel 11 API

Once your API is ready, you need a reliable host. I regularly deploy Laravel apps to DigitalOcean — their managed databases and App Platform work well for production Laravel APIs, and the droplet-based setup gives you full control when you need it. For quick side projects, Railway is worth a look too — zero-config deployments with a free tier that’s genuinely useful for prototyping.

Regardless of where you deploy, run these before going live:

php artisan config:cache
php artisan route:cache
php artisan optimize

Going Deeper

This guide covers the core of how to build REST API with Laravel 11, but there’s more ground to cover in production: API versioning, rate limiting (built into Laravel via throttle middleware), query scopes for filtering, and robust request classes instead of inline validation. If you want structured learning, Udemy has solid Laravel API courses that go deep on these topics at your own pace.

Summary

Knowing how to build REST API with Laravel 11 means understanding the new leaner structure, using install:api to bootstrap Sanctum, leveraging API resources for consistent output, and handling errors gracefully. The framework does a lot of the heavy lifting — your job is to keep things clean, tested, and secure. This stack remains one of the fastest ways to ship a production-grade API in the PHP ecosystem.