Search by

cofa / laravel-api-docs

Zero-config, structure-agnostic API documentation generator for Laravel. Scans every route, infers headers, body, query and URL parameters plus expected responses, and renders them as beautiful Blade documentation inside your own project.

Maintainers

Package info

github.com/Cofa12/Apis-hosted_documentation

pkg:composer/cofa/laravel-api-docs

Transparency log

Statistics

Installs: 21

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.3.5 2026-09-02 11:05 UTC

This package is auto-updated.

Last update: 2026-09-02 11:21:58 UTC


README

CI

Zero-config API documentation for Laravel, built on the OpenAPI 3.1 standard and rendered as Blade views inside your own project.

Point it at an application and it reads the whole API — however the project is organised — then compiles what it found into an OpenAPI document and renders that document as a searchable reference page with headers, parameters, request bodies, expected responses, code samples and an in-page request console.

It also remembers: every generation is compared against the last one, so each endpoint carries a timeline of what changed about it and when.

composer require cofa/laravel-api-docs
php artisan api-docs:generate

Then open /api/documentation.

Why it works on any project layout

The scanner reads Laravel's route collection, not your directory tree. By the time it runs, every route is registered — whether it came from routes/api.php, a module's service provider, a package, an invokable controller or a closure. Nothing has to be moved, renamed or annotated.

From each route it then works backwards through the code that handles it:

What you get Where it comes from
Method, URI, name, middleware, handler the route itself
Group, summary, description @group / docblock / #[ApiGroup] / controller name
URL parameters URI placeholders, where() constraints, route model binding, action type hints
Body parameters the form request's rules(), or $request->validate([...]) / Validator::make() inside the action
Query parameters $request->query(), input(), boolean(), integer()… plus paginate() detection
Types, constraints, enums, examples the validation rules, including Rule::in(), Rule::enum(), Password::min()
Headers configured defaults, auth middleware, #[ApiHeader], @header
Authentication auth middleware, @authenticated, #[Authenticated]
Success responses API resources (followed into nested resources), models, response()->json(), @response
Error responses auth (401), authorization (403), model binding (404), validation (422), throttling (429), abort()
Change history the diff between this generation and the last recorded snapshot
Tenant scoping the active tenant, for per-tenant documents, history and cache

Rules are read by instantiating the form request when that is safe, and by parsing the source with nikic/php-parser when it is not — so rules that depend on runtime state still get documented. An action the generator cannot read is reported, never fatal: the rest of the API is still documented.

OpenAPI is the source of truth

The scan result is compiled into an OpenAPI 3.1 document, and the Blade UI renders that document. That means:

  • GET /api/documentation.json serves a spec you can hand to Postman, Insomnia, an SDK generator or a contract test.
  • Resources become reusable components/schemas entries referenced with $ref.
  • Validation rules become real JSON Schema (minLength, maximum, pattern, enum, format, union types for nullables).
  • Authentication becomes a securitySchemes entry plus per-operation security.
  • The renderer also works on a spec you did not generate — point openapi.source at any OpenAPI 3.x file or URL and it will document that instead.

Nothing OpenAPI reserves is lost either: Accept, Content-Type and Authorization stay out of parameters (as the specification requires) but are kept on the operation under x-headers, alongside x-controller, x-middleware and x-route-name.

Endpoint history

Every api-docs:generate compares the new document against the last recorded snapshot and stores the difference as a revision. The page then shows a changelog of recent revisions, and each endpoint carries its own timeline:

rev-4  2026-08-30  1 added, 2 changed
  Added    POST /api/webhooks
  Changed  PUT  /api/users/{user}
      · Body field `email` is now required   [breaking]
      · Response 422 added
  Changed  GET  /api/users
      · Added query parameter `filter`

It tracks summaries and descriptions, grouping, deprecation, authentication, path/query/header parameters, request body fields (nested ones included), response status codes and response body fields — reporting each one as a sentence rather than a JSON diff. Changes that can break an existing client (a removed endpoint or field, a newly required field, newly required auth) are flagged as breaking.

php artisan api-docs:history                      # the timeline, newest first
php artisan api-docs:history --endpoint=users     # only endpoints matching a path
php artisan api-docs:history --breaking           # only revisions that break clients
php artisan api-docs:history --json               # the raw record
php artisan api-docs:generate --no-history        # generate without recording

The record lives in resources/views/vendor/api-docs/history.json. Commit it: that is what makes the timeline survive across environments and deployments. Configure retention and display under api-docs.history, or set history.enabled to false to turn the whole feature off.

Multi tenancy

A multi tenant application serves the same routes from many contexts, so the artefacts have to be scoped per tenant. Put a {tenant} placeholder anywhere in the config and it is replaced with the current tenant key:

'title'     => '{tenant} API',
'base_url'  => 'https://{tenant}.example.com',
'output'    => ['spec_file' => 'resources/views/vendor/api-docs/{tenant}/openapi.json'],
'history'   => ['path' => 'resources/views/vendor/api-docs/{tenant}/history.json'],
'tenancy'   => ['enabled' => true],

Each tenant then gets its own document, its own change history and its own cache entry — the cache key is scoped automatically, because sharing one cached document between tenants would serve one tenant's documentation to another. The tenant key is sanitised before it reaches a path or a cache key, so a key like ../../etc cannot escape its directory.

The tenant is detected automatically for stancl/tenancy (the tenant() helper) and spatie/laravel-multitenancy (Tenant::current()). For anything else, point the resolver at a closure or an invokable class:

'tenancy' => [
    'enabled'  => true,
    'resolver' => fn () => auth()->user()?->company_id,
    // or, if you run `config:cache`, an invokable class:
    // 'resolver' => \App\Docs\CurrentTenant::class,
],

Resolution happens on every read rather than once at boot, so a console command that walks through tenants (php artisan tenants:run api-docs:generate) writes each tenant's documentation to its own place. When no tenant is active the central_key (central by default) is used instead.

On the page itself, the code samples and the try-it console follow the host the documentation is being viewed on, so a tenant sees its own domain rather than a single configured URL. Set tenancy.follow_request_host to false to use the configured base_url instead. For domain based tenancy the docs route accepts a Laravel domain pattern:

'serve' => ['domain' => '{account}.example.com'],

If your configuration is cached, remember that a closure resolver cannot be serialised — use the invokable class form.

Caching

Caching is off by default: the documentation is scanned live, which is what you want in development. Turn cache.enabled on in production and refresh it during deployment.

The documentation never depends on the cache being healthy. If the store cannot be reached — a database cache driver pointed at a connection whose cache table was never migrated, a Redis that is down — the page falls back to a live scan and the commands report the reason instead of failing. With caching off, the cache store is not touched at all.

If your default store is somewhere the documentation should not rely on (a tenant database, for instance), point it somewhere else:

'cache' => ['enabled' => true, 'store' => 'file'],

Installation

composer require cofa/laravel-api-docs

The service provider is auto-discovered. Publish the config if you want to tune it:

php artisan vendor:publish --tag=api-docs-config

Usage

# scan, write openapi.json and the Blade templates into resources/views/vendor/api-docs
php artisan api-docs:generate

# overwrite templates you have already customised
php artisan api-docs:generate --force

# only refresh the spec
php artisan api-docs:generate --no-views

# also build a single self-contained HTML file (public/docs/index.html)
php artisan api-docs:generate --static

# just the spec, anywhere you like
php artisan api-docs:export storage/app/openapi.yaml --format=yaml
php artisan api-docs:export --print

# drop the cached document
php artisan api-docs:clear

You do not have to run anything at all in development: the route renders the documentation live from the current code. Enable api-docs.cache.enabled in production and refresh it during deployment.

The generated Blade files are ordinary views. Edit them and they stay edited — api-docs:generate never overwrites an existing template unless you pass --force.

Documenting by hand

Everything is inferred, but anything can be overridden. Both a docblock and an attribute syntax are available; explicit documentation always wins.

/**
 * @group Users
 *
 * Endpoints for managing user accounts.
 */
class UserController
{
    /**
     * Create a user
     *
     * Creates the account and sends the welcome email.
     *
     * @authenticated
     * @header X-Tenant acme  The tenant the user belongs to.
     * @bodyParam name string required The full name. Example: Ada Lovelace
     * @queryParam notify boolean Send the welcome email. Example: true
     * @urlParam team integer required The team to add the user to.
     * @response 201 {"data": {"id": 1, "name": "Ada Lovelace"}}
     * @response 409 The email is already taken.
     * @apiResource 200 App\Http\Resources\UserResource
     */
    public function store(StoreUserRequest $request) { /* … */ }
}

The same thing with attributes:

use Cofa\ApiDocs\Attributes\{ApiDoc, ApiGroup, ApiHeader, ApiParam, ApiResponse, Authenticated, HideFromDocs};

#[ApiGroup('Users', description: 'Endpoints for managing user accounts.')]
class UserController
{
    #[ApiDoc(summary: 'Create a user', description: 'Creates the account and sends the welcome email.')]
    #[Authenticated]
    #[ApiHeader(name: 'X-Tenant', value: 'acme', required: true)]
    #[ApiParam(name: 'notify', type: 'boolean', in: 'query', description: 'Send the welcome email.')]
    #[ApiResponse(status: 201, resource: UserResource::class)]
    #[ApiResponse(status: 409, content: ['message' => 'That email is taken.'])]
    public function store(StoreUserRequest $request) { /* … */ }
}

If both a docblock tag and an attribute describe the same parameter on the same action, the attribute's value wins. Precedence is decided field by field, not source by source: if the docblock documents email and an attribute documents notify, both apply, and an attribute that names a parameter without saying anything about its type leaves the documented type alone. The same rule covers the summary, description, group, deprecation, authentication, headers and responses.

Nothing is overruled quietly. api-docs:generate reports every disagreement:

 WARN 1 documentation conflict between docblocks and attributes. The attribute value is used:

 - UserController::update — body param `email`: docblock and #[ApiParam] disagree (required: true vs false). Using attribute value.

Teams that would rather have documentation drift break the build than resolve itself can set strict_precedence to true, which makes api-docs:generate fail on a disagreement and write nothing. The documentation page is never affected either way — drift should fail a build, not take the docs down.

Hide something with #[HideFromDocs] or @ignore on the action or the whole controller.

Form requests can describe their own fields, and the descriptions are merged with the ones derived from the rules:

public function bodyParameters(): array
{
    return [
        'name' => ['description' => 'The full name of the user.', 'example' => 'Ada Lovelace'],
    ];
}

Configuration

config/api-docs.php covers:

  • routes — include/exclude URI patterns, required middleware, whether to skip closures.
  • strict_precedence — fail api-docs:generate on a docblock/attribute disagreement instead of warning.
  • grouping — group by controller or by URI segment, plus an explicit group order.
  • auth — which middleware means "authenticated", and the header to show.
  • headers — defaults sent with every request, and with every body.
  • responses — default status per verb, whether to document error paths, resource wrapper key, how deep to follow nested resources.
  • openapi — spec version, servers, contact/license, security schemes, whether to emit component schemas, and source for rendering an external spec.
  • code_samples — any of curl, javascript, php, python.
  • ui — theme, logo, try-it console, whether to show controllers and middleware.
  • output — where the templates, the spec and the static build are written.
  • serve — path, route name, middleware and domain for the documentation route.
  • cache — cache the compiled document instead of re-reading the code on every request.

The page itself

  • Three-pane layout with a grouped, filterable sidebar (press / to search).
  • Method-coloured endpoint cards that collapse, deep-link and print cleanly.
  • Parameter tables with types, requiredness, constraints, enum values and examples — nested objects and arrays included.
  • Response tabs per status code with syntax-highlighted bodies and an expandable schema table.
  • Code samples in cURL, JavaScript, PHP and Python, filled in with real example values.
  • A "try it" console that sends the request from the browser and shows the live response.
  • A changelog of recent revisions, and a per-endpoint history showing what changed and when.
  • Light and dark themes, respecting the system preference and remembering the choice.
  • No CDN, no build step, no external requests — the CSS and JS are inlined, so it works behind a strict CSP and offline.

Testing

composer install
composer test

291 tests cover the rule parser, docblock parser, parameter nesting, schema generation, the spec reader (including third-party OpenAPI documents), code samples, the change differ and history store, tenant resolution and scoping, degraded cache stores, docblock/attribute precedence, the scanner end to end, the generated document, the rendered page and every console command.

CI runs the suite on every supported combination — PHP 8.2, 8.3 and 8.4 against Laravel 12 and 13 — on each push and pull request.

Requirements

  • PHP 8.2+ (Laravel 13 itself needs PHP 8.3+)
  • Laravel 12 or 13

The code itself runs unchanged on Laravel 10 and 11 — the suite passes against both — but those branches are past their security support window, so a current Composer refuses to install them and they are not listed as supported. If you are pinned to one of them, require this package with your own advisory exception (policy.advisories.block) and it will work.

License

MIT.