Skip to content

API Documentation Guide

Guide for generating well-organized API documentation using Scribe and Scalar UI.

Overview

Our API documentation uses:

  • Scribe: Generates OpenAPI spec from Laravel controller annotations
  • Scalar: Modern UI for rendering the OpenAPI spec with visual hierarchy
  • Custom x-tagGroups: Organizes endpoints into logical groups with arrow notation

Tag Naming Convention

Use the format: "Aggregate - Category"

Examples:

  • Products - Management - CRUD operations for products
  • Products - Lifecycle - Activation, deactivation, discontinuation
  • Inventory - Stock Management - Stock adjustments, reservations
  • Purchase Orders - Workflow - Approve, send, receive, cancel

Structure:

Module → Aggregate → Category
   ↓        ↓          ↓
Operations → Products → Management

How It Works

1. Controller Annotations

Add @group annotation to controller docblocks:

php
/**
 * ProductController handles CRUD operations for products
 *
 * @group Products - Management
 */
final class ProductController extends Controller
{
    // Controller methods...
}

2. Visual Hierarchy

The tag format creates this structure in Scalar:

📁 Operations → Products
   └─ Products - Management
   └─ Products - Lifecycle
   └─ Products - Search

📁 Operations → Inventory
   └─ Inventory - Stock Management
   └─ Inventory - Reports
   └─ Inventory - Transfers

3. x-tagGroups Extension

The docs:generate command automatically adds x-tagGroups to the OpenAPI spec:

yaml
x-tagGroups:
  - name: 'Operations → Products'
    tags:
      - 'Products - Management'
      - 'Products - Lifecycle'
      - 'Products - Search'
  - name: 'Operations → Inventory'
    tags:
      - 'Inventory - Stock Management'
      - 'Inventory - Reports'

Documenting a New Module

Step 1: Plan Your Structure

Identify your module's aggregates and categories:

Example: CRM Module

  • Leads → Management, Conversion, Assignment
  • Customers → Management, Lifecycle, Health Tracking
  • Opportunities → Management, Pipeline, Forecasting

Step 2: Add @group Annotations

Update controller docblocks with the tag format:

php
// app/Modules/CRM/Presentation/Controllers/Leads/LeadController.php

/**
 * LeadController handles CRUD operations for leads
 *
 * @group Leads - Management
 */
final class LeadController extends Controller
php
// app/Modules/CRM/Presentation/Controllers/Leads/ConvertLeadController.php

/**
 * ConvertLeadController handles lead to customer conversion
 *
 * @group Leads - Conversion
 */
final class ConvertLeadController extends Controller

Step 3: Bulk Update Script (Optional)

Create a script to update all controller tags at once:

bash
# Create: scripts/update-crm-tags.php
#!/usr/bin/env php
<?php

$controllerGroups = [
    // Leads
    'Leads/LeadController.php' => 'Leads - Management',
    'Leads/ConvertLeadController.php' => 'Leads - Conversion',
    'Leads/AssignLeadController.php' => 'Leads - Assignment',

    // Customers
    'Customers/CustomerController.php' => 'Customers - Management',
    'Customers/ActivateCustomerController.php' => 'Customers - Lifecycle',
    'Customers/UpdateHealthScoreController.php' => 'Customers - Health Tracking',

    // Add all your controllers...
];

$basePath = __DIR__ . '/../app/Modules/CRM/Presentation/Controllers/';

foreach ($controllerGroups as $file => $newGroup) {
    $filePath = $basePath . $file;

    if (!file_exists($filePath)) {
        echo "❌ Not found: $file\n";
        continue;
    }

    $content = file_get_contents($filePath);
    $pattern = '/@group\s+.*$/m';
    $replacement = '@group ' . $newGroup;

    $newContent = preg_replace($pattern, $replacement, $content, 1, $count);

    if ($count > 0 && $newContent !== $content) {
        file_put_contents($filePath, $newContent);
        echo "✅ Updated: $file$newGroup\n";
    }
}

Run it:

bash
php scripts/update-crm-tags.php

Step 4: Update HierarchicalTagGroupsGenerator (Optional)

If you have multiple modules, make the module name dynamic:

php
// Current (hardcoded to Operations):
$tagGroups[] = [
    'name' => "Operations → {$aggregate}",
    'tags' => $tags,
];

// Multi-module version:
$tagGroups[] = [
    'name' => "{$this->getModuleName($aggregate)} → {$aggregate}",
    'tags' => $tags,
];

private function getModuleName(string $aggregate): string
{
    // Map aggregates to modules
    return match($aggregate) {
        'Products', 'Inventory', 'Purchase Orders' => 'Operations',
        'Leads', 'Customers', 'Opportunities' => 'CRM',
        'Orders', 'Invoices', 'Payments' => 'Sales',
        default => 'General',
    };
}

Step 5: Generate Documentation

bash
php artisan docs:generate

View at: https://your-domain.test/docs

Best Practices

Naming Guidelines

DO:

  • Use descriptive, action-oriented category names
  • Keep aggregate names consistent with domain language
  • Group related operations together

DON'T:

  • Use technical terms (avoid "CRUD", "API", "Endpoints")
  • Create too many categories (aim for 3-5 per aggregate)
  • Mix different concerns in one category

Example Organization

Good:

Products - Management      (index, show, store, update, destroy)
Products - Lifecycle       (activate, deactivate, discontinue)
Products - Search          (search, catalog, featured, filters)

Bad:

Products - CRUD           (too technical)
Products - Misc           (too vague)
Products - Everything     (not organized)

Category Suggestions

Common category patterns:

  • Management: CRUD operations (index, show, store, update, destroy)
  • Lifecycle: State transitions (activate, deactivate, archive, restore)
  • Search: Filtering and discovery (search, advanced-search, filters)
  • Reports: Analytics and summaries (statistics, metrics, insights)
  • Workflow: Process steps (submit, approve, reject, complete)
  • Assignment: Ownership operations (assign, reassign, transfer)
  • Bulk Operations: Mass actions (bulk-update, bulk-delete, import)

Scribe Configuration

Key settings in config/scribe.php:

php
return [
    // Use Scalar UI
    'type' => 'external_laravel',
    'theme' => 'scalar',

    // Organize by @group tag
    'routes' => [
        [
            'match' => [
                'prefixes' => ['api/v1/*'],
                'domains' => ['*'],
            ],
            'include' => [],
            'exclude' => [],
        ],
    ],
];

Troubleshooting

Tags Not Showing

Problem: Endpoints don't appear in documentation

Solution:

  1. Verify @group annotation exists in controller docblock
  2. Check route is matched by Scribe config (prefixes)
  3. Run php artisan docs:generate to regenerate

Flat Hierarchy

Problem: Groups show with "/" instead of arrow (→)

Solution:

  • Use " - " separator in tag names, not "/"
  • Ensure x-tagGroups is added by the command
  • Check HierarchicalTagGroupsGenerator is creating groups correctly

Empty Groups

Problem: Groups appear but contain no endpoints

Solution:

  • Tag names in controllers must EXACTLY match tags in x-tagGroups
  • Check for typos in tag names
  • Verify groupByAggregate() logic matches your tag format

Reference: Operations Module

The Operations module serves as the reference implementation. See:

  • Controllers: app/Modules/Operations/Presentation/Controllers/
  • Bulk Update Script: scripts/simplify-tag-names.php
  • Generated Docs: storage/app/private/scribe/openapi.yaml

Examine how it's organized:

Operations → Products
  ├─ Products - Management
  ├─ Products - Lifecycle
  └─ Products - Search

Operations → Inventory
  ├─ Inventory - Stock Management
  ├─ Inventory - Reports
  └─ Inventory - Transfers

Operations → Purchase Orders
  ├─ Purchase Orders - Management
  ├─ Purchase Orders - Workflow
  └─ Purchase Orders - Reports

Additional Resources

Documentation for SynthesQ CRM/ERP Platform