laravel-api
Auto-invoked skill
Build REST APIs with versioning and OpenAPI documentation
Trigger Keywords
This skill automatically activates when Claude detects these keywords:
api
rest
endpoint
openapi
json
resource
Overview
The laravel-api skill provides expertise for building production-ready REST APIs. When activated, Claude understands API versioning strategies, JSON resource transformations, rate limiting, authentication, and OpenAPI documentation.
What This Skill Provides
- API Versioning - URL-based (/api/v1/) or header-based versioning
- JSON Resources - Proper response transformation with relationships
- Request Validation - Form requests with API-specific error formatting
- Rate Limiting - Per-user and per-endpoint throttling
- Authentication - Sanctum, Passport, or API key strategies
- Documentation - OpenAPI/Swagger spec generation
Example Conversations
# Building a new API
"I need to create a REST API for managing products with filtering and pagination"
# Adding versioning
"Help me version my API - I need to support v1 and v2 simultaneously"
# Response formatting
"How should I structure my API responses for a collection with pagination?"
# Authentication
"Set up Sanctum authentication for my API with token abilities"
JSON Resource Implementation
Transform your models into consistent API responses:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class ProductResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'slug' => $this->slug,
'price' => [
'amount' => $this->price,
'formatted' => number_format($this->price, 2),
'currency' => 'USD',
],
'in_stock' => $this->stock_quantity > 0,
// Conditional relationships - only included if loaded
'category' => new CategoryResource($this->whenLoaded('category')),
'reviews' => ReviewResource::collection($this->whenLoaded('reviews')),
// Conditional counts
'reviews_count' => $this->whenCounted('reviews'),
// Conditional fields based on user
'cost' => $this->when($request->user()?->isAdmin(), $this->cost),
'created_at' => $this->created_at->toISOString(),
];
}
// Wrap single resources
public function with(Request $request): array
{
return [
'meta' => [
'api_version' => 'v1',
],
];
}
}
Resource Collection with Custom Pagination
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
class ProductCollection extends ResourceCollection
{
public $collects = ProductResource::class;
public function toArray(Request $request): array
{
return [
'data' => $this->collection,
'meta' => [
'total' => $this->total(),
'per_page' => $this->perPage(),
'current_page' => $this->currentPage(),
'last_page' => $this->lastPage(),
'has_more' => $this->hasMorePages(),
],
];
}
}
// Usage in controller
public function index(Request $request)
{
$products = Product::query()
->with(['category', 'reviews'])
->withCount('reviews')
->when($request->category, fn ($q, $cat) => $q->where('category_id', $cat))
->when($request->search, fn ($q, $s) => $q->where('name', 'like', "%{$s}%"))
->paginate($request->per_page ?? 15);
return new ProductCollection($products);
}
API Response Patterns
The skill ensures consistent response structures:
// Collection response with pagination
{
"data": [
{"id": 1, "name": "Product 1", "price": {"amount": 29.99}},
{"id": 2, "name": "Product 2", "price": {"amount": 49.99}}
],
"meta": {
"total": 100,
"per_page": 15,
"current_page": 1,
"last_page": 7,
"has_more": true
},
"links": {
"first": "/api/v1/products?page=1",
"last": "/api/v1/products?page=7",
"next": "/api/v1/products?page=2",
"prev": null
}
}
// Error response (422 Validation)
{
"message": "The given data was invalid.",
"errors": {
"email": ["The email field is required."],
"password": ["The password must be at least 8 characters."]
}
}
API Versioning Setup
<?php
// routes/api.php - URL-based versioning (recommended)
use Illuminate\Support\Facades\Route;
Route::prefix('v1')->group(function () {
Route::apiResource('products', App\Http\Controllers\Api\V1\ProductController::class);
Route::apiResource('orders', App\Http\Controllers\Api\V1\OrderController::class);
});
Route::prefix('v2')->group(function () {
Route::apiResource('products', App\Http\Controllers\Api\V2\ProductController::class);
});
// app/Http/Controllers/Api/V1/ProductController.php
namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Controller;
use App\Http\Resources\V1\ProductResource;
use App\Models\Product;
class ProductController extends Controller
{
public function index()
{
return ProductResource::collection(
Product::with('category')->paginate()
);
}
public function show(Product $product)
{
return new ProductResource($product->load('category', 'reviews'));
}
}
Rate Limiting Configuration
<?php
// app/Providers/AppServiceProvider.php
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Facades\RateLimiter;
public function boot(): void
{
// Default API rate limit
RateLimiter::for('api', function ($request) {
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});
// Stricter limit for expensive operations
RateLimiter::for('exports', function ($request) {
return Limit::perHour(10)->by($request->user()->id);
});
// Different limits by subscription tier
RateLimiter::for('tiered', function ($request) {
$user = $request->user();
return match ($user?->subscription_tier) {
'enterprise' => Limit::none(),
'pro' => Limit::perMinute(1000)->by($user->id),
default => Limit::perMinute(100)->by($user?->id ?: $request->ip()),
};
});
}
// Apply in routes
Route::middleware(['throttle:api'])->group(function () {
Route::apiResource('products', ProductController::class);
});
Route::middleware(['throttle:exports'])->group(function () {
Route::get('/export/orders', [ExportController::class, 'orders']);
});
Sanctum API Authentication
<?php
// routes/api.php
Route::post('/auth/token', function (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.'],
]);
}
// Create token with abilities (permissions)
$token = $user->createToken($request->device_name, [
'products:read',
'products:write',
'orders:read',
]);
return response()->json([
'token' => $token->plainTextToken,
'expires_at' => now()->addDays(30)->toISOString(),
]);
});
// Protected routes
Route::middleware('auth:sanctum')->group(function () {
Route::get('/user', fn (Request $request) => $request->user());
// Check token abilities
Route::middleware('ability:products:write')->group(function () {
Route::post('/products', [ProductController::class, 'store']);
Route::put('/products/{product}', [ProductController::class, 'update']);
});
});
Testing API Endpoints
<?php
use App\Models\Product;
use App\Models\User;
it('lists products with pagination', function () {
Product::factory()->count(20)->create();
$response = $this->getJson('/api/v1/products?per_page=10');
$response
->assertOk()
->assertJsonCount(10, 'data')
->assertJsonStructure([
'data' => [['id', 'name', 'price']],
'meta' => ['total', 'per_page', 'current_page'],
]);
});
it('requires authentication for protected routes', function () {
$this->postJson('/api/v1/products', ['name' => 'Test'])
->assertUnauthorized();
});
it('creates product with valid token', function () {
$user = User::factory()->create();
$token = $user->createToken('test', ['products:write'])->plainTextToken;
$response = $this->withToken($token)
->postJson('/api/v1/products', [
'name' => 'New Product',
'price' => 29.99,
]);
$response->assertCreated()
->assertJsonPath('data.name', 'New Product');
});
it('respects rate limits', function () {
$user = User::factory()->create();
// Exhaust rate limit
for ($i = 0; $i < 60; $i++) {
$this->actingAs($user)->getJson('/api/v1/products');
}
// Next request should be rate limited
$this->actingAs($user)
->getJson('/api/v1/products')
->assertStatus(429);
});
Common Patterns
| Pattern | When to Use |
|---|---|
JsonResource |
Single model transformation |
ResourceCollection |
Collections with custom pagination |
whenLoaded() |
Conditional relationship inclusion |
QueryBuilder |
Filtering, sorting, including via query params |
Common Pitfalls
- N+1 in resources - Always eager load relationships before transforming
- Exposing sensitive data - Explicitly define what to include, don't use
toArray() - Inconsistent errors - Use exception handlers for uniform error responses
- Missing rate limits - Always protect public endpoints
Related Commands
- /laravel-agent:api:make - Create API resources
- /laravel-agent:api:docs - Generate OpenAPI documentation
Related Agent
- laravel-api-builder - Builds complete API resources