jasonej/laravel-endpoints

Single file endpoints for Laravel.

Maintainers

Package info

github.com/Jasonej/laravel-endpoints

pkg:composer/jasonej/laravel-endpoints

Transparency log

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.0 2026-07-17 04:23 UTC

This package is auto-updated.

Last update: 2026-07-17 04:30:47 UTC


README

Single file endpoints for Laravel.

Latest Version on Packagist Total Downloads Tests

A typical Laravel endpoint is spread across a route, a form request, and a controller. laravel-endpoints collapses all three into a single auto-discovered class: extend Endpoint, declare the route with an attribute, and handle the request in __invoke. Add rules() to validate and authorize() 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.