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 productsProducts - Lifecycle- Activation, deactivation, discontinuationInventory - Stock Management- Stock adjustments, reservationsPurchase Orders - Workflow- Approve, send, receive, cancel
Structure:
Module → Aggregate → Category
↓ ↓ ↓
Operations → Products → ManagementHow It Works
1. Controller Annotations
Add @group annotation to controller docblocks:
/**
* 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 - Transfers3. x-tagGroups Extension
The docs:generate command automatically adds x-tagGroups to the OpenAPI spec:
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:
// app/Modules/CRM/Presentation/Controllers/Leads/LeadController.php
/**
* LeadController handles CRUD operations for leads
*
* @group Leads - Management
*/
final class LeadController extends Controller// app/Modules/CRM/Presentation/Controllers/Leads/ConvertLeadController.php
/**
* ConvertLeadController handles lead to customer conversion
*
* @group Leads - Conversion
*/
final class ConvertLeadController extends ControllerStep 3: Bulk Update Script (Optional)
Create a script to update all controller tags at once:
# 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:
php scripts/update-crm-tags.phpStep 4: Update HierarchicalTagGroupsGenerator (Optional)
If you have multiple modules, make the module name dynamic:
// 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
php artisan docs:generateView 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:
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:
- Verify
@groupannotation exists in controller docblock - Check route is matched by Scribe config (
prefixes) - Run
php artisan docs:generateto 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
HierarchicalTagGroupsGeneratoris 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