REST API Development

API Documentation with OpenAPI/Swagger

18 min Lesson 19 of 35

API Documentation with OpenAPI/Swagger

Comprehensive, accurate, and interactive documentation is crucial for API adoption and developer experience. OpenAPI (formerly known as Swagger) is the industry-standard specification for describing RESTful APIs. In this lesson, we'll explore how to create professional API documentation using OpenAPI specifications, annotations, Swagger UI, and automated generation tools.

Why API Documentation Matters

Poor or missing documentation is one of the primary reasons developers abandon APIs:

  • Developer Experience: Well-documented APIs reduce integration time from weeks to days
  • Self-Service: Good docs reduce support requests and allow developers to solve problems independently
  • Adoption: Complete documentation directly correlates with higher API adoption rates
  • Maintenance: Documentation helps your own team understand and maintain the API
  • Testing: Interactive docs enable developers to test endpoints without writing code
Industry Research: Studies show that 70% of developers consider documentation quality when choosing an API. Stripe, Twilio, and GitHub are praised for their exceptional documentation, which has directly contributed to their developer ecosystem growth.

OpenAPI Specification Overview

The OpenAPI Specification (OAS) is a JSON or YAML format for describing the entire surface area of your API. A complete OpenAPI document includes:

  • General Information: API title, description, version, contact info, license
  • Servers: Base URLs for production, staging, development environments
  • Paths: All available endpoints with their HTTP methods
  • Parameters: Query strings, path variables, headers, request bodies
  • Responses: Status codes, response schemas, examples
  • Schemas: Data models and their properties
  • Security: Authentication methods (API keys, OAuth, JWT)
  • Tags: Logical groupings of related endpoints

Basic OpenAPI Document Structure

Here's a complete example of an OpenAPI 3.0 specification document:

# openapi.yaml openapi: 3.0.3 info: title: E-Commerce API description: | Complete REST API for managing an e-commerce platform. ## Features - Product catalog management - Order processing - User authentication - Shopping cart operations ## Rate Limiting - 1000 requests per hour for authenticated users - 100 requests per hour for anonymous users version: 2.1.0 contact: name: API Support email: api@example.com url: https://example.com/support license: name: MIT url: https://opensource.org/licenses/MIT servers: - url: https://api.example.com/v2 description: Production server - url: https://staging-api.example.com/v2 description: Staging server - url: http://localhost:8000/api/v2 description: Development server tags: - name: Products description: Product catalog operations - name: Orders description: Order management - name: Authentication description: User authentication endpoints paths: /products: get: tags: - Products summary: Get all products description: Retrieve a paginated list of products with optional filtering operationId: getProducts parameters: - name: page in: query description: Page number for pagination required: false schema: type: integer minimum: 1 default: 1 - name: per_page in: query description: Number of items per page required: false schema: type: integer minimum: 1 maximum: 100 default: 20 - name: category in: query description: Filter by category ID required: false schema: type: integer - name: min_price in: query description: Minimum price filter required: false schema: type: number format: float - name: max_price in: query description: Maximum price filter required: false schema: type: number format: float - name: search in: query description: Search products by name or description required: false schema: type: string responses: '200': description: Successful response content: application/json: schema: type: object properties: data: type: array items: $ref: '#/components/schemas/Product' meta: $ref: '#/components/schemas/PaginationMeta' '400': description: Bad request - invalid parameters content: application/json: schema: $ref: '#/components/schemas/Error' post: tags: - Products summary: Create a new product description: Create a new product (requires authentication) operationId: createProduct security: - bearerAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ProductInput' responses: '201': description: Product created successfully content: application/json: schema: type: object properties: data: $ref: '#/components/schemas/Product' '401': description: Unauthorized '422': description: Validation error content: application/json: schema: $ref: '#/components/schemas/ValidationError' /products/{id}: get: tags: - Products summary: Get product by ID description: Retrieve detailed information about a specific product operationId: getProduct parameters: - name: id in: path description: Product ID required: true schema: type: integer responses: '200': description: Successful response content: application/json: schema: type: object properties: data: $ref: '#/components/schemas/Product' '404': description: Product not found components: schemas: Product: type: object properties: id: type: integer example: 123 name: type: string example: "Wireless Headphones" description: type: string example: "Premium noise-cancelling wireless headphones" price: type: number format: float example: 199.99 category: $ref: '#/components/schemas/Category' images: type: array items: type: string format: uri example: - "https://example.com/images/product1.jpg" stock: type: integer example: 50 created_at: type: string format: date-time updated_at: type: string format: date-time ProductInput: type: object required: - name - price - category_id properties: name: type: string minLength: 3 maxLength: 255 description: type: string price: type: number format: float minimum: 0 category_id: type: integer stock: type: integer minimum: 0 default: 0 Category: type: object properties: id: type: integer name: type: string slug: type: string PaginationMeta: type: object properties: current_page: type: integer per_page: type: integer total: type: integer last_page: type: integer Error: type: object properties: message: type: string errors: type: object ValidationError: type: object properties: message: type: string errors: type: object additionalProperties: type: array items: type: string securitySchemes: bearerAuth: type: http scheme: bearer bearerFormat: JWT </div>

Laravel OpenAPI Annotations with L5-Swagger

Instead of writing YAML manually, you can use PHP annotations directly in your controllers. Laravel's L5-Swagger package generates OpenAPI documentation from your code:

Installation

# 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>

Annotated Controller Example

<?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:
  1. Install L5-Swagger in your Laravel project using Composer
  2. Add OpenAPI annotations to at least 3 controller methods (GET, POST, PUT/PATCH)
  3. Define schema annotations for 2 models with all their properties
  4. Document authentication using bearer tokens with securitySchemes
  5. Add parameter documentation including query strings, path parameters, and request bodies
  6. Document all possible response codes (200, 201, 400, 401, 404, 422, 500)
  7. Generate the OpenAPI specification using php artisan l5-swagger:generate
  8. Access Swagger UI and test your endpoints interactively
  9. Add response examples for successful and error cases
  10. 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.