# Install L5-Swagger
composer require darkaonline/l5-swagger
# Publish configuration
php artisan vendor:publish --provider "L5Swagger\L5SwaggerServiceProvider"
# Generate documentation
php artisan l5-swagger:generate
</div>
<?php
namespace App\Http\Controllers\Api;
use App\Models\Product;
use Illuminate\Http\Request;
use App\Http\Resources\ProductResource;
/**
* @OA\Info(
* title="E-Commerce API",
* version="2.1.0",
* description="Complete REST API for e-commerce platform",
* @OA\Contact(
* email="api@example.com",
* name="API Support"
* )
* )
*
* @OA\Server(
* url="https://api.example.com/v2",
* description="Production server"
* )
*
* @OA\SecurityScheme(
* securityScheme="bearerAuth",
* type="http",
* scheme="bearer",
* bearerFormat="JWT"
* )
*/
class ProductController extends Controller
{
/**
* @OA\Get(
* path="/products",
* tags={"Products"},
* summary="Get all products",
* description="Retrieve paginated list of products with filtering",
* operationId="getProducts",
* @OA\Parameter(
* name="page",
* in="query",
* description="Page number",
* required=false,
* @OA\Schema(type="integer", minimum=1, default=1)
* ),
* @OA\Parameter(
* name="per_page",
* in="query",
* description="Items per page",
* required=false,
* @OA\Schema(type="integer", minimum=1, maximum=100, default=20)
* ),
* @OA\Parameter(
* name="category",
* in="query",
* description="Filter by category ID",
* required=false,
* @OA\Schema(type="integer")
* ),
* @OA\Parameter(
* name="search",
* in="query",
* description="Search term",
* required=false,
* @OA\Schema(type="string")
* ),
* @OA\Response(
* response=200,
* description="Successful response",
* @OA\JsonContent(
* @OA\Property(
* property="data",
* type="array",
* @OA\Items(ref="#/components/schemas/Product")
* ),
* @OA\Property(
* property="meta",
* ref="#/components/schemas/PaginationMeta"
* )
* )
* ),
* @OA\Response(
* response=400,
* description="Bad request"
* )
* )
*/
public function index(Request $request)
{
$query = Product::with('category');
if ($request->has('category')) {
$query->where('category_id', $request->category);
}
if ($request->has('search')) {
$query->where('name', 'LIKE', "%{$request->search}%");
}
$products = $query->paginate($request->get('per_page', 20));
return ProductResource::collection($products);
}
/**
* @OA\Post(
* path="/products",
* tags={"Products"},
* summary="Create new product",
* description="Create a new product (authentication required)",
* operationId="createProduct",
* security={{"bearerAuth":{}}},
* @OA\RequestBody(
* required=true,
* @OA\JsonContent(
* required={"name", "price", "category_id"},
* @OA\Property(property="name", type="string", minLength=3, maxLength=255, example="Wireless Mouse"),
* @OA\Property(property="description", type="string", example="Ergonomic wireless mouse"),
* @OA\Property(property="price", type="number", format="float", minimum=0, example=29.99),
* @OA\Property(property="category_id", type="integer", example=5),
* @OA\Property(property="stock", type="integer", minimum=0, default=0, example=100)
* )
* ),
* @OA\Response(
* response=201,
* description="Product created successfully",
* @OA\JsonContent(
* @OA\Property(property="data", ref="#/components/schemas/Product")
* )
* ),
* @OA\Response(
* response=401,
* description="Unauthorized"
* ),
* @OA\Response(
* response=422,
* description="Validation error",
* @OA\JsonContent(ref="#/components/schemas/ValidationError")
* )
* )
*/
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|min:3|max:255',
'description' => 'nullable|string',
'price' => 'required|numeric|min:0',
'category_id' => 'required|exists:categories,id',
'stock' => 'nullable|integer|min:0',
]);
$product = Product::create($validated);
return new ProductResource($product);
}
/**
* @OA\Get(
* path="/products/{id}",
* tags={"Products"},
* summary="Get product by ID",
* description="Retrieve detailed product information",
* operationId="getProduct",
* @OA\Parameter(
* name="id",
* in="path",
* description="Product ID",
* required=true,
* @OA\Schema(type="integer")
* ),
* @OA\Response(
* response=200,
* description="Successful response",
* @OA\JsonContent(
* @OA\Property(property="data", ref="#/components/schemas/Product")
* )
* ),
* @OA\Response(
* response=404,
* description="Product not found"
* )
* )
*/
public function show($id)
{
$product = Product::with('category', 'images')->findOrFail($id);
return new ProductResource($product);
}
}
</div>
Schema Definitions with Annotations
Define reusable schemas using model annotations:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
/**
* @OA\Schema(
* schema="Product",
* title="Product",
* description="Product model",
* required={"id", "name", "price"},
* @OA\Property(
* property="id",
* type="integer",
* description="Product ID",
* example=123
* ),
* @OA\Property(
* property="name",
* type="string",
* description="Product name",
* example="Wireless Headphones"
* ),
* @OA\Property(
* property="description",
* type="string",
* description="Product description",
* example="Premium noise-cancelling headphones"
* ),
* @OA\Property(
* property="price",
* type="number",
* format="float",
* description="Product price",
* example=199.99
* ),
* @OA\Property(
* property="category",
* ref="#/components/schemas/Category"
* ),
* @OA\Property(
* property="stock",
* type="integer",
* description="Available stock",
* example=50
* ),
* @OA\Property(
* property="created_at",
* type="string",
* format="date-time"
* ),
* @OA\Property(
* property="updated_at",
* type="string",
* format="date-time"
* )
* )
*/
class Product extends Model
{
protected $fillable = ['name', 'description', 'price', 'category_id', 'stock'];
public function category()
{
return $this->belongsTo(Category::class);
}
}
/**
* @OA\Schema(
* schema="PaginationMeta",
* title="Pagination Metadata",
* @OA\Property(property="current_page", type="integer", example=1),
* @OA\Property(property="per_page", type="integer", example=20),
* @OA\Property(property="total", type="integer", example=150),
* @OA\Property(property="last_page", type="integer", example=8)
* )
*/
/**
* @OA\Schema(
* schema="ValidationError",
* title="Validation Error",
* @OA\Property(property="message", type="string", example="The given data was invalid."),
* @OA\Property(
* property="errors",
* type="object",
* @OA\Property(
* property="name",
* type="array",
* @OA\Items(type="string", example="The name field is required.")
* )
* )
* )
*/
</div>
Organization Tip: Place @OA\Info and @OA\Server annotations in your base controller or a dedicated documentation controller. Place @OA\Schema annotations in your models or resource classes for better organization.
Swagger UI Integration
After generating your OpenAPI specification, L5-Swagger automatically creates an interactive Swagger UI interface:
# Configuration: config/l5-swagger.php
return [
'default' => 'default',
'documentations' => [
'default' => [
'api' => [
'title' => 'E-Commerce API Documentation',
],
'routes' => [
'api' => 'api/documentation',
],
'paths' => [
'docs' => storage_path('api-docs'),
'docs_json' => 'api-docs.json',
'docs_yaml' => 'api-docs.yaml',
'annotations' => [
base_path('app'),
],
],
],
],
];
# Generate documentation
php artisan l5-swagger:generate
# Access Swagger UI at:
# http://localhost:8000/api/documentation
</div>
Advanced Documentation Features
Response Examples
<?php
/**
* @OA\Response(
* response=200,
* description="Successful response",
* @OA\JsonContent(
* @OA\Property(
* property="data",
* type="object",
* example={
* "id": 123,
* "name": "Wireless Headphones",
* "price": 199.99,
* "category": {
* "id": 5,
* "name": "Electronics"
* },
* "stock": 50
* }
* )
* )
* )
*/
</div>
Enum Parameters
<?php
/**
* @OA\Parameter(
* name="sort",
* in="query",
* description="Sort order",
* required=false,
* @OA\Schema(
* type="string",
* enum={"price_asc", "price_desc", "name_asc", "name_desc", "newest"},
* default="newest"
* )
* )
*/
</div>
File Upload Documentation
<?php
/**
* @OA\Post(
* path="/products/{id}/image",
* tags={"Products"},
* summary="Upload product image",
* @OA\Parameter(
* name="id",
* in="path",
* required=true,
* @OA\Schema(type="integer")
* ),
* @OA\RequestBody(
* required=true,
* @OA\MediaType(
* mediaType="multipart/form-data",
* @OA\Schema(
* @OA\Property(
* property="image",
* type="string",
* format="binary",
* description="Product image file (JPEG, PNG)"
* )
* )
* )
* ),
* @OA\Response(
* response=200,
* description="Image uploaded successfully"
* )
* )
*/
public function uploadImage(Request $request, $id)
{
$request->validate([
'image' => 'required|image|max:5120', // 5MB
]);
// Upload logic...
}
</div>
Auto-Generation Tools and Alternatives
Besides L5-Swagger, several tools can generate OpenAPI documentation:
1. Scribe - Laravel API Documentation
# Install Scribe
composer require --dev knuckleswtf/scribe
# Publish config
php artisan vendor:publish --tag=scribe-config
# Generate docs
php artisan scribe:generate
</div>
Scribe offers advantages over L5-Swagger:
- Automatically infers types from route definitions
- Generates example responses from actual API calls
- Creates beautiful static HTML documentation
- Supports Postman collection export
2. API Platform
For Laravel projects, you can also use API Platform, which auto-generates OpenAPI docs from your Eloquent models.
Keep Documentation Updated: Outdated documentation is worse than no documentation. Integrate doc generation into your CI/CD pipeline to ensure docs stay synchronized with code changes.
Practice Exercise:
- Install L5-Swagger in your Laravel project using Composer
- Add OpenAPI annotations to at least 3 controller methods (GET, POST, PUT/PATCH)
- Define schema annotations for 2 models with all their properties
- Document authentication using bearer tokens with securitySchemes
- Add parameter documentation including query strings, path parameters, and request bodies
- Document all possible response codes (200, 201, 400, 401, 404, 422, 500)
- Generate the OpenAPI specification using php artisan l5-swagger:generate
- Access Swagger UI and test your endpoints interactively
- Add response examples for successful and error cases
- Document file upload endpoints with multipart/form-data
Documentation Best Practices
- Be Comprehensive: Document every endpoint, parameter, and response
- Use Clear Descriptions: Explain what each endpoint does and why someone would use it
- Provide Examples: Include realistic request/response examples
- Document Errors: Explain all possible error codes and their causes
- Include Rate Limits: Document throttling and rate limiting policies
- Show Authentication: Clearly explain how to authenticate requests
- Version Your Docs: Maintain documentation for all supported API versions
- Add Code Samples: Provide examples in multiple programming languages
- Keep It Updated: Use automated tools to regenerate docs with every change
- Test Interactive Features: Verify that "Try it out" functionality works correctly
Pro Tip: Consider using tools like Stoplight Studio or SwaggerHub for collaborative API documentation editing. These platforms provide visual editors, version control, and team collaboration features that make maintaining API docs much easier.
Summary
OpenAPI/Swagger documentation transforms your API from a black box into an accessible, self-documenting interface. By using annotation-based tools like L5-Swagger or automated generators like Scribe, you can maintain comprehensive documentation that stays synchronized with your codebase. Interactive Swagger UI allows developers to explore and test your API without writing any code, dramatically improving developer experience and adoption rates.