In today's interconnected world, REST APIs serve as the backbone of modern web applications. Laravel 12 provides powerful tools and conventions to build scalable, maintainable, and secure REST APIs. This comprehensive guide will walk you through the best practices for creating professional-grade APIs using Laravel's latest features.
📋 Table of Contents
- 🚀 Building Robust REST APIs with Laravel 12: A Complete Guide
- 📋 Table of Contents
- Why Laravel for REST APIs?
- Project Structure and Organization
- API Versioning Strategy
- Authentication with Laravel Sanctum
- API Resources and Data Transformation
- Request Validation and Form Requests
- Advanced Filtering with Spatie Query Builder
- Rate Limiting and Security
- Testing Your API
- Conclusion
Why Laravel for REST APIs?
Laravel 12 offers exceptional features for API development:
- Built-in API Resources: Transform data consistently across your API
- Laravel Sanctum: Lightweight authentication for SPAs and mobile apps
- Form Request Validation: Clean, reusable validation logic
- API Versioning: Built-in support for API versioning
- Eloquent ORM: Powerful database interactions with relationships
- Middleware Stack: Flexible request/response processing
- Testing Framework: Comprehensive testing tools built-in
Project Structure and Organization
A well-organized project structure is crucial for maintainable APIs. Here's the recommended structure for a Laravel API project:
app/
├── Http/
│ ├── Controllers/
│ │ └── Api/
│ │ ├── ApiController.php # Base API controller
│ │ └── v1/ # Version-specific controllers
│ │ ├── ProductController.php
│ │ ├── Auth/
│ │ │ ├── CurrentUserController.php
│ │ │ └── SocialiteLoginController.php
│ │ └── Admin/
│ │ └── ProductController.php
│ ├── Resources/ # API resource transformers
│ │ ├── ProductResource.php
│ │ └── UserResource.php
│ ├── Requests/ # Form request validation
│ │ ├── ProductStoreRequest.php
│ │ └── ProductUpdateRequest.php
│ └── Middleware/ # Custom middleware
├── Traits/
│ └── ApiResponses.php # Standardized API responses
└── Models/
├── Product.php
└── User.php
API Versioning Strategy
API versioning is essential for maintaining backward compatibility while evolving your API. Laravel 12 makes this easy with the apiPrefix configuration.
Bootstrap Configuration
// bootstrap/app.php
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__ . '/../routes/web.php',
api: __DIR__ . '/../routes/api.php',
commands: __DIR__ . '/../routes/console.php',
channels: __DIR__ . '/../routes/channels.php',
health: '/up',
apiPrefix: 'api/v1',
)
->withMiddleware(function (Middleware $middleware): void {
$middleware->statefulApi();
$middleware->api(prepend: [
ThrottleRequests::class . ':api',
SubstituteBindings::class,
]);
$middleware->alias([
'throttle' => ThrottleRequests::class,
'throttle.login' => ThrottleRequests::class,
'verified' => App\Http\Middleware\EnsureEmailIsVerified::class,
'admin' => App\Http\Middleware\AdminMiddleware::class,
]);
->create();
Route Organization
// routes/api.php
Route::prefix('v1')->group(function () {
// Public routes
Route::get('/products', [ProductController::class, 'index']);
Route::get('/products/{product}', [ProductController::class, 'show']);
// Authenticated routes
Route::middleware(['auth:sanctum'])->group(function () {
// Admin routes
Route::middleware(['admin'])->prefix('admin')->group(function () {
Route::apiResource('/products', Admin\ProductController::class);
});
});
});
Authentication with Laravel Sanctum
Laravel Sanctum provides a simple, lightweight authentication system for SPAs and mobile applications.
Configuration
// config/sanctum.php
return [
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
'%s%s',
'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
Sanctum::currentApplicationUrlWithPort()
))),
'guard' => ['web'],
'expiration' => null, // Tokens don't expire
'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''),
'middleware' => [
'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class,
'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class,
'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class,
],
];
User Model Setup
// app/Domain/User/Models/User.php
<?php
namespace App\Domain\User\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
use HasApiTokens, Notifiable;
protected $fillable = [
'name',
'email',
'password',
];
protected $hidden = [
'password',
'remember_token',
];
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
}
}
Authentication Routes
// routes/api.php
Route::get('/sanctum/csrf-cookie', [CsrfCookieController::class, 'show']);
Route::get('/user', fn (Request $request) => $request->user())->middleware('auth:sanctum');
// Authentication endpoints
Route::post('/login', [AuthController::class, 'login']);
Route::post('/logout', [AuthController::class, 'logout'])->middleware('auth:sanctum');
Route::post('/register', [AuthController::class, 'register']);
// Social authentication
Route::prefix('auth/{provider}')->group(function () {
Route::get('/url', [SocialiteLoginController::class, 'redirectToProvider']);
Route::get('/callback', [SocialiteLoginController::class, 'handleProviderCallback']);
});
API Resources and Data Transformation
API Resources provide a clean, consistent way to transform your models into JSON responses.
Base API Controller
// app/Http/Controllers/Api/ApiController.php
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Traits\ApiResponses;
class ApiController extends Controller
{
use ApiResponses;
Standardized Response Trait
// app/Traits/ApiResponses.php
<?php
declare(strict_types=1);
namespace App\Traits;
use Illuminate\Http\JsonResponse;
trait ApiResponses
{
protected function ok($message, $data = [], $statusCode = 200): JsonResponse
{
return $this->success($message, $data, $statusCode);
}
protected function success($message, $data = [], $statusCode = 200): JsonResponse
{
return response()->json([
'data' => $data,
'message' => $message,
'status' => $statusCode,
], $statusCode);
}
protected function error($errors = [], $statusCode = null): JsonResponse
{
if (is_string($errors)) {
return response()->json([
'message' => $errors,
'status' => $statusCode,
], $statusCode);
}
return response()->json([
'errors' => $errors,
]);
}
}
API Resource Example
// app/Http/Resources/ProductResource.php
<?php
declare(strict_types=1);
namespace App\Http\Resources;
use App\Models\Product;
use App\Services\ProductService;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
/** @mixin Product */
class ProductResource extends JsonResource
{
/**
* Transform the resource into an array.
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'brand_id' => $this->brand_id,
'category_id' => $this->category_id,
'collection_id' => $this->collection_id,
// Conditional loading of relationships
'brand' => $this->whenLoaded('brand', fn () => $this->brand),
'category' => $this->whenLoaded('category', fn () => $this->category),
'collection' => $this->whenLoaded('collection', fn () => $this->collection),
// Product details
'name' => $this->name,
'model' => $this->model,
'description' => $this->description,
'short_description' => $this->short_description,
// Pricing
'price' => $this->price,
'formatted_price' => $this->whenHas('price', fn () => $this->formatted_price),
// Media
'url' => $this->url,
'images' => $this->images,
'main_image' => $this->main_image,
// Relationships with conditional loading
'sizes' => $this->whenLoaded('sizes', fn () => SizeResource::collection($this->sizes)) ?? [],
'attributes' => $this->whenLoaded('attributes', fn () =>
app(ProductService::class)->transformAttributes($this->attributes)
),
// User-specific data
'bookmark_id' => $this->when($request->user(), function () use ($request) {
return $this->bookmark()
->whereUserId($request->user()->id)
->pluck('id')->first() ?? false;
}),
'review' => $this->when($request->user(), fn () =>
$this->review()->whereUserId($request->user()->id)->first() ?? false
),
// Aggregated data
'reviews_count' => $this->whenCounted('reviews', fn () => $this->reviews_count),
'reviews_avg_rating' => $this->hasAttribute('reviews_avg_rating') && $this->reviews_avg_rating
? round((float) $this->reviews_avg_rating, 2)
: 0,
];
}
}
Request Validation and Form Requests
Laravel's Form Requests provide a clean way to handle validation logic.
Product Store Request
// app/Http/Requests/ProductStoreRequest.php
<?php
declare(strict_types=1);
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class ProductStoreRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return $this->user()?->hasRole('admin') ?? false;
}
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'model' => ['required', 'string', 'max:255'],
'description' => ['required', 'string'],
'short_description' => ['required', 'string', 'max:500'],
'price' => ['required', 'numeric', 'min:0'],
'brand_id' => ['required', 'exists:brands,id'],
'category_id' => ['required', 'exists:categories,id'],
'collection_id' => ['nullable', 'exists:collections,id'],
'images' => ['array'],
'images.*' => ['image', 'mimes:jpeg,png,jpg,webp', 'max:2048'],
'attributes' => ['array'],
'attributes.*.attribute_id' => ['required', 'exists:attributes,id'],
'attributes.*.value' => ['required', 'string'],
];
}
/**
* Get custom messages for validator errors.
*/
public function messages(): array
{
return [
'name.required' => 'Product name is required.',
'price.required' => 'Product price is required.',
'price.min' => 'Product price must be at least 0.',
'images.*.max' => 'Image size must not exceed 2MB.',
];
}
/**
* Prepare the data for validation.
*/
protected function prepareForValidation(): void
{
$this->merge([
'price' => $this->price * 100, // Convert to cents
]);
}
}
Advanced Filtering with Spatie Query Builder
The Spatie Query Builder package provides powerful filtering, sorting, and searching capabilities for your API.
Installation
composer require spatie/laravel-query-builder
Basic Implementation
// app/Http/Controllers/Api/v1/ProductController.php
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api\v1;
use App\Http\Controllers\Api\ApiController;
use App\Http\Resources\ProductResource;
use App\Models\Product;
use Illuminate\Http\Request;
use Spatie\QueryBuilder\QueryBuilder;
use Spatie\QueryBuilder\AllowedFilter;
use Spatie\QueryBuilder\AllowedSort;
class ProductController extends ApiController
{
public function index(Request $request)
{
$products = QueryBuilder::for(Product::class)
->allowedFilters([
AllowedFilter::exact('brand_id'),
AllowedFilter::exact('category_id'),
AllowedFilter::exact('collection_id'),
AllowedFilter::partial('name'),
AllowedFilter::scope('price_range'),
AllowedFilter::scope('in_stock'),
])
->allowedSorts([
'name',
'price',
'created_at',
AllowedSort::field('popularity', 'views_count'),
])
->allowedIncludes([
'brand',
'category',
'collection',
'reviews',
'attributes',
])
->defaultSort('-created_at')
->paginate($request->get('per_page', 15));
return ProductResource::collection($products);
}
public function show(Product $product)
{
$product->load([
'brand',
'category',
'collection',
'attributes.attribute',
'reviews.user',
]);
return new ProductResource($product);
}
}
Custom Filters
// app/Models/Product.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Spatie\QueryBuilder\AllowedFilter;
class Product extends Model
{
protected $fillable = [
'name',
'model',
'description',
'short_description',
'price',
'brand_id',
'category_id',
'collection_id',
];
protected $casts = [
'price' => 'integer',
'images' => 'array',
];
public function brand(): BelongsTo
{
return $this->belongsTo(Brand::class);
}
public function category(): BelongsTo
{
return $this->belongsTo(Category::class);
}
public function attributes(): HasMany
{
return $this->hasMany(ProductAttribute::class);
}
/**
* Scope for price range filtering
*/
public function scopePriceRange($query, $min, $max)
{
return $query->whereBetween('price', [$min * 100, $max * 100]);
}
}
Usage Examples
# Basic filtering
GET /api/v1/products?filter[brand_id]=1&filter[category_id]=2
# Text search
GET /api/v1/products?filter[name]=laptop
# Price range filtering
GET /api/v1/products?filter[price_range]=100,500
# Sorting
GET /api/v1/products?sort=price&sort=-created_at
# Including relationships
GET /api/v1/products?include=brand,category,reviews
# Combining filters
GET /api/v1/products?filter[brand_id]=1&filter[price_range]=100,500&sort=-price&include=brand
Rate Limiting and Security
Custom Rate Limiting
// routes/api.php
Route::middleware(['throttle:login'])->group(function () {
Route::post('/login', [AuthController::class, 'login']);
Route::post('/register', [AuthController::class, 'register']);
});
Route::middleware(['auth:sanctum', 'throttle:60,1'])->group(function () {
Route::apiResource('products', ProductController::class);
});
Testing Your API
Comprehensive testing ensures your API works correctly and remains stable.
Feature Test Example
// tests/Feature/ProductApiTest.php
<?php
namespace Tests\Feature;
use App\Models\Product;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Laravel\Sanctum\Sanctum;
use Tests\TestCase;
class ProductApiTest extends TestCase
{
use RefreshDatabase;
public function test_can_list_products()
{
Product::factory()->count(3)->create();
$response = $this->getJson('/api/v1/products');
$response->assertStatus(200)
->assertJsonStructure([
'data' => [
'*' => [
'id',
'name',
'price',
'brand',
'category',
]
]
]);
}
public function test_can_filter_products_by_brand()
{
$brand1 = Brand::factory()->create();
$brand2 = Brand::factory()->create();
Product::factory()->create(['brand_id' => $brand1->id]);
Product::factory()->create(['brand_id' => $brand2->id]);
$response = $this->getJson("/api/v1/products?filter[brand_id]={$brand1->id}");
$response->assertStatus(200);
$response->assertJsonCount(1, 'data');
}
public function test_authenticated_user_can_create_product()
{
$user = User::factory()->create();
$user->assignRole('admin');
Sanctum::actingAs($user);
$productData = [
'name' => 'Test Product',
'model' => 'TP-001',
'description' => 'Test description',
'short_description' => 'Short description',
'price' => 99.99,
'brand_id' => Brand::factory()->create()->id,
'category_id' => Category::factory()->create()->id,
];
$response = $this->postJson('/api/v1/products', $productData);
$response->assertStatus(201)
->assertJsonFragment(['name' => 'Test Product']);
$this->assertDatabaseHas('products', [
'name' => 'Test Product',
'price' => 9999, // Price in cents
]);
}
public function test_unauthenticated_user_cannot_create_product()
{
$productData = [
'name' => 'Test Product',
'model' => 'TP-001',
'description' => 'Test description',
'price' => 99.99,
];
$response = $this->postJson('/api/v1/products', $productData);
$response->assertStatus(401);
}
}
Conclusion
Building robust REST APIs with Laravel 12 requires attention to detail, proper architecture, and adherence to best practices. By implementing the strategies outlined in this guide, you'll create APIs that are:
- Scalable: Proper versioning and organization support growth
- Secure: Authentication, validation, and rate limiting protect your API
- Maintainable: Clean code structure and comprehensive testing
- User-friendly: Consistent responses and clear error handling
- Performant: Efficient queries and proper caching strategies
Remember to:
- Always validate input data
- Use API Resources for consistent data transformation
- Implement proper authentication with Laravel Sanctum
- Version your APIs from the start
- Write comprehensive tests
- Document your API endpoints
- Monitor performance and usage
The combination of Laravel's powerful features and these best practices will help you build professional-grade REST APIs that serve your applications reliably and efficiently.
Follow me on LinkedIn for more Laravel tips!
Would you like to learn more about Laravel, API development, or specific implementation details? Let me know in the comments below!