zero-to-prod / laravel-openapi
OpenAPI support for Laravel.
Requires
- php: ^8.3
- illuminate/console: ^12.0|^13.0
- illuminate/http: ^12.0|^13.0
- illuminate/routing: ^12.0|^13.0
- illuminate/support: ^12.0|^13.0
Requires (Dev)
- devizzent/cebe-php-openapi: ^1.1
- laravel/mcp: ^0.9
- laravel/pint: ^1.30
- league/openapi-psr7-validator: ^0.24.0
- nyholm/psr7: ^1.8
- orchestra/testbench: ^10.0|^11.0
- pestphp/pest: ^3.0|^4.0
- phpstan/phpstan: ^2.2
- rector/rector: ^2.5
- symfony/psr-http-message-bridge: ^7.4|^8.1
Suggests
- devizzent/cebe-php-openapi: Required by `php artisan openapi:validate` to validate the generated document.
- laravel/mcp: Required by the MCP server that exposes this package's documentation to coding agents.
- league/openapi-psr7-validator: Required by the ValidatesSchema test trait to match responses against the document.
- nyholm/psr7: PSR-7 implementation used by the ValidatesSchema test trait.
- symfony/psr-http-message-bridge: Converts Laravel requests and responses to PSR-7 for the ValidatesSchema test trait.
README
Laravel OpenAPI generates an OpenAPI document to help users build confidently with your API.
The package uses the OpenAPI specification as part of the package API itself.
This is especially useful for AI agents, because many models are already trained on the OpenAPI specification. They can build with this package effectively without specialized knowledge or skills.
Requirements
- PHP
^8.3 - Laravel 12 or 13
Installation
Laravel OpenAPI can be installed via Composer:
composer require zero-to-prod/laravel-openapi
Configuration
You may want to publish the configuration file to override or customize the behavior.
php artisan vendor:publish --tag=openapi-config
Agent development
This project ships with an MCP server to aid in agent development.
Quick start
Annotate a controller method. Whatever you write here is placed in the document.
Use the validation tools to prove the document conforms to the OpenAPI specification and that your application actually behaves the way it claims.
use Illuminate\Http\JsonResponse; use ZeroToProd\LaravelOpenapi\ApiSchema; class ShowArticleController { #[ApiSchema([ 'paths' => [ '/articles/{id}' => [ 'get' => [ 'operationId' => 'showArticle', 'parameters' => [ [ 'name' => 'id', 'in' => 'path', 'required' => true, 'schema' => ['type' => 'string'], ], ], 'responses' => [ '200' => [ 'description' => 'The article.', 'content' => [ 'application/vnd.api+json' => [ 'schema' => [ 'type' => 'object', 'required' => ['id', 'title'], 'properties' => [ 'id' => ['type' => 'string'], 'title' => ['type' => 'string'], ], ], ], ], ], ], ], ], ], ])] public function __invoke(string $id): JsonResponse { return new JsonResponse( data: ['id' => $id, 'title' => 'Zero to prod'], headers: ['Content-Type' => 'application/vnd.api+json'], ); } }
GET /openapi.json serves:
{
"openapi": "3.0.4",
"info": {
"title": "Laravel",
"version": "1.0.0"
},
"servers": [
{
"url": "/"
}
],
"paths": {
"/articles/{id}": {
"get": {
"operationId": "showArticle",
"...": "..."
}
},
"/openapi.json": {
"get": {
"operationId": "getSchema",
"...": "..."
}
}
}
}
The fragment on every registered route is merged into the document-level fields from config/openapi.php, paths are sorted, and the
result is served verbatim. Nothing is rewritten or checked on the way out, so run openapi:validate before you
deploy.
Paths in the attribute are resolved relative to the first entry in openapi.servers. With the default of / they are absolute, so
declare the path the route actually serves.
Routing
By default, the package registers one route: GET /openapi.json, named openapi.schema.
| Key | Default | Purpose |
|---|---|---|
openapi.route.enabled |
true |
Register the route at all |
openapi.route.uri |
'openapi.json' |
URI within the prefix |
openapi.route.name |
openapi.schema |
Route name |
openapi.route.prefix |
'' |
Group prefix |
openapi.route.middleware |
['api'] |
Group middleware |
For full customization, turn the default off and place the route:
// config/openapi.php 'route' => ['enabled' => false, /* ... */], // routes/api.php use ZeroToProd\LaravelOpenapi\ApiSchema; Route::middleware('auth:sanctum') ->prefix('internal') ->group(fn () => ApiSchema::routes());
Override the route:
ApiSchema::routes('docs/openapi.json', 'docs.schema')->middleware('throttle:60,1');
Moving the endpoint does not move how it is documented: the document always describes this route as /openapi.json. Keep the two in
step, or accept that the document describes the old path.
Validation
This package ships with optional validation tools to verify both the document and the behavior behind it.
Schema validation
This command builds the document and validates it against the OpenAPI specification.
composer require --dev devizzent/cebe-php-openapi
php artisan openapi:validate
On failure it lists the specification errors and exits non-zero; on success it exits 0, which makes it useful in a build pipeline.
Behavior validation
If you want to prove the document matches your application's behavior, this package comes with built-in assertions that you can use in your tests.
composer require --dev league/openapi-psr7-validator symfony/psr-http-message-bridge nyholm/psr7
use ZeroToProd\LaravelOpenapi\ValidatesSchema; abstract class TestCase extends BaseTestCase { use ValidatesSchema; }
Then assert against it:
$this->assertMatchesSchema($this->getJson('articles/42'));
The operation is resolved from the request automatically, and both the request and the response are checked. The response is returned, so the assertion can be chained onto an existing call.
Coverage
A declared response that no test ever exercises is a claim nothing checks. Every call to assertMatchesSchema() records the
(path, method, status) it validated, so you can find the ones you missed.
Reset the record before the suite runs, then report on it afterwards:
php artisan openapi:coverage --reset && vendor/bin/pest && php artisan openapi:coverage
ERROR 1 of 2 declared responses were never exercised.
⇂ GET /articles/{id} -> 404
The command exits non-zero while anything is missing, so it gates a build the same way openapi:validate does.
Records are appended to storage/framework/cache/openapi-coverage.jsonl, which is append-only JSON Lines so parallel test workers can
share it. Change the location with openapi.coverage.path.
To assert the same thing from inside the suite, call assertSchemaFullyExercised() from a test that runs last. It only sees what the
current process recorded, so prefer the command when your suite runs in parallel.
MCP server
The package registers an MCP server so coding agents can read how it is meant to be used.
Install the dependency via composer.
composer require --dev laravel/mcp
Set up your agents
claude mcp add -s local -t stdio laravel-openapi php artisan mcp:start laravel-openapi
codex mcp add laravel-openapi -- php "artisan" "mcp:start" "laravel-openapi"
gemini mcp add -s project -t stdio laravel-openapi php artisan mcp:start laravel-openapi
For an agent configured by file, add the server directly:
{
"mcpServers": {
"laravel-openapi": {
"type": "stdio",
"command": "php",
"args": [
"artisan",
"mcp:start",
"laravel-openapi"
]
}
}
}