jasonej / laravel-endpoints
Single file endpoints for Laravel.
Requires
- php: ^8.3||^8.4||^8.5
- illuminate/contracts: ^13.0
- illuminate/routing: ^13.0
- illuminate/support: ^13.0
- illuminate/validation: ^13.0
- spatie/php-attribute-reader: ^1.1
- spatie/php-structure-discoverer: ^2.4
Requires (Dev)
- larastan/larastan: ^3.10
- laravel/pint: ^1.29
- orchestra/testbench: ^11.0
- phpunit/phpunit: ^12.5
README
Single file endpoints for Laravel.
A typical Laravel endpoint is spread across a route, a form request, and a controller.
laravel-endpointscollapses all three into a single auto-discovered class: extendEndpoint, declare the route with an attribute, and handle the request in__invoke. Addrules()to validate andauthorize()to gate access, just like a form request.
#[Post('/api/comments', middleware: ['api', 'auth:sanctum'])] final class CreateCommentEndpoint extends Endpoint { public function authorize(): bool { return Gate::allows('create', Comment::class); } public function rules(): array { return [ 'body' => ['required', 'string'], ]; } public function __invoke() { // ... } }
Installation
You can install the package via composer:
composer require jasonej/laravel-endpoints
You can publish the config file with:
php artisan vendor:publish --tag="endpoints-config"
Usage
Defining an endpoint
Extend Endpoint, declare a route with an attribute, and handle the request in __invoke. No need to register it
in routes/api.php as endpoints are auto-discovered.
<?php declare(strict_types=1); namespace App\Endpoints; use Jasonej\LaravelEndpoints\Attributes\Get; use Jasonej\LaravelEndpoints\Endpoint; #[Get('/status')] final class StatusEndpoint extends Endpoint { public function __invoke(): array { return ['status' => 'ok']; } }
HTTP method attributes
There is one attribute per verb (#[Get], #[Post], #[Put], #[Patch], #[Delete], and #[Options]) plus a
raw #[Route] for an explicit or multi-verb set. Route names accept a string or a BackedEnum.
use Jasonej\LaravelEndpoints\Attributes\Post; #[Post('/posts', name: 'posts.store')] final class CreatePostEndpoint extends Endpoint { /* … */ }
use Jasonej\LaravelEndpoints\Attributes\Route; #[Route(['GET', 'POST'], '/webhook', name: 'webhook')] final class WebhookEndpoint extends Endpoint { /* … */ }
The attributes are repeatable, so you can point several routes at a single handler:
use Jasonej\LaravelEndpoints\Attributes\Patch; use Jasonej\LaravelEndpoints\Attributes\Put; #[Put('/profile', name: 'profile.replace')] #[Patch('/profile', name: 'profile.update')] final class UpdateProfileEndpoint extends Endpoint { /* … */ }
Route options
Every attribute forwards the full set of route options:
| Option | Type | Maps to |
|---|---|---|
name |
string|BackedEnum |
->name() |
middleware |
array |
->middleware() |
withoutMiddleware |
array |
->withoutMiddleware() |
wheres |
array<string, string> |
->where() |
domain |
string |
->domain() |
scopeBindings |
bool |
->scopeBindings() / ->withoutScopedBindings() |
#[Get(
'/posts/{post}',
name: 'posts.show',
middleware: ['api', 'auth:sanctum'],
withoutMiddleware: ['throttle'],
wheres: ['post' => '[0-9]+'],
domain: 'api.example.com',
scopeBindings: true,
)]
final class ShowPostEndpoint extends Endpoint { /* … */ }
Handling the request
Read input with the typed accessors (string(), integer(), boolean(), enum(), array(), all(), …), or
collect it into a DTO. Route-model-bound parameters are injected into __invoke just like any controller action, and
you can return an array, a resource, or a response.
use App\Http\Resources\PostResource; use App\Models\Post; #[Get('/posts/{post}', wheres: ['post' => '[0-9]+'])] final class ShowPostEndpoint extends Endpoint { public function __invoke(Post $post): PostResource { return PostResource::make($post); } }
#[Post('/posts')] final class CreatePostEndpoint extends Endpoint { public function __invoke(): PostResource { $post = Post::create([ 'title' => $this->string('title')->value(), 'body' => $this->string('body')->value(), 'published' => $this->boolean('published'), ]); return PostResource::make($post); } }
Validation and authorization
Both are optional hooks that work just like a form request. Define rules() (with messages() and attributes() to
customize) to validate input, and authorize() to gate access. Validation runs when the endpoint is resolved, and a
failing authorize() throws a 403.
use Illuminate\Support\Facades\Gate; use App\Models\Post; #[Post('/posts', middleware: ['auth:sanctum'])] final class CreatePostEndpoint extends Endpoint { public function authorize(): bool { return Gate::allows('create', Post::class); } public function rules(): array { return [ 'title' => ['required', 'string', 'max:120'], 'body' => ['required', 'string'], ]; } public function messages(): array { return ['title.max' => 'Keep the title under 120 characters.']; } public function attributes(): array { return ['body' => 'post body']; } public function __invoke(): PostResource { /* … */ } }
Configuration and discovery
The package scans the paths in config/endpoints.php for classes extending Endpoint. Publish the config to change
where it looks:
php artisan vendor:publish --tag="endpoints-config"
// config/endpoints.php return [ 'paths' => [ app_path(), // default base_path('modules'), // add your own ], ];
Endpoints can live anywhere under a configured path.
Route caching in production
Discovery reflects over the filesystem on every request that is not route-cached. In production, cache your routes so discovery runs once at build time instead of on every request:
php artisan route:cache
Changelog
Please see CHANGELOG for more information on what has changed recently.
Credits
Please see CONTRIBUTORS for more information on who contributed to the package.
License
The MIT License (MIT). Please see LICENSE for more information.