Introduction
REST APIs are the backbone of modern web applications. Whether you are powering a mobile app, a single-page application or a third-party integration, a well-designed API makes all the difference. In this guide, we will build a production-grade REST API from scratch using Laravel 11.
Prerequisites: PHP 8.2+, Composer, basic Laravel knowledge and a clear understanding of REST basics.
1. Project Setup
Start with a fresh Laravel project and install the API scaffolding. Laravel 11 keeps the application skeleton smaller, which makes API-focused projects easier to reason about.
bashcomposer create-project laravel/laravel laravel-api cd laravel-api php artisan install:api
The install:api command prepares Sanctum, API routes and migrations so you can focus on application design instead of setup chores.
2. API Route Structure
Version your API from day one. A small prefix like v1 gives you room to ship future breaking changes without disrupting existing clients.
phpuse App\Http\Controllers\Api\V1\AuthController; use App\Http\Controllers\Api\V1\PostController; Route::prefix('v1')->group(function () { Route::post('/register', [AuthController::class, 'register']); Route::post('/login', [AuthController::class, 'login']); Route::middleware('auth:sanctum')->group(function () { Route::apiResource('posts', PostController::class); Route::post('/logout', [AuthController::class, 'logout']); }); });
3. API Resources
Never return raw Eloquent models from an API. Resources let you shape responses, hide private fields and include relationships only when they were intentionally loaded.
phpclass PostResource extends JsonResource { public function toArray(Request $request): array { return [ 'id' => $this->id, 'title' => $this->title, 'slug' => $this->slug, 'excerpt' => $this->excerpt, 'body' => $this->when($request->routeIs('posts.show'), $this->body), 'author' => new UserResource($this->whenLoaded('user')), 'created_at' => $this->created_at->toDateTimeString(), ]; } }
Pro tip: Use when() and whenLoaded() to avoid over-fetching and N+1 query issues.
4. Authentication with Sanctum
Sanctum is a strong fit for token-based APIs. Keep login responses clear, expire tokens when possible and delete the current access token on logout.
phppublic function login(LoginRequest $request): JsonResponse { if (! Auth::attempt($request->only('email', 'password'))) { return response()->json(['message' => 'Invalid credentials'], 401); } $token = $request->user()->createToken('api-token', ['*'], now()->addDays(30)); return response()->json([ 'user' => new UserResource($request->user()), 'token' => $token->plainTextToken, ]); }
5. Form Requests and Error Handling
Keep controllers thin by moving validation into Form Request classes. This creates a reusable validation layer and makes error responses predictable.
phpclass StorePostRequest extends FormRequest { public function rules(): array { return [ 'title' => ['required', 'string', 'max:255'], 'body' => ['required', 'string'], 'status' => ['required', Rule::in(['draft', 'published'])], ]; } }
6. Rate Limiting
Rate limits protect login endpoints and high-cost API routes. Laravel lets you define limits by IP, authenticated user or another request-specific key.
Important: Return clear 429 responses so clients know when to retry.
7. Testing Your API
Production APIs need tests around authentication, validation, permissions and response structure. Laravel HTTP tests make those checks readable.
phpit('can create a post when authenticated', function () { $user = User::factory()->create(); $this->actingAs($user, 'sanctum') ->postJson('/api/v1/posts', [ 'title' => 'My First Post', 'body' => 'Hello world', 'status' => 'published', ]) ->assertCreated() ->assertJsonPath('data.title', 'My First Post'); });
Conclusion
A scalable Laravel API is built from small repeatable decisions: version routes, transform responses with Resources, authenticate with Sanctum, validate with Form Requests, apply rate limits and protect everything with tests.