APIs power modern web and mobile applications, enabling seamless communication between different systems. Laravel 12 provides an elegant and robust toolkit for building RESTful APIs that are secure, maintainable, and performant. This comprehensive guide covers essential best practices to help you create production-ready APIs that scale.
Understanding RESTful API Design
Before diving into Laravel-specific implementations, let's establish REST principles:
- Stateless communication – Each request contains all necessary information
- Resource-based URLs – Use nouns, not verbs (e.g.,
/users, not/getUsers) - HTTP methods – GET (read), POST (create), PUT/PATCH (update), DELETE (remove)
- Standard status codes – 200 (OK), 201 (Created), 404 (Not Found), 422 (Validation Error)
- JSON responses – Consistent data format across endpoints
Setting Up API Routes in Laravel 12
Laravel provides a dedicated routes/api.php file for API routes. These routes are automatically prefixed with /api and are stateless by default.
Basic Resource Routes
use App\Http\Controllers\Api\UserController;
use Illuminate\Support\Facades\Route;
// RESTful resource routes
Route::apiResource('users', UserController::class);
// Manual route definitions
Route::get('/users', [UserController::class, 'index']);
Route::post('/users', [UserController::class, 'store']);
Route::get('/users/{user}', [UserController::class, 'show']);
Route::put('/users/{user}', [UserController::class, 'update']);
Route::patch('/users/{user}', [UserController::class, 'update']);
Route::delete('/users/{user}', [UserController::class, 'destroy']);
Route Model Binding
Laravel 12's route model binding automatically resolves Eloquent models:
Route::get('/users/{user}', function (User $user) {
return new UserResource($user);
});
Creating API Controllers
Generate API-specific controllers with the --api flag:
php artisan make:controller Api/UserController --api --model=User
Controller Best Practices
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Resources\UserResource;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
class UserController extends Controller
{
/**
* Display a listing of users.
*/
public function index(): AnonymousResourceCollection
{
$users = User::query()
->with('profile')
->paginate(15);
return UserResource::collection($users);
}
/**
* Store a newly created user.
*/
public function store(Request $request): JsonResponse
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users',
'password' => 'required|min:8|confirmed',
]);
$validated['password'] = bcrypt($validated['password']);
$user = User::create($validated);
return (new UserResource($user))
->response()
->setStatusCode(201);
}
/**
* Display the specified user.
*/
public function show(User $user): UserResource
{
return new UserResource($user->load('profile', 'posts'));
}
/**
* Update the specified user.
*/
public function update(Request $request, User $user): UserResource
{
$validated = $request->validate([
'name' => 'sometimes|string|max:255',
'email' => 'sometimes|email|unique:users,email,' . $user->id,
]);
$user->update($validated);
return new UserResource($user);
}
/**
* Remove the specified user.
*/
public function destroy(User $user): JsonResponse
{
$user->delete();
return response()->json([
'message' => 'User deleted successfully'
], 204);
}
}
Using API Resources for Consistent Responses
API Resources transform your Eloquent models into JSON responses with complete control over data structure.
Creating Resources
php artisan make:resource UserResource
php artisan make:resource UserCollection
Building Resource Classes
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class UserResource extends JsonResource
{
/**
* Transform the resource into an array.
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'created_at' => $this->created_at->toISOString(),
'updated_at' => $this->updated_at->toISOString(),
// Conditional attributes
'email_verified' => $this->when(
$request->user()?->isAdmin(),
$this->email_verified_at !== null
),
// Relationship loading
'profile' => new ProfileResource($this->whenLoaded('profile')),
'posts' => PostResource::collection($this->whenLoaded('posts')),
// Computed values
'posts_count' => $this->when(
$this->relationLoaded('posts'),
fn() => $this->posts->count()
),
];
}
/**
* Get additional data that should be returned with the resource array.
*/
public function with(Request $request): array
{
return [
'meta' => [
'version' => '1.0',
'timestamp' => now()->toISOString(),
],
];
}
}
Using Resources in Controllers
// Single resource
return new UserResource($user);
// Collection
return UserResource::collection(User::paginate(15));
// With additional metadata
return UserResource::collection($users)->additional([
'meta' => [
'total_active' => User::where('active', true)->count(),
],
]);
Implementing Authentication with Laravel Sanctum
Laravel 12 uses Sanctum for API token authentication, perfect for SPAs and mobile applications.
Installation and Setup
composer require laravel/sanctum
php artisan install:api
This command publishes configuration and runs migrations. Update your User model:
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
use HasApiTokens, HasFactory, Notifiable;
// ... rest of model
}
Token Generation
namespace App\Http\Controllers\Api;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;
class AuthController extends Controller
{
public function login(Request $request)
{
$request->validate([
'email' => 'required|email',
'password' => 'required',
'device_name' => 'required',
]);
$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($request->device_name)->plainTextToken;
return response()->json([
'user' => new UserResource($user),
'token' => $token,
'token_type' => 'Bearer',
]);
}
public function logout(Request $request)
{
$request->user()->currentAccessToken()->delete();
return response()->json([
'message' => 'Logged out successfully'
]);
}
}
Protecting Routes
Route::middleware('auth:sanctum')->group(function () {
Route::get('/profile', [ProfileController::class, 'show']);
Route::apiResource('posts', PostController::class);
Route::post('/logout', [AuthController::class, 'logout']);
});
API Versioning Strategy
Versioning ensures backward compatibility when making breaking changes to your API.
URL-Based Versioning
// routes/api.php
Route::prefix('v1')->name('v1.')->group(function () {
Route::apiResource('users', Api\V1\UserController::class);
Route::apiResource('posts', Api\V1\PostController::class);
});
Route::prefix('v2')->name('v2.')->group(function () {
Route::apiResource('users', Api\V2\UserController::class);
Route::apiResource('posts', Api\V2\PostController::class);
});
Directory Structure
app/Http/Controllers/Api/
├── V1/
│ ├── UserController.php
│ └── PostController.php
└── V2/
├── UserController.php
└── PostController.php
Error Handling and Validation
Custom Exception Handler
Update bootstrap/app.php in Laravel 12:
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Http\Request;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
return Application::configure(basePath: dirname(__DIR__))
->withExceptions(function (Exceptions $exceptions) {
$exceptions->render(function (NotFoundHttpException $e, Request $request) {
if ($request->is('api/*')) {
return response()->json([
'status' => 'error',
'message' => 'Resource not found',
], 404);
}
});
})->create();
Consistent Response Format
namespace App\Traits;
trait ApiResponse
{
protected function successResponse($data, string $message = 'Success', int $code = 200)
{
return response()->json([
'status' => 'success',
'message' => $message,
'data' => $data,
], $code);
}
protected function errorResponse(string $message, int $code = 400, ?array $errors = null)
{
return response()->json([
'status' => 'error',
'message' => $message,
'errors' => $errors,
], $code);
}
}
Rate Limiting
Laravel 12 provides built-in rate limiting for API protection:
// bootstrap/app.php
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Facades\RateLimiter;
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});
// Custom rate limits
RateLimiter::for('uploads', function (Request $request) {
return $request->user()?->isPremium()
? Limit::none()
: Limit::perMinute(10);
});
Apply rate limiting to routes:
Route::middleware(['auth:sanctum', 'throttle:api'])->group(function () {
Route::apiResource('posts', PostController::class);
});
Performance Optimization
Eager Loading Relationships
public function index()
{
$users = User::with(['profile', 'posts:id,user_id,title'])
->withCount('posts')
->paginate(15);
return UserResource::collection($users);
}
Caching Strategies
use Illuminate\Support\Facades\Cache;
public function index()
{
$users = Cache::remember('users.all', 3600, function () {
return User::with('profile')->get();
});
return UserResource::collection($users);
}
Pagination Best Practices
// Standard pagination
$users = User::paginate(15);
// Cursor pagination for better performance
$users = User::cursorPaginate(15);
// Custom pagination parameters
$perPage = $request->input('per_page', 15);
$users = User::paginate(min($perPage, 100));
Testing Your API
namespace Tests\Feature\Api;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class UserApiTest extends TestCase
{
use RefreshDatabase;
public function test_can_list_users()
{
User::factory()->count(5)->create();
$response = $this->getJson('/api/v1/users');
$response->assertStatus(200)
->assertJsonStructure([
'data' => [
'*' => ['id', 'name', 'email']
]
]);
}
public function test_can_create_user_with_authentication()
{
$user = User::factory()->create();
$response = $this->actingAs($user, 'sanctum')
->postJson('/api/v1/users', [
'name' => 'John Doe',
'email' => 'john@example.com',
]);
$response->assertStatus(201)
->assertJson([
'data' => [
'name' => 'John Doe',
'email' => 'john@example.com',
]
]);
}
}
Conclusion
Building RESTful APIs with Laravel 12 combines elegant syntax with powerful features. By implementing API resources for consistent responses, Sanctum for authentication, proper versioning strategies, comprehensive error handling, rate limiting, and performance optimizations, you'll create APIs that are secure, maintainable, and scale effortlessly.
Remember to always validate input data, use proper HTTP status codes, document your API endpoints, and write comprehensive tests. These practices ensure your API remains reliable and developer-friendly as your application grows.