Laravel Framework

REST API Design with Laravel

18 min Lesson 36 of 45

REST API Design with Laravel

Laravel provides comprehensive tools for building robust, secure, and scalable RESTful APIs. In this lesson, we'll explore API design conventions, authentication, versioning, rate limiting, and best practices for creating production-ready APIs.

RESTful Conventions

REST (Representational State Transfer) follows specific conventions for API design. Laravel makes it easy to implement these standards:

<!-- HTTP Methods and Their Purposes --> GET /api/users # List all users (index) GET /api/users/{id} # Show single user (show) POST /api/users # Create new user (store) PUT /api/users/{id} # Update entire user (update) PATCH /api/users/{id} # Partially update user (update) DELETE /api/users/{id} # Delete user (destroy) <!-- Nested Resources --> GET /api/users/{id}/posts # Get user's posts POST /api/users/{id}/posts # Create post for user DELETE /api/posts/{id} # Delete specific post
REST Best Practices:
  • Use nouns for resources (not verbs)
  • Use plural names for collections (/users, not /user)
  • Use HTTP methods to define actions
  • Return appropriate HTTP status codes
  • Version your API from the start

Setting Up API Routes

Laravel provides a dedicated routes/api.php file for API routes. These routes automatically have the /api prefix and are stateless:

// routes/api.php use App\Http\Controllers\Api\UserController; use App\Http\Controllers\Api\PostController; use Illuminate\Support\Facades\Route; // Public API routes Route::get('/status', function () { return response()->json([ 'status' => 'online', 'version' => '1.0.0', 'timestamp' => now()->toIso8601String() ]); }); // Protected API routes Route::middleware(['auth:sanctum'])->group(function () { // User resources Route::apiResource('users', UserController::class); // Nested resources Route::apiResource('users.posts', PostController::class) ->shallow(); // Use shallow routing for nested resources // Custom endpoints Route::post('/users/{user}/follow', [UserController::class, 'follow']); Route::delete('/users/{user}/unfollow', [UserController::class, 'unfollow']); });
apiResource vs resource: The apiResource() method creates routes without create and edit methods, as APIs typically don't serve HTML forms. It only includes: index, store, show, update, destroy.

API Controllers

Create API-specific controllers with proper response formatting and error handling:

// app/Http/Controllers/Api/UserController.php namespace App\Http\Controllers\Api; use App\Http\Controllers\Controller; use App\Http\Resources\UserResource; use App\Models\User; use Illuminate\Http\Request; use Illuminate\Http\Response; class UserController extends Controller { /** * Display a listing of users. */ public function index(Request $request) { $users = User::query() ->when($request->search, function ($query, $search) { $query->where('name', 'like', "%{$search}%"); }) ->paginate($request->per_page ?? 15); return UserResource::collection($users); } /** * Store a newly created user. */ public function store(Request $request) { $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); } /** * Display the specified user. */ public function show(User $user) { return new UserResource($user->load(['posts', 'followers'])); } /** * Update the specified user. */ public function update(Request $request, User $user) { $this->authorize('update', $user); $validated = $request->validate([ 'name' => 'sometimes|string|max:255', 'email' => 'sometimes|email|unique:users,email,' . $user->id, 'bio' => 'nullable|string|max:1000' ]); $user->update($validated); return new UserResource($user); } /** * Remove the specified user. */ public function destroy(User $user) { $this->authorize('delete', $user); $user->delete(); return response()->json([ 'message' => 'User deleted successfully' ], Response::HTTP_OK); } }

API Resources

API Resources provide a transformation layer between your Eloquent models and JSON responses:

// Generate resource: php artisan make:resource UserResource // app/Http/Resources/UserResource.php 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, 'bio' => $this->bio, 'avatar_url' => $this->avatar_url, 'created_at' => $this->created_at->toIso8601String(), 'updated_at' => $this->updated_at->toIso8601String(), // Conditional attributes 'email_verified' => $this->when($this->email_verified_at, true), 'phone' => $this->when($request->user()?->isAdmin(), $this->phone), // Relationships 'posts' => PostResource::collection($this->whenLoaded('posts')), 'followers_count' => $this->when($this->relationLoaded('followers'), fn() => $this->followers->count() ), // Computed attributes 'is_following' => $this->when( $request->user(), fn() => $request->user()->isFollowing($this->resource) ), // Links 'links' => [ 'self' => route('api.users.show', $this->id), 'posts' => route('api.users.posts.index', $this->id) ] ]; } /** * Get additional data for resource response. */ public function with(Request $request): array { return [ 'meta' => [ 'version' => '1.0.0', 'timestamp' => now()->toIso8601String() ] ]; } }

API Versioning

Implement API versioning to allow backward compatibility and smooth transitions:

// Method 1: URL Path Versioning (Recommended) // routes/api.php Route::prefix('v1')->group(function () { Route::apiResource('users', Api\V1\UserController::class); Route::apiResource('posts', Api\V1\PostController::class); }); Route::prefix('v2')->group(function () { Route::apiResource('users', Api\V2\UserController::class); Route::apiResource('posts', Api\V2\PostController::class); }); // URLs: /api/v1/users, /api/v2/users // Method 2: Header Versioning // app/Http/Middleware/ApiVersion.php namespace App\Http\Middleware; use Closure; use Illuminate\Http\Request; class ApiVersion { public function handle(Request $request, Closure $next, string $version) { $acceptedVersion = $request->header('Accept-Version', '1.0'); if ($acceptedVersion !== $version) { return response()->json([ 'error' => 'API version mismatch', 'requested' => $acceptedVersion, 'supported' => ['1.0', '2.0'] ], 406); } return $next($request); } } // Register middleware Route::middleware(['api.version:2.0'])->group(function () { // V2 routes }); // Method 3: Content Negotiation // Accept: application/vnd.myapp.v2+json
Versioning Best Practices:
  • Use URL path versioning for simplicity and clarity
  • Only increment versions for breaking changes
  • Maintain old versions for a deprecation period
  • Document version differences clearly
  • Use semantic versioning (major.minor.patch)

Rate Limiting

Protect your API from abuse with Laravel's built-in rate limiting:

// app/Providers/RouteServiceProvider.php use Illuminate\Cache\RateLimiting\Limit; use Illuminate\Http\Request; use Illuminate\Support\Facades\RateLimiter; public function boot(): void { // Configure rate limiters RateLimiter::for('api', function (Request $request) { return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip()); }); // Authenticated user limits RateLimiter::for('api-authenticated', function (Request $request) { return $request->user() ? Limit::perMinute(120)->by($request->user()->id) : Limit::perMinute(20)->by($request->ip()); }); // Premium tier limits RateLimiter::for('api-premium', function (Request $request) { if ($request->user()?->isPremium()) { return Limit::perMinute(1000)->by($request->user()->id); } return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip()); }); // Multiple limits RateLimiter::for('api-strict', function (Request $request) { return [ Limit::perMinute(10), // 10 per minute Limit::perHour(100), // 100 per hour Limit::perDay(1000), // 1000 per day ]; }); // Custom response when limit exceeded RateLimiter::for('api-custom', function (Request $request) { return Limit::perMinute(60) ->by($request->user()?->id ?: $request->ip()) ->response(function (Request $request, array $headers) { return response()->json([ 'error' => 'Rate limit exceeded', 'message' => 'Too many requests. Please slow down.', 'retry_after' => $headers['Retry-After'] ], 429, $headers); }); }); } // Apply to routes // routes/api.php Route::middleware(['throttle:api-premium'])->group(function () { Route::apiResource('users', UserController::class); }); // Apply to specific endpoints Route::post('/expensive-operation', [ApiController::class, 'process']) ->middleware('throttle:5,1'); // 5 requests per minute
Rate Limit Headers: Laravel automatically adds X-RateLimit-Limit, X-RateLimit-Remaining, and Retry-After headers to rate-limited responses, making it easy for API clients to handle limits gracefully.

Response Formatting

Create consistent, standardized API responses across your application:

// app/Traits/ApiResponse.php namespace App\Traits; use Illuminate\Http\JsonResponse; use Illuminate\Http\Response; trait ApiResponse { /** * Success response */ protected function success($data = null, string $message = null, int $code = Response::HTTP_OK): JsonResponse { $response = [ 'success' => true, 'message' => $message, 'data' => $data, ]; return response()->json(array_filter($response), $code); } /** * Error response */ protected function error(string $message, int $code = Response::HTTP_BAD_REQUEST, $errors = null): JsonResponse { $response = [ 'success' => false, 'message' => $message, 'errors' => $errors, ]; return response()->json(array_filter($response), $code); } /** * Validation error response */ protected function validationError($errors, string $message = 'Validation failed'): JsonResponse { return response()->json([ 'success' => false, 'message' => $message, 'errors' => $errors ], Response::HTTP_UNPROCESSABLE_ENTITY); } /** * Not found response */ protected function notFound(string $message = 'Resource not found'): JsonResponse { return $this->error($message, Response::HTTP_NOT_FOUND); } /** * Unauthorized response */ protected function unauthorized(string $message = 'Unauthorized'): JsonResponse { return $this->error($message, Response::HTTP_UNAUTHORIZED); } /** * Paginated response */ protected function paginated($data, string $message = null): JsonResponse { return response()->json([ 'success' => true, 'message' => $message, 'data' => $data->items(), 'meta' => [ 'current_page' => $data->currentPage(), 'last_page' => $data->lastPage(), 'per_page' => $data->perPage(), 'total' => $data->total(), 'from' => $data->firstItem(), 'to' => $data->lastItem(), ], 'links' => [ 'first' => $data->url(1), 'last' => $data->url($data->lastPage()), 'prev' => $data->previousPageUrl(), 'next' => $data->nextPageUrl(), ] ]); } } // Use in controller class UserController extends Controller { use ApiResponse; public function store(Request $request) { $validated = $request->validate([...]); $user = User::create($validated); return $this->success( new UserResource($user), 'User created successfully', Response::HTTP_CREATED ); } public function index() { $users = User::paginate(15); return $this->paginated($users, 'Users retrieved successfully'); } }

CORS Configuration

Configure Cross-Origin Resource Sharing to allow your API to be accessed from different domains:

// config/cors.php return [ 'paths' => ['api/*', 'sanctum/csrf-cookie'], 'allowed_methods' => ['*'], 'allowed_origins' => [ 'https://example.com', 'https://app.example.com', ], // For development only 'allowed_origins_patterns' => [ '/^https?:\/\/localhost(:\d+)?$/', ], 'allowed_headers' => ['*'], 'exposed_headers' => [ 'X-RateLimit-Limit', 'X-RateLimit-Remaining', ], 'max_age' => 0, 'supports_credentials' => true, ]; // Add CORS middleware to api routes // app/Http/Kernel.php protected $middlewareGroups = [ 'api' => [ \Illuminate\Routing\Middleware\SubstituteBindings::class, \App\Http\Middleware\HandleCors::class, // Add this ], ]; // Or use Laravel's built-in CORS handling // Ensure \Fruitcake\Cors\HandleCors is in $middleware
CORS Security: Never use 'allowed_origins' => ['*'] in production! Always specify exact domains. Use 'supports_credentials' => true only when necessary, as it requires specific origins (not wildcards).

API Documentation

Document your API using Laravel's built-in tools or popular packages:

// Install Scribe for API documentation composer require --dev knuckleswtf/scribe // Generate documentation php artisan scribe:generate // Add docblocks to controllers /** * List all users * * Returns a paginated list of users with optional search filtering. * * @group User Management * * @queryParam search string Filter users by name. Example: john * @queryParam per_page int Number of items per page. Example: 20 * * @response { * "data": [ * { * "id": 1, * "name": "John Doe", * "email": "john@example.com" * } * ], * "links": {...}, * "meta": {...} * } */ public function index(Request $request) { // ... } // Alternative: OpenAPI/Swagger composer require darkaonline/l5-swagger // Generate Swagger documentation php artisan l5-swagger:generate // Add Swagger annotations /** * @OA\Get( * path="/api/users", * summary="Get list of users", * tags={"Users"}, * @OA\Parameter( * name="page", * in="query", * description="Page number", * required=false, * @OA\Schema(type="integer") * ), * @OA\Response( * response=200, * description="Successful operation" * ) * ) */

Exercise 1: Build a Complete API Resource

Create a Post API with the following requirements:

  1. Create PostController with all CRUD operations
  2. Create PostResource with proper data transformation
  3. Implement filtering by category and author
  4. Add rate limiting (60 requests per minute)
  5. Include proper validation for store/update
  6. Return standardized JSON responses
  7. Add pagination with meta information

Exercise 2: Implement API Versioning

Create two versions of a User API:

  1. V1: Returns user with id, name, email
  2. V2: Returns user with uuid (instead of id), full_name (instead of name), email, created_at
  3. Set up routes for both versions
  4. Create separate controllers or use version detection
  5. Document the differences between versions

Exercise 3: Advanced Rate Limiting

Implement tiered rate limiting:

  1. Free users: 30 requests per minute
  2. Premium users: 120 requests per minute
  3. Admin users: No limits
  4. Anonymous users: 10 requests per minute by IP
  5. Return custom error messages with upgrade information
  6. Add X-RateLimit-Tier custom header

Summary

In this lesson, you learned how to design and build RESTful APIs with Laravel. You explored API routing conventions, resource controllers and transformations, versioning strategies, rate limiting, CORS configuration, and API documentation. These skills enable you to create professional, scalable APIs that follow industry best practices.

In the next lesson, we'll dive into Laravel Policies & Gates for fine-grained authorization control.