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.
Requires
- php: ^8.2
- ext-json: *
- illuminate/console: ^12.0|^13.0
- illuminate/routing: ^12.0|^13.0
- illuminate/support: ^12.0|^13.0
- nikic/php-parser: ^4.18|^5.0
Requires (Dev)
- orchestra/testbench: ^10.0|^11.0
- phpunit/phpunit: ^11.0|^12.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
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.jsonserves a spec you can hand to Postman, Insomnia, an SDK generator or a contract test.- Resources become reusable
components/schemasentries referenced with$ref. - Validation rules become real JSON Schema (
minLength,maximum,pattern,enum,format, union types for nullables). - Authentication becomes a
securitySchemesentry plus per-operationsecurity. - The renderer also works on a spec you did not generate — point
openapi.sourceat 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— failapi-docs:generateon 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, andsourcefor rendering an external spec.code_samples— any ofcurl,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.