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:apiThis 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:apiUpdate your .env with a database connection, then run migrations:
php artisan migrateYou’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 --apiThe --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 migrateDefining 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 optimizeGoing 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.