spatie/laravel-openapi-cli

Create a Laravel command for your OpenAPI spec(s). Works in apps and other packages. Useful for AI agents.

Fund package maintenance!
Spatie

Installs: 80

Dependents: 1

Suggesters: 0

Security: 0

Stars: 4

Watchers: 0

Forks: 0

Open Issues: 0

pkg:composer/spatie/laravel-openapi-cli

0.0.1 2026-02-11 13:47 UTC

This package is auto-updated.

Last update: 2026-02-11 15:08:08 UTC


README

Latest Version on Packagist GitHub Tests Action Status GitHub Code Style Action Status Total Downloads

Turn any OpenAPI spec into dedicated Laravel artisan commands. Each endpoint gets its own command with typed options for path parameters, query parameters, and request bodies.

OpenApiCli::register('https://api.bookstore.io/openapi.yaml', 'bookstore')
    ->baseUrl('https://api.bookstore.io')
    ->bearer(env('BOOKSTORE_TOKEN'))
    ->banner('Bookstore API v2')
    ->cache(ttl: 600)
    ->followRedirects()
    ->yamlOutput()
    ->showHtmlBody()
    ->useOperationIds()
    ->onError(function (Response $response, Command $command) {
        return match ($response->status()) {
            429 => $command->warn('Rate limited. Retry after '.$response->header('Retry-After').'s.'),
            default => false,
        };
    });

List all endpoints:

php artisan bookstore:list
Bookstore API v2

GET    bookstore:get-books             List all books
POST   bookstore:post-books            Add a new book
GET    bookstore:get-books-reviews     List reviews for a book
DELETE bookstore:delete-books          Delete a book

Human-readable output (default):

php artisan bookstore:get-books --limit=2
# Data

| id | title                    | author          |
|----|--------------------------|-----------------|
| 1  | The Great Gatsby         | F. Fitzgerald   |
| 2  | To Kill a Mockingbird    | Harper Lee      |

# Meta

total: 2

YAML output:

php artisan bookstore:get-books --limit=2 --yaml
data:
  -
    id: 1
    title: 'The Great Gatsby'
    author: 'F. Fitzgerald'
  -
    id: 2
    title: 'To Kill a Mockingbird'
    author: 'Harper Lee'
meta:
  total: 2

Support us

We invest a lot of resources into creating best in class open source packages. You can support us by buying one of our paid products.

We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on our contact page. We publish all received postcards on our virtual postcard wall.

Installation

You can install the package via composer:

composer require spatie/laravel-openapi-cli

The package will automatically register its service provider.

Usage

Registering an API

Register your OpenAPI spec in a service provider (typically AppServiceProvider). You can use a local file path or a remote URL:

use Spatie\OpenApiCli\Facades\OpenApiCli;

public function boot()
{
    OpenApiCli::register(base_path('openapi/bookstore-api.yaml'), 'bookstore')
        ->baseUrl('https://api.example-bookstore.com')
        ->bearer(env('BOOKSTORE_TOKEN'));
}

The second argument is the namespace - it groups all commands under a common prefix. For a spec with GET /books, POST /books, and GET /books/{book_id}/reviews, you get:

  • bookstore:get-books
  • bookstore:post-books
  • bookstore:get-books-reviews
  • bookstore:list

Direct registration (no namespace)

If you omit the namespace, commands are registered directly without a prefix. This is useful for Laravel Zero CLI tools or any app where a single API is the primary interface:

OpenApiCli::register(base_path('openapi/api.yaml'))
    ->baseUrl('https://api.example.com')
    ->bearer(env('API_TOKEN'));

For the same spec, you get:

  • get-books
  • post-books
  • get-books-reviews

Note: The list command is not registered when no namespace is set, since it would conflict with Laravel's built-in list command.

Remote specs

You can register a spec directly from a URL. The spec is fetched via HTTP on every boot by default:

OpenApiCli::register('https://api.example.com/openapi.yaml', 'example')
    ->baseUrl('https://api.example.com')
    ->bearer(env('EXAMPLE_TOKEN'));

To enable caching (recommended for production), call cache():

OpenApiCli::register('https://api.example.com/openapi.yaml', 'example')
    ->cache(); // cache for 60 seconds (default)

You can customize the TTL, cache store, and key prefix:

OpenApiCli::register('https://api.example.com/openapi.yaml', 'example')
    ->cache(ttl: 600); // 10 minutes

OpenApiCli::register('https://api.example.com/openapi.yaml', 'example')
    ->cache(ttl: 600, store: 'redis', prefix: 'my-api:');

Command naming

Commands are named {namespace}:{method}-{path} where path parameters are stripped. Without a namespace, the command is just {method}-{path}:

Method Path Command (namespaced) Command (direct)
GET /books bookstore:get-books get-books
POST /books bookstore:post-books post-books
GET /books/{book_id}/reviews bookstore:get-books-reviews get-books-reviews
DELETE /authors/{author_id}/books/{book_id} bookstore:delete-authors-books delete-authors-books

When two paths would produce the same command name (e.g. /books and /books/{id} both yield get-books), the trailing path parameter is appended to disambiguate:

Path Command (namespaced) Command (direct)
/books bookstore:get-books get-books
/books/{id} bookstore:get-books-id get-books-id

Operation ID mode

If your spec includes operationId fields, you can use those for command names instead of the URL path:

OpenApiCli::register(base_path('openapi/api.yaml'), 'api')
    ->baseUrl('https://api.example.com')
    ->useOperationIds();

With operationId: listBooks in the spec, the command becomes api:list-books instead of api:get-books. Endpoints without an operationId fall back to path-based naming.

Path parameters

Path parameters become required --options:

php artisan bookstore:get-books-reviews --book-id=42
php artisan bookstore:delete-authors-books --author-id=1 --book-id=7

Parameter names are converted to kebab-case (book_id becomes --book-id, bookId becomes --book-id).

Query parameters

Query parameters defined in the spec become optional --options:

# If the spec defines ?genre and ?limit query params for GET /books
php artisan bookstore:get-books --genre=fiction --limit=10

Bracket notation is converted to kebab-case (e.g. filter[id] becomes --filter-id).

Sending data

Form fields

Use --field to send key-value data:

php artisan bookstore:post-books --field title="The Great Gatsby" --field author_id=1

Fields are sent as JSON by default. If the spec declares application/x-www-form-urlencoded as the content type, fields are sent as form data instead.

JSON input

Send raw JSON with --input:

php artisan bookstore:post-books --input '{"title":"The Great Gatsby","metadata":{"genre":"fiction","year":1925}}'

--field and --input cannot be used together.

File uploads

Upload files using the @ prefix on field values:

php artisan bookstore:post-books-cover --book-id=42 --field cover=@/path/to/cover.jpg
php artisan bookstore:post-books-cover --book-id=42 --field cover=@/path/to/cover.jpg --field alt="Book Cover"

When any field contains a file, the request is sent as multipart/form-data.

Listing endpoints

Every namespaced API gets a {namespace}:list command:

php artisan bookstore:list
GET    bookstore:get-books                      List all books
POST   bookstore:post-books                     Add a new book
GET    bookstore:get-books-reviews              List reviews for a book
DELETE bookstore:delete-books                   Delete a book
POST   bookstore:post-books-cover               Upload a cover image

Note: The list command is only registered when a namespace is provided. Direct registrations (without a namespace) do not get a list command.

Custom banner

Add a banner that displays above the endpoint list when running {namespace}:list. This is useful for branding, ASCII art logos, or contextual information.

String banner

OpenApiCli::register(base_path('openapi/api.yaml'), 'api')
    ->baseUrl('https://api.example.com')
    ->banner('Bookstore API v1.0');

Callable banner

For full control over styling, pass a callable that receives the Command instance:

OpenApiCli::register(base_path('openapi/api.yaml'), 'api')
    ->baseUrl('https://api.example.com')
    ->banner(function ($command) {
        $command->info('=== My API ===');
        $command->comment('Environment: ' . app()->environment());
    });

The banner only appears in the list command output, not when running individual endpoint commands.

Authentication

Bearer token

OpenApiCli::register(base_path('openapi/api.yaml'), 'api')
    ->baseUrl('https://api.example.com')
    ->bearer(env('API_TOKEN'));

API key header

OpenApiCli::register(base_path('openapi/api.yaml'), 'api')
    ->baseUrl('https://api.example.com')
    ->apiKey('X-API-Key', env('API_KEY'));

Basic auth

OpenApiCli::register(base_path('openapi/api.yaml'), 'api')
    ->baseUrl('https://api.example.com')
    ->basic('username', 'password');

Dynamic authentication

Use a closure for tokens that may rotate or need to be fetched dynamically:

OpenApiCli::register(base_path('openapi/api.yaml'), 'api')
    ->baseUrl('https://api.example.com')
    ->auth(fn () => Cache::get('api_token'));

The closure is called fresh on each request.

Output formatting

Human-readable (default)

php artisan bookstore:get-books
# # Data
#
# | ID | Title             |
# |----|-------------------|
# | 1  | The Great Gatsby  |
#
# # Meta
#
# Total: 1

JSON responses are automatically converted into readable markdown-style output: tables for arrays of objects, key-value lines for simple objects, and section headings for wrapper patterns like {"data": [...], "meta": {...}}.

JSON output

php artisan bookstore:get-books --json
# {
#     "data": [
#         { "id": 1, "title": "The Great Gatsby" }
#     ]
# }

The --json flag outputs raw JSON (pretty-printed by default).

YAML output

php artisan bookstore:get-books --yaml
# data:
#   -
#     id: 1
#     title: 'The Great Gatsby'

The --yaml flag converts JSON responses to YAML. If --json or --minify is also passed, those take priority.

Minified output

php artisan bookstore:get-books --minify
# {"data":[{"id":1,"title":"The Great Gatsby"}]}

The --minify flag implies --json — no need to pass both.

YAML as default

If you prefer YAML output as the default for a specific registration, use the yamlOutput() config method:

OpenApiCli::register(base_path('openapi/api.yaml'), 'api')
    ->baseUrl('https://api.example.com')
    ->yamlOutput();

With yamlOutput(), commands output YAML by default. The --json and --minify flags override this and produce JSON output instead.

JSON as default

If you prefer JSON output as the default for a specific registration, use the jsonOutput() config method:

OpenApiCli::register(base_path('openapi/api.yaml'), 'api')
    ->baseUrl('https://api.example.com')
    ->jsonOutput();

With jsonOutput(), commands output pretty-printed JSON by default. The --json and --minify flags still work as expected.

Response headers

php artisan bookstore:get-books --headers
# HTTP/1.1 200 OK
# Content-Type: application/json
# X-RateLimit-Remaining: 99
#
# # Data
#
# | ID | Title             |
# |----|-------------------|
# | 1  | The Great Gatsby  |

HTML responses

When an API returns HTML (e.g., an error page), the body is hidden by default to avoid flooding the terminal. You'll see a hint instead:

Response is not JSON (content-type: text/html, status: 500, content-length: 1234)

Use --output-html to see the full response body.

Pass --output-html to show the body:

php artisan bookstore:get-books --output-html

To always show HTML bodies for a specific registration, use the showHtmlBody() config method:

OpenApiCli::register(base_path('openapi/api.yaml'), 'api')
    ->baseUrl('https://api.example.com')
    ->showHtmlBody();

Non-HTML non-JSON responses (e.g., text/plain) are always shown in full.

Syntax highlighting

JSON and human-readable output are syntax-highlighted by default when running in a terminal. JSON output gets keyword/value coloring via tempest/highlight, and human-readable output gets colored headings, keys, and table formatting.

To disable highlighting (e.g. when piping output), use the built-in --no-ansi flag:

php artisan bookstore:get-books --no-ansi
php artisan bookstore:get-books --json --no-ansi

Highlighting is automatically disabled when output is not a TTY (e.g. piped to a file or another command).

Redirect handling

By default, HTTP redirects are not followed. This means a 301 or 302 response is returned as-is, so you can see exactly what the API responds with. To opt in to following redirects:

OpenApiCli::register(base_path('openapi/api.yaml'), 'api')
    ->baseUrl('https://api.example.com')
    ->followRedirects();

Error handling

  • 4xx/5xx errors: Displays the status code and response body in human-readable format by default (or JSON with --json, minified with --minify). HTML responses suppress the body by default (use --output-html to see it). Other non-JSON responses show the raw body with a content-type notice.
  • Network errors: Shows connection failure details.
  • Missing path parameters: Tells you which --option is required.
  • Invalid JSON input: Shows the parse error.

All error cases exit with a non-zero code for scripting.

Custom error handling

Register an onError callback to handle HTTP errors in a way that's specific to your API:

OpenApiCli::register(base_path('openapi/api.yaml'), 'api')
    ->baseUrl('https://api.example.com')
    ->bearer(env('API_TOKEN'))
    ->onError(function (Response $response, Command $command) {
        return match ($response->status()) {
            403 => $command->error('Your API token lacks permission for this endpoint.'),
            429 => $command->warn('Rate limited. Try again in ' . $response->header('Retry-After') . 's.'),
            500 => $command->error('Server error — try again later.'),
            default => false, // fall through to default handling
        };
    });

The callback receives the Illuminate\Http\Client\Response and the Illuminate\Console\Command instance, giving you access to all Artisan output methods (line(), info(), warn(), error(), table(), newLine(), etc.).

Return a truthy value to indicate "handled" — this suppresses the default "HTTP {code} Error" output. Return false or null to fall through to default error handling. The command always exits with a non-zero code regardless of whether the callback handles the error.

Multiple APIs

Register as many specs as you need with different namespaces:

OpenApiCli::register(base_path('openapi/bookstore.yaml'), 'bookstore')
    ->baseUrl('https://api.example-bookstore.com')
    ->bearer(env('BOOKSTORE_TOKEN'));

OpenApiCli::register(base_path('openapi/stripe.yaml'), 'stripe')
    ->baseUrl('https://api.stripe.com')
    ->bearer(env('STRIPE_KEY'));

Base URL resolution

The base URL is resolved in this order:

  1. The URL set via ->baseUrl() on the registration
  2. The first entry in the spec's servers array
  3. If neither is available, the command throws an error

Debugging

Pass -vvv to any command to see the full request before it's sent:

php artisan bookstore:get-books -vvv
  Request
  -------
  GET https://api.example-bookstore.com/books

  Request Headers
  ---------------
  Accept: application/json
  Content-Type: application/json
  Authorization: Bearer sk-abc...

This shows the HTTP method, resolved URL, request headers (including authentication), and request body when present.

Command reference

Every endpoint command supports these universal options:

Option Description
--field=key=value Send a form field (repeatable)
--input=JSON Send raw JSON body
--json Output raw JSON instead of human-readable format
--yaml Output as YAML
--minify Minify JSON output (implies --json)
-H, --headers Include response headers in output
--output-html Show the full response body when content-type is text/html

Path and query parameter options are generated from the spec and shown in each command's --help output.

Testing

composer test

Changelog

Please see CHANGELOG for more information on what has changed recently.

Contributing

Please see CONTRIBUTING for details.

Security Vulnerabilities

Please review our security policy on how to report security vulnerabilities.

Credits

License

The MIT License (MIT). Please see License File for more information.