July 2024
·2 min read
Build a Production REST API with Laravel: Auth, Validation & Resources
Build a production-grade Laravel REST API with Sanctum authentication, Form Request validation, API Resources, and consistent error responses.
Introduction
- Laravel is a go-to backend for full-stack projects pairing with React/Next.js or Angular frontends.
- This guide covers the core pillars of a production API: authentication (Sanctum), validation (Form Requests), transformation (API Resources), and error handling.
- Assumption: fresh Laravel 11 app, no existing API setup.
Project Setup
- Install Laravel:
composer create-project laravel/laravel my-api. - Configure
.env:DB_*,APP_URL,SANCTUM_STATEFUL_DOMAINS. - Install Sanctum:
composer require laravel/sanctum && php artisan vendor:publish --tag=sanctum-config && php artisan migrate.
Authentication with Laravel Sanctum
- Sanctum provides token-based API authentication (SPA cookies for same-domain, bearer tokens for mobile/third-party).
- Two flows: stateful (SPA with CSRF cookie) and stateless (token per API client).
// routes/api.php
Route::post('/auth/login', [AuthController::class, 'login']);
Route::post('/auth/register', [AuthController::class, 'register']);
Route::middleware('auth:sanctum')->group(function () {
Route::get('/user', fn(Request $r) => $r->user());
Route::post('/auth/logout', [AuthController::class, 'logout']);
Route::apiResource('posts', PostController::class);
});// AuthController.php (login method)
public function login(LoginRequest $request): JsonResponse
{
if (! Auth::attempt($request->only('email', 'password'))) {
return response()->json(['message' => 'Invalid credentials'], 401);
}
$token = Auth::user()->createToken('api-token')->plainTextToken;
return response()->json(['token' => $token]);
}Form Request Validation
- Form Requests centralise validation rules and authorisation logic away from controllers.
php artisan make:request StorePostRequest.
// app/Http/Requests/StorePostRequest.php
class StorePostRequest extends FormRequest
{
public function authorize(): bool { return true; }
public function rules(): array
{
return [
'title' => ['required', 'string', 'max:255'],
'body' => ['required', 'string'],
'category' => ['required', 'in:Frontend,Backend,DevOps,Cloud'],
'tags' => ['nullable', 'array'],
'tags.*' => ['string', 'max:32'],
];
}
}- Validation failures auto-return 422 with
errorsJSON — no extra handling needed.
API Resources: Transforming Models
- API Resources decouple your database schema from your API contract.
php artisan make:resource PostResourceandPostCollection.
// app/Http/Resources/PostResource.php
class PostResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'title' => $this->title,
'slug' => $this->slug,
'excerpt' => $this->excerpt,
'category' => $this->category,
'tags' => $this->tags,
'created_at' => $this->created_at->toIso8601String(),
];
}
}Controller Pattern
- Keep controllers thin: validate → authorise → act → transform → respond.
// PostController.php
public function store(StorePostRequest $request): JsonResponse
{
$post = Post::create($request->validated() + ['user_id' => $request->user()->id]);
return (new PostResource($post))->response()->setStatusCode(201);
}
public function index(): AnonymousResourceCollection
{
return PostResource::collection(Post::paginate(15));
}Consistent Error Responses
- Override
renderinapp/Exceptions/Handler.phpto always return JSON for API routes. - Standard error envelope:
{ "message": "...", "errors": { ... } }. - HTTP status codes: 200 (OK), 201 (Created), 204 (No Content), 400 (Bad Request), 401 (Unauthenticated), 403 (Forbidden), 404 (Not Found), 422 (Validation), 500 (Server Error).
Pagination
Post::paginate(15)wrapped inPostResource::collection()auto-generates Laravel's pagination envelope withdata,links,meta.- Use
simplePaginatefor cursor-style pagination on large datasets.
Rate Limiting
- Laravel's built-in rate limiter: define in
AppServiceProviderwithRateLimiter::for('api', ...). throttle:60,1middleware limits to 60 requests per minute per IP.
API Versioning
- Prefix routes:
Route::prefix('v1')->group(...). - Separate controllers per version or use transformation-layer versioning in Resources.
Testing the API
php artisan make:test PostApiTest— use$this->postJson('/api/v1/posts', [...])for HTTP assertions.RefreshDatabasetrait for a clean DB per test.- Test authentication:
$this->actingAs($user, 'sanctum').
Conclusion
- Form Requests + API Resources enforce a clean separation between input, business logic, and output.
- Sanctum covers both SPA and mobile clients without needing a full OAuth server.
- Next steps: add role-based policies (
php artisan make:policy), queue email notifications, and Dockerize for deployment.