Advanced API Development
Advanced API Development
Building production-ready APIs requires advanced techniques beyond basic CRUD operations. In this lesson, we'll explore API versioning, HATEOAS principles, content negotiation, sophisticated rate limiting strategies, and comprehensive API documentation practices that make your APIs maintainable and developer-friendly.
API Versioning Strategies
API versioning allows you to introduce breaking changes without disrupting existing clients. Laravel provides flexible routing mechanisms for implementing various versioning strategies.
<?php
// routes/api.php - URI Versioning (most common)
Route::prefix('v1')->group(function () {
Route::apiResource('users', App\Http\Controllers\Api\V1\UserController::class);
Route::apiResource('posts', App\Http\Controllers\Api\V1\PostController::class);
});
Route::prefix('v2')->group(function () {
Route::apiResource('users', App\Http\Controllers\Api\V2\UserController::class);
Route::apiResource('posts', App\Http\Controllers\Api\V2\PostController::class);
});
// Header Versioning (Accept header)
// routes/api.php
Route::middleware(['api.version:v1'])->group(function () {
Route::apiResource('users', App\Http\Controllers\Api\V1\UserController::class);
});
Route::middleware(['api.version:v2'])->group(function () {
Route::apiResource('users', App\Http\Controllers\Api\V2\UserController::class);
});
// 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)
{
$acceptHeader = $request->header('Accept', '');
if (str_contains($acceptHeader, "version={$version}")) {
return $next($request);
}
return response()->json([
'message' => "API version {$version} required in Accept header",
'example' => "Accept: application/json;version={$version}"
], 406);
}
}
// Query Parameter Versioning
// /api/users?version=v1
Route::middleware(['api.version.query'])->group(function () {
Route::apiResource('users', function (Request $request) {
$version = $request->query('version', 'v1');
$controller = "App\Http\Controllers\Api\{$version}\UserController";
return app($controller)->index($request);
});
});
HATEOAS (Hypermedia as the Engine of Application State)
HATEOAS is a REST constraint that makes APIs self-documenting by including hypermedia links in responses, allowing clients to discover available actions dynamically.
<?php
// app/Http/Resources/UserResource.php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class UserResource extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'created_at' => $this->created_at->toIso8601String(),
'links' => [
'self' => [
'href' => route('api.users.show', $this->id),
'method' => 'GET',
],
'update' => [
'href' => route('api.users.update', $this->id),
'method' => 'PUT',
],
'delete' => [
'href' => route('api.users.destroy', $this->id),
'method' => 'DELETE',
],
'posts' => [
'href' => route('api.users.posts.index', $this->id),
'method' => 'GET',
],
],
'_embedded' => [
'latest_post' => new PostResource($this->whenLoaded('latestPost')),
],
];
}
}
// app/Http/Resources/PostCollection.php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\ResourceCollection;
class PostCollection extends ResourceCollection
{
public function toArray($request)
{
return [
'data' => $this->collection,
'links' => [
'self' => [
'href' => $request->fullUrl(),
],
'first' => [
'href' => $this->url(1),
],
'last' => [
'href' => $this->url($this->lastPage()),
],
'prev' => [
'href' => $this->previousPageUrl(),
],
'next' => [
'href' => $this->nextPageUrl(),
],
],
'meta' => [
'current_page' => $this->currentPage(),
'last_page' => $this->lastPage(),
'per_page' => $this->perPage(),
'total' => $this->total(),
],
];
}
}
// Usage in Controller
public function show(User $user)
{
$user->load('latestPost');
return new UserResource($user);
}
// Example JSON Response:
{
"id": 1,
"name": "John Doe",
"email": "john@example.com",
"created_at": "2024-01-15T10:30:00Z",
"links": {
"self": {"href": "https://api.example.com/v1/users/1", "method": "GET"},
"update": {"href": "https://api.example.com/v1/users/1", "method": "PUT"},
"delete": {"href": "https://api.example.com/v1/users/1", "method": "DELETE"},
"posts": {"href": "https://api.example.com/v1/users/1/posts", "method": "GET"}
},
"_embedded": {
"latest_post": {
"id": 42,
"title": "My Latest Article",
"links": {...}
}
}
}
Content Negotiation
Content negotiation allows your API to serve different response formats (JSON, XML, CSV) based on client preferences specified in the Accept header.
<?php
// app/Http/Middleware/ContentNegotiation.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Response;
class ContentNegotiation
{
public function handle(Request $request, Closure $next)
{
$response = $next($request);
$acceptHeader = $request->header('Accept', 'application/json');
// Parse Accept header
if (str_contains($acceptHeader, 'application/xml')) {
return $this->convertToXml($response);
} elseif (str_contains($acceptHeader, 'text/csv')) {
return $this->convertToCsv($response);
}
// Default to JSON
return $response;
}
protected function convertToXml($response)
{
$data = $response->getData(true);
$xml = new \SimpleXMLElement('<?xml version="1.0"?><root></root>');
$this->arrayToXml($data, $xml);
return Response::make($xml->asXML(), $response->status())
->header('Content-Type', 'application/xml');
}
protected function arrayToXml($data, &$xml)
{
foreach ($data as $key => $value) {
if (is_array($value)) {
$subNode = $xml->addChild($key);
$this->arrayToXml($value, $subNode);
} else {
$xml->addChild($key, htmlspecialchars($value));
}
}
}
protected function convertToCsv($response)
{
$data = $response->getData(true);
// Flatten data if needed
if (isset($data['data']) && is_array($data['data'])) {
$data = $data['data'];
}
$csv = fopen('php://temp', 'r+');
// Write headers
if (!empty($data)) {
fputcsv($csv, array_keys(reset($data)));
// Write rows
foreach ($data as $row) {
fputcsv($csv, $row);
}
}
rewind($csv);
$output = stream_get_contents($csv);
fclose($csv);
return Response::make($output, $response->status())
->header('Content-Type', 'text/csv')
->header('Content-Disposition', 'attachment; filename="export.csv"');
}
}
// Register middleware in app/Http/Kernel.php
protected $middlewareGroups = [
'api' => [
\App\Http\Middleware\ContentNegotiation::class,
// ... other middleware
],
];
Advanced Rate Limiting Strategies
Laravel's throttle middleware can be customized for sophisticated rate limiting based on user tiers, endpoint sensitivity, and dynamic limits.
<?php
// app/Providers/RouteServiceProvider.php
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
public function boot()
{
// Tier-based rate limiting
RateLimiter::for('api', function (Request $request) {
$user = $request->user();
if (!$user) {
// Anonymous users: 60 requests per minute
return Limit::perMinute(60)->by($request->ip());
}
// Rate limit based on user tier
return match($user->tier) {
'free' => Limit::perMinute(100)->by($user->id),
'pro' => Limit::perMinute(1000)->by($user->id),
'enterprise' => Limit::perMinute(10000)->by($user->id),
default => Limit::perMinute(100)->by($user->id),
};
});
// Endpoint-specific rate limiting
RateLimiter::for('expensive-operations', function (Request $request) {
return Limit::perMinute(10)->by($request->user()?->id ?: $request->ip())
->response(function (Request $request, array $headers) {
return response()->json([
'message' => 'Too many expensive operations. Please try again later.',
'retry_after' => $headers['Retry-After'],
], 429, $headers);
});
});
// Dynamic rate limiting with multiple limits
RateLimiter::for('flexible', function (Request $request) {
return [
Limit::perMinute(60)->by($request->ip()),
Limit::perDay(1000)->by($request->user()?->id ?: $request->ip()),
];
});
// Time-based rate limiting (business hours vs off-hours)
RateLimiter::for('time-based', function (Request $request) {
$hour = now()->hour;
$isBusinessHours = $hour >= 9 && $hour < 17;
$limit = $isBusinessHours ? 100 : 500; // More generous off-hours
return Limit::perMinute($limit)->by($request->user()?->id ?: $request->ip());
});
}
// routes/api.php - Apply rate limiting
Route::middleware(['throttle:api'])->group(function () {
Route::get('/users', [UserController::class, 'index']);
Route::get('/posts', [PostController::class, 'index']);
});
Route::middleware(['throttle:expensive-operations'])->group(function () {
Route::post('/reports/generate', [ReportController::class, 'generate']);
Route::post('/exports/large', [ExportController::class, 'exportLarge']);
});
// Custom throttle middleware with Redis
// app/Http/Middleware/ApiThrottle.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Redis;
class ApiThrottle
{
public function handle(Request $request, Closure $next, int $maxAttempts = 60)
{
$key = $this->resolveRequestSignature($request);
$attempts = Redis::incr($key);
if ($attempts === 1) {
Redis::expire($key, 60); // 1 minute window
}
if ($attempts > $maxAttempts) {
$retryAfter = Redis::ttl($key);
return response()->json([
'message' => 'Rate limit exceeded.',
'retry_after' => $retryAfter,
], 429, [
'X-RateLimit-Limit' => $maxAttempts,
'X-RateLimit-Remaining' => 0,
'Retry-After' => $retryAfter,
]);
}
$response = $next($request);
return $response->withHeaders([
'X-RateLimit-Limit' => $maxAttempts,
'X-RateLimit-Remaining' => max(0, $maxAttempts - $attempts),
]);
}
protected function resolveRequestSignature(Request $request): string
{
$user = $request->user();
$identifier = $user ? "user:{$user->id}" : "ip:{$request->ip()}";
return "throttle:{$identifier}:{$request->path()}";
}
}
API Documentation with OpenAPI/Swagger
Comprehensive API documentation is essential for developer experience. Laravel integrates seamlessly with OpenAPI specifications using packages like L5-Swagger.
# Install L5-Swagger
composer require darkaonline/l5-swagger
# Publish configuration
php artisan vendor:publish --provider "L5Swagger\L5SwaggerServiceProvider"
# Generate documentation
php artisan l5-swagger:generate
<?php
// app/Http/Controllers/Api/V1/UserController.php
namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Controller;
use App\Http\Resources\UserResource;
use App\Models\User;
use Illuminate\Http\Request;
/**
* @OA\Info(
* title="My Application API",
* version="1.0.0",
* description="RESTful API for My Application",
* @OA\Contact(
* email="api@example.com"
* )
* )
* @OA\Server(
* url="https://api.example.com/v1",
* description="Production API Server"
* )
* @OA\SecurityScheme(
* securityScheme="bearerAuth",
* type="http",
* scheme="bearer",
* bearerFormat="JWT"
* )
*/
class UserController extends Controller
{
/**
* @OA\Get(
* path="/users",
* summary="Get list of users",
* tags={"Users"},
* security={{"bearerAuth":{}}},
* @OA\Parameter(
* name="page",
* in="query",
* description="Page number",
* required=false,
* @OA\Schema(type="integer", example=1)
* ),
* @OA\Parameter(
* name="per_page",
* in="query",
* description="Items per page",
* required=false,
* @OA\Schema(type="integer", example=15)
* ),
* @OA\Response(
* response=200,
* description="Successful operation",
* @OA\JsonContent(
* @OA\Property(property="data", type="array",
* @OA\Items(ref="#/components/schemas/User")
* ),
* @OA\Property(property="links", type="object"),
* @OA\Property(property="meta", type="object")
* )
* ),
* @OA\Response(
* response=401,
* description="Unauthenticated"
* ),
* @OA\Response(
* response=429,
* description="Too Many Requests"
* )
* )
*/
public function index(Request $request)
{
$users = User::paginate($request->input('per_page', 15));
return UserResource::collection($users);
}
/**
* @OA\Post(
* path="/users",
* summary="Create a new user",
* tags={"Users"},
* security={{"bearerAuth":{}}},
* @OA\RequestBody(
* required=true,
* @OA\JsonContent(
* required={"name","email","password"},
* @OA\Property(property="name", type="string", example="John Doe"),
* @OA\Property(property="email", type="string", format="email", example="john@example.com"),
* @OA\Property(property="password", type="string", format="password", example="secret123")
* )
* ),
* @OA\Response(
* response=201,
* description="User created successfully",
* @OA\JsonContent(ref="#/components/schemas/User")
* ),
* @OA\Response(
* response=422,
* description="Validation error"
* )
* )
*/
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users',
'password' => 'required|min:8',
]);
$user = User::create($validated);
return new UserResource($user);
}
}
/**
* @OA\Schema(
* schema="User",
* type="object",
* title="User",
* required={"id", "name", "email"},
* @OA\Property(property="id", type="integer", example=1),
* @OA\Property(property="name", type="string", example="John Doe"),
* @OA\Property(property="email", type="string", format="email", example="john@example.com"),
* @OA\Property(property="created_at", type="string", format="date-time"),
* @OA\Property(property="links", type="object",
* @OA\Property(property="self", type="object",
* @OA\Property(property="href", type="string"),
* @OA\Property(property="method", type="string")
* )
* )
* )
*/
Exercise 1: Implement API Versioning
Create a versioned API with two versions:
- Version 1: Returns user data with basic fields (id, name, email)
- Version 2: Returns user data with additional fields (profile_photo, bio, social_links)
- Implement URI-based versioning (/api/v1/users and /api/v2/users)
- Add proper deprecation warnings in v1 responses
- Test both versions return different data structures
Exercise 2: Build HATEOAS-Compliant Resources
Transform your existing API resources to be HATEOAS-compliant:
- Create a PostResource that includes hypermedia links (self, update, delete, author, comments)
- Add conditional links based on user permissions (only show delete link if user can delete)
- Implement a CommentCollection with pagination links
- Add embedded resources for related data
- Test that clients can navigate your API using only the links provided
Exercise 3: Advanced Rate Limiting
Implement a multi-tier rate limiting system:
- Create three user tiers: free (100 req/min), pro (1000 req/min), enterprise (unlimited)
- Implement endpoint-specific limits: expensive operations (10 req/min), standard operations (tier-based)
- Add rate limit headers (X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After) to all responses
- Create a custom rate limiter that tracks daily quotas in addition to per-minute limits
- Test rate limiting across multiple requests and verify proper 429 responses
Summary
In this lesson, you've mastered advanced API development techniques including versioning strategies for backward compatibility, HATEOAS principles for self-documenting APIs, content negotiation for flexible response formats, sophisticated rate limiting for protecting resources, and comprehensive OpenAPI documentation for excellent developer experience. These patterns are essential for building enterprise-grade APIs that scale and evolve gracefully.