phpsa/laravel-api-controller
phpsa/laravel-api-controller provides a reusable base controller for building RESTful APIs in Laravel. It simplifies common CRUD endpoints and standardizes JSON responses, reducing boilerplate so you can ship clean, consistent API controllers faster with less repetitive code.
phpsa/laravel-api-controller gives you a base API controller and a base API resource that implement the boring 80% of a JSON CRUD endpoint: pagination, whitelisted filtering, sorting, relationship includes, sparse fieldsets, validation and authorization hooks.
Minimal setup:
composer require phpsa/laravel-api-controlleruse Phpsa\LaravelApiController\Http\Api\Controller;
class PostController extends Controller
{
protected $model = Post::class;
protected $resource = PostResource::class; // optional
}
Route::apiResource('posts', PostController::class);Phpsa\LaravelApiController\Http\Api\Resource to inherit the response envelope/meta helpers.Where to look first, in order:
Controller class and its concern traits — that's where index/show/store/update/destroy and the query-building live.Resource base class for the response shape (data/meta/links).$allowedFilters, $allowedSorts, $allowedIncludes, $allowedFields, $defaultSort, $perPage, etc.).First real use case: a read-only index endpoint. GET /api/posts?filter[status]=published&sort=-created_at&include=author&fields[posts]=id,title&per_page=25 works with only the $model property set — then you tighten it up with whitelists.
Controller as declarative config. The idiomatic use is to keep the controller thin: declare the model, resource, and the whitelists, and let the base class do the work. Override index()/store() only when you genuinely diverge from the default behaviour.
Whitelist everything explicitly. Populate $allowedFilters, $allowedSorts, $allowedIncludes and $allowedFields per controller. Relationships that should be includable go in $allowedIncludes, and the base class eager-loads them, which is where most of the N+1 protection comes from.
Validation. Define rules on the controller for the standard actions, or override store/update to delegate to a FormRequest when rules get large or need Rule::unique()->ignore(). Keep the authorization in policies; the base controller calls into Laravel's gate/policy layer per action.
Custom guard per endpoint (new in v7.4.0). You can now declare a non-default guard on the controller and on the resource. Use it when your API is authenticated by a separate guard (e.g. api, or a token/tenant guard) rather than web:
protected $guard = 'api';
Set the same guard on the resource so authorization and any user-dependent output ($this->request->user($guard)) resolve consistently. This lets you have, say, an admin-only controller on the admin guard and public endpoints on the default guard in the same app.
Custom actions alongside the CRUD set. Add extra methods to the same controller (publish, archive, restore) so they inherit the same middleware, guard and policy conventions, then register explicit routes. Return the same resource type so clients see a consistent envelope.
Multi-controller reuse. Because behaviour is driven by properties, a pattern that works well is a thin abstract base per domain area (e.g. TenantController) that sets the guard and scoping, with concrete controllers extending it and only declaring $model/$resource.
Testing. Feature-test the endpoints with actingAs($user, 'api') matching the guard you configured, and assert on assertJsonStructure rather than exact payloads so resource changes don't churn your suite.
$request->user() falls back to the default guard. If your route uses auth:api but the controller/resource has no $guard, policies will evaluate against the wrong user (or null). New in v7.4.0 you can set $guard on both the controller and the resource — set both, and ensure the guard exists in config/auth.php with a provider that maps to the model you're authorizing. Tests must use the same guard name in actingAs().per_page needs a ceiling. Expose pagination but cap the maximum in config or by overriding the per-page resolution; otherwise a client can request 100k rows.sort=name, make sure that's a column (or map aliases); relationship sorts need explicit joins, not just an allowed-sort entry.with() closures or restrict which includes are allowed per controller.destroy typically returns 204; if your front end expects a body, override it. Unauthorized access surfaces as 403 from policies while missing records are 404 — keep that distinction deliberate in tests.index, store, ...) for behavioural changes; override the model/resource resolution methods for multi-model or polymorphic controllers; publish the package config if you need to change pagination defaults or the response envelope rather than editing every controller.How can I help you explore Laravel packages today?