A practical Laravel package designed to streamline API development by automating resource generation and providing a powerful, out-of-the-box filtering system.
This package accelerates your workflow with two core features:
-
Rapid Scaffolding: Use the
php artisan make:api-resourcecommand to intelligently generate a full set of API files (Controller, Actions, Requests, and Resource) from your existing models. Features:- Complete Scaffolding: Generates Controller, FormRequests, Actions, and API Resources.
- Smart Relation Detection: Automatically distinguishes between "Single" relations (e.g., HasOne, BelongsTo) and "Collection" relations (e.g., HasMany, BelongsToMany).
- Database Driven Validation: Generates validation rules based on your database columns (types, nullable, max length, etc.).
- Advanced Overwriting: Supports force overwriting and selective generation.
Route Suggestions: Provides the correct API route definition upon completion.
-
Powerful Filtering: Equip your API endpoints with a comprehensive set of filters from the moment you install it. Sort, search, and filter resources with ease without any initial setup.
1. Install the package via Composer:
composer require singlequote/laravel-api-resource2. (Optional) Publish Files:
You can publish the configuration and stub files to customize the package's behavior and generated file templates.
Publish the config file:
php artisan vendor:publish --tag=laravel-api-resource-configPublish the stub files:
php artisan vendor:publish --tag=laravel-api-resource-stubsUse the make:api-resource Artisan command to generate all the necessary files for a model's API endpoint.
php artisan make:api-resource UserThe command supports several options to customize the generation process:
| Option | Description |
|---|---|
--force |
Overwrite existing files without asking for confirmation. Useful for CI/CD or quick regeneration. |
--only |
Generate only specific parts. Comma separated. Available: controller, actions, requests, resource. |
--except |
Exclude specific parts from generation. Comma separated. |
--module |
Specify a module name if you are using a modular application structure. |
Example:
php artisan make:api-resource User --forcephp artisan make:api-resource User --only=resource,controllerphp artisan make:api-resource User --except=requestsThe generator recognizes a wide range of Eloquent relationships and generates the appropriate code (e.g., new UserResource vs UserResource::collection):
Single Relations (Returns Object):
- HasOne
- MorphOne
- BelongsTo
- MorphTo
- HasOneThrough
Collection Relations (Returns Array):
- HasMany
- BelongsToMany
- MorphToMany
- MorphMany
- HasManyThrough
- MorphedByMany
You can control which relations are included in the generated API using the #[SkipApiGeneration] attribute on your model methods. This attribute accepts an array of scopes to skip:
SkipApiGeneration::ALL(default): Skips generation for everything.SkipApiGeneration::ACTIONS: Skips generation in Store/Update actions (making the relation read-only).SkipApiGeneration::REQUESTS: Skips validation rules in Requests.SkipApiGeneration::RESOURCE: Skips inclusion in the API Resource.
Example
use SingleQuote\LaravelApiResource\Attributes\SkipApiGeneration;
class User extends Model
{
// Completely ignore this relation
#[SkipApiGeneration]
public function company()
{
return $this->belongsTo(Company::class);
}
// Read-only: visible in resource, but not updateable via API
#[SkipApiGeneration([SkipApiGeneration::ACTIONS, SkipApiGeneration::REQUESTS])]
public function logs()
{
return $this->hasMany(Log::class);
}
}This single command creates the following file structure, ready for you to add your business logic:
App/Http/Controllers
└── Api/UserController.php
App/Actions/Users
├── DeleteUserAction.php
├── IndexUserAction.php
├── ShowUserAction.php
├── StoreUserAction.php
└── UpdateUserAction.php
App/Http/Requests/Users
├── IndexUserRequest.php
├── ShowUserRequest.php
├── StoreUserRequest.php
└── UpdateUserRequest.php
App/Http/Resources
└── UserResource.php
Finally, add the generated route to your routes/api.php file:
use App\Http\Controllers\Api\UserController;
Route::apiResource('users', UserController::class);To enable the powerful filtering capabilities, simply add the HasApi trait to your model.
use Illuminate\Database\Eloquent\Model;
use SingleQuote\LaravelApiResource\Traits\HasApi;
class User extends Model
{
use HasApi;
// ...
}You can now use a wide range of query parameters to filter your API results directly from the URL. See the API Filtering Reference below for a full list of available methods.
The package provides helpers to easily customize your JSON response. For instance, you can use the ApiPolicyService to automatically include the results of your model's policies.
In your UserResource.php:
use SingleQuote\LaravelApiResource\Service\ApiPolicyService;
use Illuminate\Http\Request;
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
// ...
'policies' => ApiPolicyService::defaults($this->resource, ['sendInvite']),
];
}All examples use the Ziggy package for clean URL generation. A manual URL example is provided for reference.
| Helper | Value Type | Description |
|---|---|---|
limit |
number |
Sets the number of results per page. |
search |
array |
Searches specified columns for a query. |
where |
array |
Adds a "where" clause to the query. |
orWhere |
array |
Adds an "or where" clause. |
whereJsonContains |
array |
Queries a relationship with a where condition. |
whereJsonDoesntContain |
array |
Queries a json column with a whereIn condition. |
whereIn |
array |
Filters by a column's value within an array. |
whereNotIn |
array |
Filters by a column's value not in an array. |
whereNull |
string |
Finds records where a column is NULL. |
whereNotNull |
string |
Finds records where a column is not NULL. |
has |
array |
Filters based on the existence of a relationship. |
doesntHave |
array |
Filters based on the absence of a relationship. |
whereRelation |
array |
Queries a relationship with a where condition. |
with |
array |
Eager loads relationships. |
withCount |
array |
Counts the results of a given relationship. |
select |
array |
Selects specific columns to return. |
orderBy |
string |
Sorts the results in ascending order. |
orderByDesc |
string |
Sorts the results in descending order. |
The default limit is 1000. You can change this in the config file or override it per request.
axios.get(route('api.users.index', { limit: 100 }));
// GET /api/users?limit=100Search for a query within specified columns. Use * to search all fillable columns.
axios.get(route('api.users.index', {
search: {
fields: ['name', 'email'],
query: "john"
}
}));
// GET /api/users?search[fields][0]=name&search[fields][1]=email&search[query]=johnBy default the search wraps every column in LOWER(...) so matching is
case-insensitive regardless of your database collation. On large tables this
adds a per-row LOWER() evaluation.
If your columns already use a case-insensitive collation (e.g. MySQL *_ci),
you can drop that overhead — results stay identical — by publishing the config
and setting:
// config/laravel-api-resource.php
'search' => [
'lower' => false,
],Keep it true (the default) on case-sensitive collations (MySQL *_bin) and
on PostgreSQL, where LIKE is case-sensitive and the LOWER() wrapping is
required for case-insensitive matching.
Add "where" clauses. You can also provide an operator (gt, lt, sw, etc.).
| Operator | Shorthand |
|---|---|
| startsWith | sw |
| endsWith | ew |
| notContains | nin |
| contains | in |
| equals | eq |
| notEqual | neq |
| greater | gt |
| greaterEquals | gte |
| lesser | lt |
| lesserEquals | lte |
axios.get(route('api.users.index', {
where: {
date_of_birth: { gt: "1995-01-31" }
}
}));
// GET /api/users?where[date_of_birth][gt]=1995-01-31Enables BETWEEN-like queries using multiple where clauses.
axios.get(route('api.users.index', {
where: [{
date_of_birth: {
lte: "1995-01-30"
}
}, {
date_of_birth: {
gte: "1995-01-15",
},
}],
}));
// GET /api/users?where[0][date_of_birth][lte]=1995-01-30&where[1][date_of_birth][gte]=1995-01-15Use the $or / $and sentinel keys inside where (or orWhere) to wrap
sub-conditions in a parenthesized group. The group is attached to the rest of the
query with the surrounding boolean (AND by default); inside the group the
sub-conditions are combined with the group's own boolean. Groups may be nested.
This makes null-safe filters possible, e.g. "hide rows explicitly flagged true,
but keep rows that are false or where the key is missing":
axios.get(route('api.schedules.index', {
where: {
end: { lte: '2026-08-03 12:00' },
$or: [
{ 'custom_fields->hide_from_dashboard': { neq: 'true' } },
{ 'custom_fields->hide_from_dashboard': 'null' },
],
},
}));
// where[end][lte]=...&where[$or][0][custom_fields->hide_from_dashboard][neq]=true&where[$or][1][custom_fields->hide_from_dashboard]=nullProduces roughly:
... AND `end` <= ?
AND (
json_unquote(json_extract(`custom_fields`, '$."hide_from_dashboard"')) != ?
OR json_extract(`custom_fields`, '$."hide_from_dashboard"') IS NULL
)Notes:
- The
'column': 'null'convention respects the group boolean (itORs inside an$orgroup instead of alwaysAND-ing). - JSON value comparisons are string-based (
json_unquote), so pass the value as it is stored —'true'/'false'for JSON booleans,'1'/'0'for integers.
Adds an alternative where clause using OR logic.
axios.get(route('api.countries.index', {
where: {
name: {
in: 'Netherlands',
},
},
orWhere: {
name: {
in: 'Belgium',
},
},
}));
// GET /api/countries?where[name][in]=Netherlands&orWhere[name][in]=BelgiumVerifies that a column's value is (or is not) within a given array.
axios.get(route('api.users.index', {
whereJsonContains: { data->language: ['en', 'nl'] }
}));
// GET /api/users?whereJsonContains[data->language][0]=en&whereJsonContains[data->language][1]=nlVerifies that a column's value is (or is not) within a given array.
axios.get(route('api.users.index', {
whereIn: { role: ['admin', 'employee'] }
}));
// GET /api/users?whereIn[role][0]=admin&whereIn[role][1]=employeeVerifies that a column's value is NULL or not NULL.
axios.get(route('api.users.index', { whereNull: "email_verified_at" }));
// GET /api/users?whereNull=email_verified_atLimit results based on the existence of a relationship. You can also add nested conditions.
axios.get(route('api.users.index', {
has: {
roles: {
whereIn: { id: [1, 2] }
}
}
}));
// GET /api/users?has[roles][whereIn][id][0]=1&has[roles][whereIn][id][1]=2Query for a relationship's existence with a simple where condition.
axios.get(route('api.users.index', {
whereRelation: {
roles: { name: 'admin' }
}
}));
// GET /api/users?whereRelation[roles][name]=adminEager load relationships to avoid N+1 query problems.
axios.get(route('api.users.index', {
with: {
roles: {
select: ['id', 'name']
}
}
}));
// GET /api/users?with[roles][select][0]=id&with[roles][select][1]=nameCount the number of results from a relationship without loading them.
axios.get(route('api.users.index', { withCount: ['posts'] }));
// GET /api/users?withCount[0]=postsNote: To include the count in your response, you must manually add the posts_count attribute to your resource's toArray method.
// In app/Http/Resources/UserResource.php
public function toArray(Request $request): array
{
return [
// ... other attributes
'posts_count' => $this->whenCounted('posts'),
];
}Specify which columns to retrieve to keep responses lean.
axios.get(route('api.users.index', { select: ['id', 'name'] }));
// GET /api/users?select[0]=id&select[1]=nameSort the results by a given column, including columns on related models.
axios.get(route('api.users.index', { orderBy: 'roles.name' }));
// GET /api/users?orderBy=roles.nameWhen sorting on a related column, records that have no related row are kept
(they are not filtered out). For "to-many" relations (hasMany/morphMany) the
sort uses a correlated subquery, so parent rows are never duplicated and
pagination totals stay correct. Sorting on a morphTo column is undefined and is
ignored.
To make custom columns (e.g., from withCount or withSum) sortable, add them to the $apiOrderBy property on your model.
// In your Product.php model
class Product extends Model
{
public array $apiOrderBy = [
'articles_sum_price', // From a withSum query
];
}Now you can sort by this custom column:
GET /api/products?orderBy=articles_sum_price
Please see CONTRIBUTING.md for details.
You're free to use this package, but if it makes it to your production environment, we highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using.
Our address is: Quotec, Traktieweg 8c 8304 BA, Emmeloord, Netherlands.
The MIT License (MIT). Please see License File for more information.