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.
Requires
- php: ^8.2
- ext-json: *
- dskripchenko/php-array-helper: ^1.0
- laravel/framework: ^11.0 || ^12.0 || ^13.0
- phpdocumentor/reflection-docblock: ^6.0
Requires (Dev)
- mockery/mockery: ^1.6
- orchestra/testbench: ^10.0
- pestphp/pest: ^3.0
- pestphp/pest-plugin-laravel: ^3.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
- dev-master
- v5.11.0
- v5.10.1
- v5.10.0
- v5.9.2
- v5.9.1
- v5.9.0
- v5.8.0
- v5.7.1
- v5.7.0
- v5.6.2
- v5.6.1
- v5.6.0
- 5.5.1
- 5.5.0
- 5.4.0
- 5.3.0
- 5.2.0
- 5.1.4
- 5.1.3
- 5.1.2
- 5.1.1
- 5.1.0
- 5.0.0
- v4.x-dev
- 4.3.0
- 4.2.1
- 4.2.0
- 4.1.0
- 4.0.5
- 4.0.3
- 4.0.2
- 4.0.1
- 4.0.0
- 3.0.7
- 3.0.6
- 3.0.5
- 3.0.4
- 3.0.3
- 3.0.2
- 3.0.1
- 3.0.0
- 2.5.3
- 2.5.2
- 2.5.1
- 2.5
- 2.4
- 2.3.1
- 2.3.0
- 2.2.0
- 2.1.4
- 2.1.3
- 2.1.2
- 2.1.1
- 2.1.0
- 2.0.0
- v1.x-dev
- 1.0.0
- dev-feat/operation-context-schemas
This package is auto-updated.
Last update: 2026-10-01 21:58:23 UTC
README
dskripchenko/laravel-api
π English Β· Deutsch Β· Π ΡΡΡΠΊΠΈΠΉ Β· δΈζ
π Π ΡΡΡΠΊΠΈΠΉ | 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
- Features
- Installation
- Architecture
- API Versioning
- Routing & Middleware
- OpenAPI 3.0 Documentation
- CRUD Scaffolding
- Testing
- Configuration
- Error Handling
- IDE support
- Comparison with Alternatives
- API Reference
- License
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 endpointGET /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 */
A list of scalars takes [] with no child: @input string $tags[].
Fields known only at runtime
When the fields depend on the route β one generic controller registered under a
key per entity β @input [method] asks a controller method, and the method is
told which operation is being described:
use Dskripchenko\LaravelApi\Services\OpenApi\OperationContext; /** * @input integer $id * @input [entityFields] */ public function update(Request $request) { /* ... */ } public function entityFields(OperationContext $context): array { // $context->controllerKey is `users` on /v1/users/update, `posts` on /v1/posts/update return [ 'type' => 'object', 'properties' => ['email' => ['type' => 'string', 'format' => 'email', 'maxLength' => 255]], 'required' => ['email'], ]; }
The method may return docblock lines (['string $email Email']) or a JSON
Schema object, which goes into the spec as it is β constraints and all. @output [method] works the same way. See docs/docblock-tags.md.
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 $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.
Response envelope. Every response really leaves as {success, payload} (see Response format). public static $responseEnvelope = true; on the Api class makes the spec say so: templates then describe payloads, and every response β @output fields, {Template} refs, error codes β is wrapped. Templates may nest ('client!' => ['email' => 'string(email)'], ['string'], [['id' => 'integer!']]), and a custom envelope is a field map with '{payload}' where the body goes. Details: docblock-tags.md.
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 βemailbecomesstring(email), a rule that is notrequiredbecomes optional,in:a,bbecomes an enumeration. - Marks a
@responsetemplate or a@securityscheme nothing declares, and offers to declare the template β creatinggetOpenApiTemplates()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 Schemeand@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:lintand 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, and also reports an action that validates its input without
declaring any (input.undeclared). 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.