dskripchenko/laravel-api

Versioned Laravel APIs with OpenAPI 3.0 docs auto-generated from PHP docblocks β€” no annotations, no YAML β€” plus CRUD scaffolding and middleware cascades.

Maintainers

Package info

github.com/dskripchenko/laravel-api

pkg:composer/dskripchenko/laravel-api

Transparency log

Statistics

Installs: 6 771

Dependents: 4

Suggesters: 0

Stars: 13

Open Issues: 0

v5.9.2 2026-08-20 10:58 UTC

README

laravel-api

dskripchenko/laravel-api

🌐 English Β· Deutsch Β· Русский Β· δΈ­ζ–‡

Tests Latest Version Total Downloads License: MIT Laravel PHP

🌐 Русский | Deutsch | δΈ­ζ–‡

A Laravel package for versioned API routing, OpenAPI 3.0 auto-documentation, and CRUD scaffolding.

Build versioned APIs with automatic OpenAPI documentation generated from PHP docblocks β€” no YAML/JSON schemas to maintain, no annotation libraries to learn.

Table of Contents

Quick Start

composer require dskripchenko/laravel-api
// 1. Define your API class
class Api extends \Dskripchenko\LaravelApi\Components\BaseApi
{
    public static function getMethods(): array
    {
        return [
            'controllers' => [
                'user' => [
                    'controller' => UserController::class,
                    'actions' => ['list', 'show', 'create'],
                ],
            ],
        ];
    }
}

// 2. Define your module
class ApiModule extends \Dskripchenko\LaravelApi\Components\BaseModule
{
    public function getApiVersionList(): array
    {
        return ['v1' => Api::class];
    }
}

// 3. Define your ServiceProvider
class ApiServiceProvider extends \Dskripchenko\LaravelApi\Providers\ApiServiceProvider
{
    protected function getApiModule() { return new ApiModule(); }
}

// 4. Write a controller with docblocks
class UserController extends \Dskripchenko\LaravelApi\Controllers\ApiController
{
    /**
     * List users
     * @input integer ?$page Page number
     * @output integer $id User ID
     * @output string $name User name
     */
    public function list(Request $request): JsonResponse
    {
        return $this->success(User::paginate()->toArray());
    }
}

Result:

  • GET /api/v1/user/list β€” API endpoint
  • GET /api/doc β€” Auto-generated API documentation (Scalar)

Features

Feature Description
Versioned routing api/{version}/{controller}/{action} with inheritance between versions
OpenAPI 3.0 Auto-generated from @input/@output docblocks β€” no YAML files
CRUD scaffolding Complete search/create/read/update/delete with filtering, sorting, pagination
Middleware cascade Global β†’ controller β†’ action with fine-grained exclusion
Response templates Reusable $ref schemas in components/schemas
Security schemes @security tag + securitySchemes for Bearer/API key auth
Nested parameters Dot-notation: @input string $address.city β†’ nested JSON schema
File uploads @input file $avatar β†’ auto multipart/form-data
Multiple responses @response 200 {Success} / @response 422 {Error}
Header parameters @header string $Authorization β€” aggregated from controller + middleware
Soft deletes Built-in restore() and forceDelete() in CrudService
Request tracing RequestIdMiddleware β€” X-Request-Id propagation + log context
Optional output fields @output string ?$email β€” marks response fields as optional in OpenAPI schema
TypeScript generation api:generate-types β€” generates TS interfaces from OpenAPI spec
Named routes Each action registered as a named Laravel route β€” route('api.v1.user.list')
API export api:export β€” Postman Collection, HTTP Client (.http), Markdown, cURL
Markup linting api:lint β€” catches renamed actions, dangling templates, unknown types
IDE support PhpStorm plugin β€” highlighting, inspections, quick fixes, endpoint list
Test helpers assertApiSuccess(), assertApiError(), assertApiValidationError()
Publishable config config/laravel-api.php β€” prefix, URI pattern, HTTP methods

Installation

Requirements

  • PHP 8.2+
  • Laravel 11.x β€” 13.x

Install

composer require dskripchenko/laravel-api

Publish config

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

Architecture

Request lifecycle

HTTP Request
  └─ ApiServiceProvider (registers route: api/{version}/{controller}/{action})
      └─ BaseApiRequest (parses version, controller, action from URI)
          └─ BaseModule::getApi() (version string β†’ BaseApi subclass)
              └─ BaseApi::make()
                  β”œβ”€ getMethods() β†’ resolve controller + action
                  β”œβ”€ Middleware cascade (global β†’ controller β†’ action)
                  └─ app()->call(Controller@action)
                      └─ JsonResponse {success: true, payload: {...}}

Response format

Every response is wrapped in a standard envelope:

// Success
{"success": true, "payload": {"id": 1, "name": "John"}}

// Error
{"success": false, "payload": {"errorKey": "not_found", "message": "User not found"}}

// Validation error
{"success": false, "payload": {"errorKey": "validation", "messages": {"email": ["Required"]}}}

Directory structure

src/
β”œβ”€β”€ Components/        BaseApi, BaseModule, Meta
β”œβ”€β”€ Console/Commands/  ApiInstall, ApiGenerateTypes, ApiExport, ApiDocClear, ApiLint
β”œβ”€β”€ Controllers/       ApiController, CrudController, ApiDocumentationController
β”œβ”€β”€ Exceptions/        ApiException, ApiErrorHandler, Handler
β”œβ”€β”€ Facades/        ApiRequest, ApiModule, ApiErrorHandler
β”œβ”€β”€ Interfaces/     CrudServiceInterface, ApiInterface
β”œβ”€β”€ Middlewares/    ApiMiddleware, RequestIdMiddleware
β”œβ”€β”€ Providers/      ApiServiceProvider, BaseServiceProvider
β”œβ”€β”€ Requests/       BaseApiRequest, CrudSearchRequest
β”œβ”€β”€ Resources/      BaseJsonResource, BaseJsonResourceCollection
β”œβ”€β”€ Services/       ApiResponseHelper, CrudService, OpenApiTypeScriptGenerator
└── Traits/
    β”œβ”€β”€ OpenApiTrait
    └── Testing/       MakesHttpApiRequests

API Versioning

API versions use PHP class inheritance β€” later versions extend earlier ones:

// V1: full API
class ApiV1 extends BaseApi {
    public static function getMethods(): array {
        return ['controllers' => [
            'user' => [
                'controller' => UserControllerV1::class,
                'actions' => ['list', 'show', 'create', 'update', 'delete'],
            ],
        ]];
    }
}

// V2: inherits V1, modifies selectively
class ApiV2 extends ApiV1 {
    public static function getMethods(): array {
        return ['controllers' => [
            'user' => [
                'controller' => UserControllerV2::class,  // upgraded controller
                'actions' => [
                    'delete' => false,                     // removed in v2
                    'archive',                             // new in v2
                ],
            ],
        ]];
    }
}

V2 automatically inherits list, show, create, update from V1, while overriding the controller and modifying actions.

Routing & Middleware

Action configuration

'actions' => [
    'list',                              // simple: method name = action key
    'show' => 'getById',                 // alias: show β†’ calls getById()
    'disabled' => false,                 // disabled action (404)
    'create' => [
        'action' => 'store',             // explicit method name
        'method' => ['post'],            // allowed HTTP methods (default: ['post'])
        'name' => 'orders.store',        // route name: api.{version}.orders.store
        'middleware' => [RateLimit::class],
        'exclude-middleware' => [LogMiddleware::class],
        'exclude-all-middleware' => false,
    ],
]

Middleware cascade

Global middleware (getMethods root)
  └─ Controller middleware
      └─ Action middleware

Each level can exclude middleware from parent levels using exclude-middleware (specific) or exclude-all-middleware (all).

OpenAPI 3.0 Documentation

Documentation is generated automatically from PHP docblocks. No YAML or JSON files to maintain.

Basic tags

/**
 * Create an order
 * Detailed description of the endpoint.
 *
 * @input string $title Order title
 * @input string ?$notes Optional notes
 * @input integer(int64) $amount Amount in cents
 * @input string $status Status [draft,pending,confirmed]
 * @input file ?$attachment Optional file
 *
 * @output integer $id Created order ID
 * @output string(date-time) $createdAt Timestamp
 * @output string ?$notes Optional notes
 */

Nested objects (dot-notation)

/** @input object $address Address
 *  @input string $address.city City
 *  @input string $address.zip ZIP code
 *  @input array $items Order items
 *  @input integer $items[].productId Product
 *  @input integer $items[].quantity Quantity */

Headers, security, responses

/**
 * @header string $Authorization Bearer token
 * @header string ?$X-Request-Id Trace ID
 * @security BearerAuth
 * @response 200 {OrderResponse}
 * @response 422 {ValidationError}
 * @deprecated Use createV2 instead
 */

Response templates

Enable reusable schemas via components/schemas:

class Api extends BaseApi {
    public static bool $useResponseTemplates = true;

    public static function getOpenApiTemplates(): array {
        return [
            'OrderResponse' => [
                'id'         => 'integer!',            // required integer
                'title'      => 'string!',             // required string
                'total'      => 'number',              // optional number
                'created_at' => 'string(date-time)',   // with format
                'email'      => 'string(email)!',      // format + required
                'customer'   => '@Customer',           // $ref to another schema
                'items'      => '@OrderItem[]',        // array of $ref
            ],
            'Customer' => [
                'id'   => 'integer!',
                'name' => 'string!',
            ],
            'OrderItem' => [
                'product_id' => 'integer!',
                'quantity'   => 'integer',
                'price'      => 'number',
            ],
        ];
    }

    public static function getOpenApiSecurityDefinitions(): array {
        return [
            'BearerAuth' => ['type' => 'apiKey', 'name' => 'Authorization', 'in' => 'header'],
        ];
    }
}

Shorthand syntax reference:

Syntax Meaning Example
type Optional field 'number', 'string', 'object'
type! Required field 'integer!', 'string!'
type(format) With format 'string(date-time)', 'string(email)'
type(format)! Format + required 'string(email)!'
@Model $ref to schema '@Customer'
@Model[] Array of $ref '@OrderItem[]'

Array format (['type' => 'string', 'required' => true]) is also supported and can be mixed with shorthand in the same template.

Linking to a single endpoint

The reference page addresses every operation by a hash, and DocLink builds it without opening a browser β€” for a ticket, a README, or an IDE:

use Dskripchenko\LaravelApi\Services\OpenApi\DocLink;

DocLink::url('v1', 'order', 'create', 'post');
// https://example.test/api/doc#v1/tag/order/POST/v1/order/create

DocLink::anchor('v1', 'order', 'create', 'post');  // the hash alone, without '#'

The first segment is the API version, and it is deliberate: the page is handed an explicit slug per version, so a link survives the docblock summary being reworded. An action declaring several HTTP methods is several operations on the page β€” one anchor each.

Full tag reference: docs/docblock-tags.md | Linting: docs/linting.md | Cookbook: docs/cookbook.md

CRUD Scaffolding

Implement CrudService for instant CRUD endpoints:

class ProductService extends CrudService {
    public function meta(): Meta {
        return (new Meta())
            ->string('name', 'Name')
            ->number('price', 'Price')
            ->select('status', 'Status', ['active', 'draft'])
            ->crud();
    }
    public function query(): Builder { return Product::query(); }
    public function resource(Model $model): BaseJsonResource { return new BaseJsonResource($model); }
    public function collection(Collection $c): BaseJsonResourceCollection { return BaseJsonResource::collection($c); }
}

Search with filtering, sorting, pagination

POST /api/v1/product/search
{
  "filter": [
    {"column": "status", "operator": "=", "value": "active"},
    {"column": "price", "operator": "between", "value": [10, 100]},
    {"column": "name", "operator": "like", "value": "phone"},
    {"column": "description", "operator": "is_not_null"}
  ],
  "order": [{"column": "price", "value": "desc"}],
  "page": 1,
  "perPage": 20
}

Available operators: =, !=, >, <, >=, <=, in, not_in, like, between, is_null, is_not_null

Security: LIKE values are auto-escaped (%, _, \). All write operations are wrapped in DB::transaction(). Filter array is limited to 50 items.

Soft delete support

CrudService includes restore($id) and forceDelete($id) for models using SoftDeletes. These methods are not exposed via CrudController by default β€” add custom actions in your controller:

'restore' => ['action' => 'restore', 'method' => ['post']],
'forceDelete' => ['action' => 'forceDelete', 'method' => ['post']],

Testing

use Dskripchenko\LaravelApi\Traits\Testing\MakesHttpApiRequests;

class ProductTest extends TestCase
{
    use MakesHttpApiRequests;

    public function test_list(): void
    {
        $response = $this->api('v1', 'product', 'search');
        $this->assertApiSuccess($response);
    }

    public function test_not_found(): void
    {
        $response = $this->api('v1', 'product', 'read', ['id' => 999]);
        $this->assertApiError($response, 'not_found');
    }

    public function test_validation(): void
    {
        $response = $this->api('v1', 'product', 'create', []);
        $this->assertApiValidationError($response, ['name']);
    }
}

TypeScript Generation

Generate TypeScript interfaces from your OpenAPI spec:

php artisan api:generate-types                                    # All versions β†’ resources/js/shared/api/types.ts
php artisan api:generate-types --api-version=v1                       # Specific version
php artisan api:generate-types --output=frontend/src/api/types.ts # Custom path

Given @output integer $id and @output string ?$email, the generator produces:

export interface UserShowOutput {
  id: number;
  email?: string;
}

Component schemas, operation inputs, and outputs are all generated. See docs/cookbook.md for details.

IDE support

The markup this package reads is not part of any grammar PhpStorm knows, so it shows as one grey blob of prose β€” and everything the package fails quietly at fails quietly there too. There is a plugin for that: dskripchenko/laravel-api-idea.

Installing. It is distributed from its own repository rather than from JetBrains Marketplace. Add the plugin repository once β€” Settings | Plugins | βš™ | Manage Plugin Repositories… | + β€”

https://raw.githubusercontent.com/dskripchenko/laravel-api-idea/main/updatePlugins.xml

then install Laravel API from the Marketplace tab as usual; updates arrive the same way. Paste the address without a trailing space β€” the IDE encodes it into the URL, and the resulting 404 is reported as Connection failed. Or take laravel-api-idea-<version>.zip from any release and use Install Plugin from Disk….

PhpStorm, or IntelliJ IDEA Ultimate with the PHP plugin. IDEA Community cannot run it β€” the plugin hangs off the PHP plugin's PSI, and JetBrains does not publish that for Community.

While you type

  • Highlights @input, @output, @header, @response, @security, @default, @example β€” types, formats, variables, template references and status codes, so a line the generator has stopped understanding stops looking like the ones it still does.
  • Reports markup that does not parse, where it is written, instead of letting it vanish from the spec.
  • Flags unknown types with the consequence named: the generator will call the field string.
  • Compares the markup against $request->validate([...]). A validated field the docblock never mentions is reported on the rule itself, with a quick fix that writes the tag from it β€” email becomes string(email), a rule that is not required becomes optional, in:a,b becomes an enumeration.
  • Marks a @response template or a @security scheme nothing declares, and offers to declare the template β€” creating getOpenApiTemplates() when the class has none.
  • Turns the silent 404 red: an action pointing at a missing, non-public or static method is an error while typing, with a quick fix that writes the method, shaped like the ones already in the class.
  • Completes types, formats, template names, security schemes and status codes.

Getting around

  • Ctrl+Click from {TemplateName}, @Model, @security Scheme and @input [method] to their declarations.
  • Find Usages on a response template β€” every docblock naming it. A template read only by the generator otherwise looks unused to the IDE.
  • Gutter arrows both ways between the route map and the controllers. An action with no arrow points at a method that is not there.
  • A line above the controller method opening this endpoint's own page in /api/doc β€” see Linking to a single endpoint for how that address is built.
  • Every endpoint the route map declares, in one searchable tool window, with the version it answers under.

On demand

  • Runs api:lint and makes its findings clickable.
  • Exports the endpoint under the caret as a request β€” Bruno, cURL, HTTP Client, Postman or Markdown β€” through api:export --endpoint.

Linting

The markup fails quietly: an action whose method was renamed answers 404 β€” the same 404 as a wrong URL β€” and a @response naming a template that does not exist becomes a $ref into nothing, in a spec that still validates.

php artisan api:lint            # report
php artisan api:lint --strict   # fail on warnings too, for CI
php artisan api:lint --unrouted # also: public methods no action points at

It reads the route map and the docblocks with the same parser the OpenAPI generator uses. Full list of rules: docs/linting.md.

API Export

Export your API spec in multiple formats:

php artisan api:export --format=postman    # Postman Collection v2.1
php artisan api:export --format=http       # JetBrains/VS Code .http files
php artisan api:export --format=markdown   # Standalone documentation
php artisan api:export --format=curl       # Bash script with curl commands
php artisan api:export --format=bruno      # Bruno collection β€” a directory of .bru files

Options: --api-version=v1 (specific version), --output=path (custom file). By default, generates per-version files (v1.json, v1.http, v1.md, v1.sh).

Bruno is a directory rather than a document β€” a manifest, an environment and one .bru per request β€” so --output names a directory and --stdout is refused. The point of it is that a collection can live in the repository next to the code that produces it, and a diff of it reads.

One endpoint

A collection of two hundred requests is not what someone wants who is about to try one:

php artisan api:export --endpoint=v1.order.create --format=bruno --stdout
php artisan api:export --endpoint=v1.order.create --format=curl --stdout
php artisan api:export --endpoint=v1.order.create --format=bruno --output=collection/order-create.bru

The endpoint is spelled the way the package names its routes β€” version.controller.action β€” so what is copied out of a log or a route list is what the command takes. --method=get picks one when the action answers several. Every format supports this, and none of them has a special case for it: the spec is narrowed to one path first, and each exporter then does exactly what it already did.

Configuration

Publish the configuration file:

php artisan vendor:publish --tag=laravel-api-config
// config/laravel-api.php
return [
    'prefix' => 'api',                                          // URL prefix
    'uri_pattern' => '{version}/{controller}/{action}',          // Route pattern
    'available_methods' => ['get', 'post', 'put', 'patch', 'delete'],
    'openapi_path' => 'public/openapi',                           // OpenAPI JSON output
    'doc_middleware' => [],                                       // Middleware for /api/doc
];

Error Handling

ApiException

throw new ApiException('payment_failed', 'Insufficient funds');
// β†’ {"success": false, "payload": {"errorKey": "payment_failed", "message": "Insufficient funds"}}

Custom error handlers

use Dskripchenko\LaravelApi\Facades\ApiErrorHandler;
use Dskripchenko\LaravelApi\Services\ApiResponseHelper;
use Illuminate\Database\Eloquent\ModelNotFoundException;

ApiErrorHandler::addErrorHandler(
    ModelNotFoundException::class,
    fn($e) => ApiResponseHelper::sayError(['errorKey' => 'not_found', 'message' => 'Not found'], 404)
);

Handlers support inheritance: registering a handler for Exception will also catch RuntimeException via class_parents() traversal.

RequestIdMiddleware

Add to your middleware stack for request tracing:

// Reads X-Request-Id from request header or generates UUID
// Adds request_id to Log::shareContext()
// Sets X-Request-Id on response header
Dskripchenko\LaravelApi\Middlewares\RequestIdMiddleware::class

Comparison with Alternatives

vs. Classical Laravel approach (manual routes + FormRequest)

Aspect Classical Laravel laravel-api
Route definition routes/api.php β€” one route per endpoint, manual versioning getMethods() β€” declarative array, versions via class inheritance
Versioning Manual: route groups, separate controllers, copy-paste Automatic: V2 extends V1, inherit/override/disable actions
Documentation Separate process: write OpenAPI YAML manually or use annotations Auto-generated from @input/@output docblocks
Response format Ad-hoc per controller, no standard envelope Standardized {success, payload} envelope everywhere
CRUD boilerplate Write controller + FormRequest + Resource for each entity Implement CrudService (4 methods), get 6+ endpoints
Middleware per action Route-level middleware or controller middleware groups Fine-grained: global β†’ controller β†’ action with exclusion
Testing $this->getJson('/api/v1/users') $this->api('v1', 'user', 'list') + assertion helpers
Learning curve Standard Laravel knowledge Learn getMethods() structure + docblock tags
Flexibility Full control over everything Constrained to package conventions
When to choose Complex APIs with non-standard routing, GraphQL, event-driven APIs REST APIs with versioning, standard CRUD, auto-documentation needs

Advantages of laravel-api:

  • Zero-maintenance documentation β€” docblocks are the single source of truth
  • Version inheritance eliminates code duplication between API versions
  • Standardized response format across all endpoints
  • CRUD scaffolding reduces boilerplate by 60-80%

Disadvantages of laravel-api:

  • Fixed URI pattern (api/{version}/{controller}/{action}) β€” not RESTful resource routes
  • Opinionated response format β€” can't easily switch to JSON:API or HAL
  • No native support for resource-style URLs (/users/{id} vs /user/show?id=1)

vs. L5-Swagger (DarkaOnLine/L5-Swagger)

Aspect L5-Swagger laravel-api
Approach OpenAPI-first: write annotations, generate docs Code-first: write docblocks, docs + routing together
Annotation style Full OpenAPI annotations (@OA\Get, @OA\Schema, ...) Lightweight custom tags (@input, @output, @header)
Annotation verbosity High: 15-30 lines per endpoint for full spec Low: 3-10 lines per endpoint
Routing None β€” documentation only, routes defined separately Integrated β€” routing + docs from single getMethods()
Versioning Manual β€” separate annotation groups Built-in β€” class inheritance
CRUD generation None Built-in CrudService + CrudController
Response format Any β€” you define schemas Fixed {success, payload} envelope
OpenAPI coverage Full OpenAPI 3.0 spec support Subset: covers 90% of common use cases
IDE support Plugin support for @OA\* annotations Plugin: highlighting, inspections, quick fixes, navigation, endpoint list, api:lint runner
Ecosystem Large community, swagger-php underneath Smaller, focused package
Spec customization Full control over every OpenAPI field Limited to supported tags
When to choose API-first design, full OpenAPI compliance needed, existing routes Rapid development, versioned APIs, integrated routing + docs

Advantages over L5-Swagger:

  • 3-5x less annotation code per endpoint
  • Routing and documentation are always in sync (single source)
  • Built-in API versioning with inheritance
  • CRUD scaffolding included
  • No need to learn the full OpenAPI annotation specification

Disadvantages compared to L5-Swagger:

  • Less OpenAPI coverage (no callbacks, webhooks, links, discriminator)
  • Fixed response format
  • Smaller community and ecosystem
  • Not suitable for API-first (design-first) workflow

vs. Scramble (dedoc/scramble)

Aspect Scramble laravel-api
Approach Zero-config: infers spec from code (types, FormRequest, routes) Docblock tags: explicit @input/@output annotations
Route integration Uses Laravel's native routes Custom routing via getMethods()
Documentation source PHP types, FormRequest rules, return types Docblock annotations
Manual annotations Optional, for edge cases Required for all endpoints
Versioning None built-in Built-in class inheritance
CRUD None Built-in CrudService
Setup effort Minimal β€” install and it works Moderate β€” define module, API class, provider
When to choose Standard Laravel routes, minimal documentation effort Custom routing, versioning, CRUD needs

Summary: When to use laravel-api

βœ… Choose laravel-api when:

  • You need versioned APIs with inheritance between versions
  • You want integrated routing + documentation from a single source
  • You need CRUD scaffolding with filtering, sorting, pagination
  • You prefer lightweight docblock tags over verbose annotations
  • You want a standardized response format across all endpoints

❌ Choose alternatives when:

  • You need RESTful resource-style URLs (/users/{id})
  • You need full OpenAPI 3.0 compliance (callbacks, webhooks, discriminator)
  • You follow API-first (design-first) methodology
  • You need GraphQL or non-REST APIs
  • You want zero-annotation documentation (β†’ Scramble)

API Reference

Controllers

Class Methods
ApiController success($payload, $status), error($payload, $status), validationError($messages), created($payload), noContent(), notFound($message)
CrudController meta(), search(CrudSearchRequest), create(Request), read(Request, int), update(Request, int), delete(int)
ApiDocumentationController index()

Services

Class Methods
CrudService meta(), query(), resource(), collection(), search(), create(), read(), update(), delete(), restore(), forceDelete()
ApiResponseHelper say($data, $status), sayError($data, $status)

Components

Class Methods
BaseApi (all methods are static) getMethods(), make(), getOpenApiTemplates(), getOpenApiSecurityDefinitions(), beforeCallAction(), afterCallAction(), getMiddleware()
BaseModule getApi($version), makeApi(), getApiVersionList(), getApiPrefix(), getApiUriPattern(), getAvailableApiMethods(), getDocMiddleware()
Meta string($key, $name), integer($key, $name), number($key, $name), boolean($key, $name), hidden($key, $name), select($key, $name, $items), file($key, $name, $src), action($key, $condition), crud(), getOpenApiInputs(), getColumnKeys()

Middleware

Class Purpose
ApiMiddleware Abstract base β€” catches ApiException and generic exceptions
RequestIdMiddleware Generates/propagates X-Request-Id, adds to Log::shareContext()

Exceptions

Class Purpose
ApiException Exception with errorKey string for structured error responses
ApiErrorHandler Registry of exception handlers by class, with parent class traversal

Facades

Facade Resolves to
ApiRequest BaseApiRequest β€” version, controller, action, HTTP method
ApiModule BaseModule β€” version resolution, route configuration
ApiErrorHandler ApiErrorHandler β€” exception handler registry

License

MIT License. See LICENSE.md for details.