anil / fast-api-crud
A powerful Laravel package for building RESTful API CRUD operations with minimal boilerplate. Features pagination, filtering, sorting, soft deletes, permissions, lifecycle hooks, and more.
Requires
- illuminate/auth: ^7.0|^8.0|^9.0|^10.0|^11.0
- illuminate/database: ^7.0|^8.0|^9.0|^10.0|^11.0
- illuminate/http: ^7.0|^8.0|^9.0|^10.0|^11.0
- illuminate/pagination: ^7.0|^8.0|^9.0|^10.0|^11.0
- illuminate/routing: ^7.0|^8.0|^9.0|^10.0|^11.0
- illuminate/support: ^7.0|^8.0|^9.0|^10.0|^11.0
- spatie/laravel-permission: 6.*|5.*|4.*
Requires (Dev)
- laravel/pint: ^1.13
- 3.x-dev
- 2.x-dev
- v2.0.4.12
- v2.0.4.11
- v2.0.4.10
- v2.0.4.9
- v2.0.4.8
- v2.0.4.7
- v2.0.4.6
- v2.0.4.5
- v2.0.4.4
- v2.0.4.3
- v2.0.4.2
- v2.0.4.1
- v2.0.4
- v2.0.3
- v2.0.2
- v2.0.1
- v2.0.0
- 1.x-dev
- v1.0.3
- v1.0.2
- v1.0.1
- v1.0.0
- dev-3.x-dev
- dev-dependabot/github_actions/actions-cd3aa6e191
- dev-dependabot/composer/spatie/laravel-permission-tw-5.0or-tw-6.0or-tw-7.0
- dev-HasReplicatesWithRelation
- dev-dev
This package is auto-updated.
Last update: 2026-03-26 11:26:53 UTC
README
A powerful Laravel package that provides full-featured CRUD operations with minimal boilerplate. Works for both API (JSON responses) and Web (Blade views) controllers out of the box.
Supports pagination (length-aware, simple, cursor), filtering, sorting, search, soft deletes, Spatie permissions, lifecycle hooks, and much more.
Supports: Laravel 11, 12, 13 | PHP 8.2+
Requires: spatie/laravel-permission ^6.0 or ^7.0
Table of Contents
- Installation
- Quick Start
- Configuration
- API Controller
- Web Controller
- Scaffolding Command
- Query Parameters
- Controller Properties
- Lifecycle Hooks
- Contracts
- Model Traits
- Builder Macros
- Collection Macros
- API Responder
- Helper Functions
- Exceptions
- Permissions
- Routes
- Pagination Utility
- Enums
- Full Example
Installation
composer require anil/fast-api-crud
Publish the config file (optional):
php artisan vendor:publish --provider="Anil\FastApiCrud\FastApiCrudServiceProvider" --tag=config
Quick Start
1. Generate everything with one command
php artisan fast-api:make-all Post
This creates: Model, Migration, Factory, Seeder, Controller, Resource, Store/Update Requests.
2. The generated controller — zero boilerplate
// app/Http/Controllers/PostController.php class PostController extends BaseController { public function __construct() { parent::__construct( model: Post::class, storeRequest: StorePostRequest::class, updateRequest: UpdatePostRequest::class, resource: PostResource::class, ); } }
3. Register routes
// routes/api.php use App\Http\Controllers\PostController; Route::get('posts', [PostController::class, 'index']); Route::post('posts', [PostController::class, 'store']); Route::get('posts/{id}', [PostController::class, 'show']); Route::put('posts/{id}', [PostController::class, 'update']); Route::delete('posts/{id}', [PostController::class, 'destroy']);
4. That's it! You now have a fully working CRUD API
GET /posts → Paginated list with filtering, sorting, search
GET /posts/1 → Single resource
POST /posts → Create (validates via StorePostRequest)
PUT /posts/1 → Update (validates via UpdatePostRequest)
DELETE /posts/1 → Soft delete (or force delete)
Configuration
File: config/fast-api.php
return [ // Pagination 'pagination' => [ 'default_per_page' => 15, // Default records per page 'max_per_page' => 100, // Maximum allowed per page 'allow_all' => true, // Allow rowsPerPage=0 to fetch all ], // Soft Delete 'soft_delete' => [ 'anonymize_unique_columns' => true, // Append _{timestamp} to unique columns on delete ], // Response envelope keys 'response' => [ 'success_key' => 'data', 'error_key' => 'errors', 'message_key' => 'message', ], // Spatie permissions 'permissions' => [ 'enabled' => true, ], // Web (Blade) flash message keys 'web' => [ 'flash_key_success' => 'success', 'flash_key_error' => 'error', ], ];
API Controller (BaseController)
For JSON API endpoints. Returns JsonResponse, JsonResource, and AnonymousResourceCollection.
Constructor
use Anil\FastApiCrud\Http\Controllers\BaseController; class PostController extends BaseController { public function __construct() { parent::__construct( model: Post::class, storeRequest: StorePostRequest::class, updateRequest: UpdatePostRequest::class, resource: PostResource::class, ); } }
Available Methods
| Method | HTTP | Return Type | Description |
|---|---|---|---|
index() |
GET | AnonymousResourceCollection |
Paginated list |
show($id) |
GET | JsonResource |
Single resource |
store() |
POST | JsonResponse (201) |
Create resource |
update($id) |
PUT | JsonResource|JsonResponse |
Update resource |
destroy($id) |
DELETE | JsonResponse (204) |
Delete resource |
delete() |
POST | JsonResponse (204) |
Bulk delete via delete_rows array |
changeStatus($id, $column = 'status') |
PUT | JsonResource|JsonResponse |
Toggle boolean column (0/1) |
updateColumn($id, $column = 'status') |
PUT | JsonResource|JsonResponse |
Update specific fillable column |
restore($id) |
PUT | JsonResource|JsonResponse |
Restore soft-deleted |
restoreAll() |
POST | JsonResponse (204) |
Restore all trashed |
permanentDelete($id) |
POST | JsonResponse (204) |
Force delete trashed |
Methods returning JsonResource|JsonResponse return JsonResponse when an exception occurs during the operation.
Example Responses
GET /posts (index):
{
"data": [
{
"id": 1,
"name": "First Post",
"status": 1,
"created_at": "2025-01-15T10:30:00.000000Z"
},
{
"id": 2,
"name": "Second Post",
"status": 1,
"created_at": "2025-01-16T14:00:00.000000Z"
}
],
"links": {
"first": "http://example.com/posts?page=1",
"last": "http://example.com/posts?page=5",
"prev": null,
"next": "http://example.com/posts?page=2"
},
"meta": {
"current_page": 1,
"from": 1,
"last_page": 5,
"per_page": 15,
"to": 15,
"total": 68
}
}
GET /posts/1 (show):
{
"data": {
"id": 1,
"name": "First Post",
"desc": "Post description",
"status": 1,
"active": 1,
"created_at": "2025-01-15T10:30:00.000000Z",
"updated_at": "2025-01-15T10:30:00.000000Z"
}
}
POST /posts (store — 201 Created):
{
"data": {
"id": 3,
"name": "New Post",
"desc": "Description",
"status": 1,
"active": 1,
"created_at": "2025-01-17T09:00:00.000000Z",
"updated_at": "2025-01-17T09:00:00.000000Z"
}
}
DELETE /posts/1 (destroy — 204):
{
"data": []
}
Error response (400):
{
"errors": [],
"message": "Something went wrong"
}
Validation error (422 — handled by Laravel):
{
"message": "The name field is required.",
"errors": {
"name": ["The name field is required."]
}
}
Web Controller (BaseWebController)
For Blade/web applications. Returns View and RedirectResponse with flash messages.
Constructor
use Anil\FastApiCrud\Http\Controllers\BaseWebController; class PostController extends BaseWebController { public function __construct() { parent::__construct( model: Post::class, storeRequest: StorePostRequest::class, updateRequest: UpdatePostRequest::class, viewPrefix: 'admin.posts', // views: admin.posts.index, admin.posts.create, etc. routePrefix: 'admin.posts', // redirects: route('admin.posts.index') resourceName: 'post', // $post variable in views collectionName: 'posts', // $posts variable in index view resource: PostResource::class, // Optional — for data transformation before views ); } }
Available Methods
| Method | HTTP | Return | Description |
|---|---|---|---|
index() |
GET | View |
List page ({viewPrefix}.index) |
create() |
GET | View |
Create form ({viewPrefix}.create) |
store() |
POST | RedirectResponse |
Create, redirect with flash |
show($id) |
GET | View |
Detail page ({viewPrefix}.show) |
edit($id) |
GET | View |
Edit form ({viewPrefix}.edit) |
update($id) |
PUT | RedirectResponse |
Update, redirect with flash |
destroy($id) |
DELETE | RedirectResponse |
Delete, redirect with flash |
delete() |
POST | RedirectResponse |
Bulk delete, redirect |
changeStatus($id) |
PUT | RedirectResponse |
Toggle status, redirect |
restore($id) |
PUT | RedirectResponse |
Restore, redirect |
restoreAll() |
POST | RedirectResponse |
Restore all, redirect |
permanentDelete($id) |
POST | RedirectResponse |
Force delete, redirect |
View Variables
- index:
$posts(or your$collectionName) — paginated collection - show/edit:
$post(or your$resourceName) — single model instance
Customizing Flash Messages
Override any of the message methods in your controller. All return string and work with Laravel's __() translation helper:
class PostController extends BaseWebController { protected function storeSuccessMessage(): string { return __('Post created!'); } protected function updateSuccessMessage(): string { return __('Post updated!'); } protected function destroySuccessMessage(): string { return __('Post deleted.'); } protected function bulkDeleteSuccessMessage(): string { return __('Posts deleted.'); } protected function statusChangeSuccessMessage(): string { return __('Status updated.'); } protected function columnUpdateSuccessMessage(): string { return __('Column updated.'); } protected function restoreSuccessMessage(): string { return __('Post restored.'); } protected function restoreAllSuccessMessage(): string { return __('All posts restored.'); } protected function permanentDeleteSuccessMessage(): string { return __('Post permanently deleted.'); } }
Flash messages use session keys from config (fast-api.web.flash_key_success and fast-api.web.flash_key_error).
Overriding Views
Override any method to pass extra data to views:
class PostController extends BaseWebController { public function create(): \Illuminate\View\View { return view($this->viewName('create'), [ 'categories' => Category::all(), 'tags' => Tag::all(), ]); } public function edit(int|string $id): \Illuminate\View\View { $query = $this->buildShowQuery(); return view($this->viewName('edit'), [ $this->resourceName => $query->findOrFail($id), 'categories' => Category::all(), ]); } }
Blade View Example
{{-- resources/views/admin/posts/index.blade.php --}} @extends('layouts.app') @section('content') <div class="container"> @if(session('success')) <div class="alert alert-success">{{ session('success') }}</div> @endif @if(session('error')) <div class="alert alert-danger">{{ session('error') }}</div> @endif <a href="{{ route('admin.posts.create') }}">Create Post</a> <table> <thead> <tr> <th>ID</th> <th>Name</th> <th>Actions</th> </tr> </thead> <tbody> @foreach($posts as $post) <tr> <td>{{ $post->id }}</td> <td>{{ $post->name }}</td> <td> <a href="{{ route('admin.posts.show', $post) }}">View</a> <a href="{{ route('admin.posts.edit', $post) }}">Edit</a> <form action="{{ route('admin.posts.destroy', $post) }}" method="POST" class="d-inline"> @csrf @method('DELETE') <button type="submit">Delete</button> </form> </td> </tr> @endforeach </tbody> </table> {{ $posts->links() }} </div> @endsection
Web Routes
// routes/web.php use App\Http\Controllers\PostController; Route::resource('admin/posts', PostController::class); // Additional routes for extended operations Route::put('admin/posts/{id}/status-change', [PostController::class, 'changeStatus']); Route::put('admin/posts/{id}/restore', [PostController::class, 'restore']); Route::post('admin/posts/restore-all', [PostController::class, 'restoreAll']); Route::post('admin/posts/{id}/permanent-delete', [PostController::class, 'permanentDelete']);
Scaffolding Command
# Generate API controller + resources php artisan fast-api:make-all Post # Generate Web controller + Blade views php artisan fast-api:make-all Post --web # Multiple models at once php artisan fast-api:make-all Post,Tag,Category # Multiple models with web php artisan fast-api:make-all Post,Tag,Category --web
Generated files per model:
| File | Path |
|---|---|
| Model | app/Models/Post.php |
| Migration | database/migrations/create_posts_table.php |
| Factory | database/factories/PostFactory.php |
| Seeder | database/seeders/PostSeeder.php |
| Resource | app/Http/Resources/Post/PostResource.php |
| Store Request | app/Http/Requests/Post/StorePostRequest.php |
| Update Request | app/Http/Requests/Post/UpdatePostRequest.php |
| Controller | app/Http/Controllers/PostController.php |
| Views (--web) | resources/views/posts/index.blade.php, create.blade.php, edit.blade.php, show.blade.php |
Query Parameters
The package reads these query parameters automatically:
Filtering
GET /posts?filters={"active":1,"status":1,"queryFilter":"search term"}
The filters parameter accepts a JSON object. Each key is matched against the model's scopes:
activecallsscopeActive(1)on the modelqueryFiltercallsscopeQueryFilter("search term")
Sorting
GET /posts?sortBy=created_at&descending=true
sortBy— column name to sort by (default:id)descending—truefor DESC,falsefor ASC (default:true)
If the model implements Sortable, its defaults are used when no sort params are provided.
Pagination
GET /posts?rowsPerPage=25
GET /posts?rowsPerPage=0 # Returns all records (if allow_all is true)
rowsPerPage— records per page (default: 15, max: 100)
Search
GET /posts?search=laravel
If the model implements the Searchable interface, performs a LIKE search across the columns returned by searchableColumns().
Combined Example
GET /posts?filters={"active":1}&sortBy=name&descending=false&rowsPerPage=20&search=laravel
Controller Properties
Customize behavior by setting properties in your controller:
class PostController extends BaseController { // Pagination protected PaginationType $paginationType = PaginationType::LengthAware; // Options: LengthAware, Simple, Cursor, None // Index query customization protected array $scopes = ['active']; // Apply scopes to index protected array $with = ['user', 'tags']; // Eager load in index protected array $withCount = ['comments']; // Count relations in index protected array $withAggregate = ['ratings' => 'score']; // Aggregates in index // Show query customization protected array $loadScopes = []; // Scopes for show protected array $load = ['user', 'tags', 'comments']; // Eager load in show protected array $loadCount = ['comments']; // Count in show protected array $loadAggregate = []; // Aggregates in show // Operation scopes protected array $deleteScopes = []; // Scopes when finding record for delete protected array $updateScopes = []; // Scopes when finding record for update protected array $columnScopes = []; // Scopes for changeStatus/updateColumn protected array $restoreScopes = []; // Scopes for restore operations // Delete behavior protected bool $forceDelete = false; // true = permanent delete, false = soft delete public function __construct() { parent::__construct( model: Post::class, storeRequest: StorePostRequest::class, updateRequest: UpdatePostRequest::class, resource: PostResource::class, ); } }
Scopes with Parameters
// Simple scopes (no parameters) protected array $scopes = ['active', 'published']; // Parameterized scopes protected array $scopes = [ 'status' => 1, 'active' => 1, 'type' => 'article', ]; // Array parameters protected array $scopes = [ 'statusIn' => [1, 2, 3], ]; // Closure parameters protected array $scopes = [ 'custom' => function ($query) { $query->where('featured', true); }, ]; // Mixed protected array $scopes = [ 'active', 'status' => 1, ];
All scope properties ($scopes, $loadScopes, $deleteScopes, $updateScopes, $columnScopes, $restoreScopes) support the same syntax.
Lifecycle Hooks
Define methods on your model to hook into CRUD operations. These are called automatically by the controller.
class Post extends Model { // Called before/after store() public function beforeCreate(): void { $this->slug = Str::slug($this->name); } public function afterCreate(): void { // Sync relations from request if (request()->filled('tag_ids')) { $this->tags()->sync(request()->input('tag_ids')); } } // Called before/after update() public function beforeUpdate(): void { } public function afterUpdate(): void { } // Called before/after destroy() and delete() public function beforeDelete(): void { } public function afterDelete(): void { } // Called before/after changeStatus() public function beforeStatusChange(): void { } public function afterStatusChange(): void { } // Called before/after updateColumn() public function beforeColumnUpdate(): void { } public function afterColumnUpdate(): void { } // Called before/after restore() public function beforeRestore(): void { } public function afterRestore(): void { } // Called before/after permanentDelete() public function beforeForceDelete(): void { } public function afterForceDelete(): void { } }
You can also override hooks in the controller:
class PostController extends BaseController { protected function afterCreate(Model $model): void { // Controller-level hook overrides model hook Notification::send($admins, new PostCreated($model)); } }
Contracts
Searchable
Enables automatic LIKE search on ?search= query parameter.
use Anil\FastApiCrud\Contracts\Searchable; class Post extends Model implements Searchable { public function searchableColumns(): array { return [ 'name', 'desc', 'user:name,email', // Search in related model columns ]; } }
Request: GET /posts?search=laravel
Generates: WHERE (name LIKE '%laravel%' OR desc LIKE '%laravel%' OR EXISTS (SELECT ... FROM users WHERE name LIKE '%laravel%' OR email LIKE '%laravel%'))
Sortable
Provides default sort configuration when no sortBy query parameter is given.
use Anil\FastApiCrud\Contracts\Sortable; class Post extends Model implements Sortable { public function sortByDefaults(): array { return [ 'sortBy' => 'created_at', 'sortByDesc' => true, ]; } }
HasPermissionSlug
Enables automatic Spatie permission middleware registration.
use Anil\FastApiCrud\Contracts\HasPermissionSlug; class Post extends Model implements HasPermissionSlug { public function getPermissionSlug(): string { return 'posts'; } }
This automatically registers middleware:
| Action | Permission | Routes |
|---|---|---|
| View | view-posts |
index, show |
| Store | store-posts |
store |
| Update | update-posts |
update, updateColumn |
| Delete | delete-posts |
destroy, delete, permanentDelete |
| Change Status | change-status-posts |
changeStatus |
| Restore | restore-posts |
restore, restoreAll |
Model Traits
HasDateScopes
Adds query scopes for common date ranges. All accept an optional $column parameter (default: created_at).
use Anil\FastApiCrud\Concerns\HasDateScopes; class Post extends Model { use HasDateScopes; }
Available scopes:
Post::query()->today(); // Records from today Post::query()->yesterday(); // Records from yesterday Post::query()->thisWeek(); // Monday to now Post::query()->lastWeek(); // Last Monday to Sunday Post::query()->monthToDate(); // 1st of month to now Post::query()->thisMonth(); // Entire current month Post::query()->lastMonth(); // Entire previous month Post::query()->quarterToDate(); // Start of quarter to now Post::query()->lastQuarter(); // Previous quarter Post::query()->yearToDate(); // January 1 to now Post::query()->lastYear(); // Last 12 months Post::query()->last7Days(); // Last 7 days Post::query()->last30Days(); // Last 30 days Post::query()->date('2025-01-01 to 2025-01-31'); // Custom range // Use a different column Post::query()->today('published_at'); Post::query()->lastMonth('updated_at');
HasUuidPrimaryKey
Automatically assigns UUID v4 as primary key on model creation.
use Anil\FastApiCrud\Concerns\HasUuidPrimaryKey; class Post extends Model { use HasUuidPrimaryKey; }
- Sets
incrementingtofalse - Sets
keyTypetostring - Auto-generates UUID on creation if key is empty
AnonymizesOnDelete
Anonymizes unique column values on soft delete to prevent constraint violations.
use Anil\FastApiCrud\Concerns\AnonymizesOnDelete; class User extends Model { use SoftDeletes, AnonymizesOnDelete; }
When soft-deleted, unique columns get _{timestamp} appended:
email: john@example.com → john@example.com_1705312800
This prevents conflicts when creating a new user with john@example.com while the old record is soft-deleted. Controlled by fast-api.soft_delete.anonymize_unique_columns config.
ReplicatesWithRelations
Replicate a model along with all its loaded relations.
use Anil\FastApiCrud\Concerns\ReplicatesWithRelations; class Post extends Model { use ReplicatesWithRelations; } // Usage $post = Post::with(['tags', 'comments', 'author'])->find(1); $clone = $post->replicateWithRelations(); // $clone is a saved copy with all relations duplicated
Supported relations: BelongsTo, MorphTo, HasOne, MorphOne, HasMany, MorphMany, BelongsToMany, MorphToMany
Not supported: HasOneThrough and HasManyThrough relations will throw an Exception during replication.
The trait also re-applies castable attributes (numeric, boolean, string, json) to the replicated model to ensure proper type handling.
Builder Macros
These macros are registered on Illuminate\Database\Eloquent\Builder and available on all queries.
initializer
->initializer(bool $orderBy = true): Builder
Apply request-based filters, sorting, and scopes automatically.
$query = Post::query()->initializer(); // With orderBy disabled $query = Post::query()->initializer(orderBy: false);
How it works:
- Reads
?filters={"scope":"value"}— decodes JSON, calls matching model scopes (usesStr::studlyto findscope{Name}methods) - Reads
?sortBy=column&descending=true— applies ordering - If model implements
Sortableand no sort params given, usessortByDefaults() - Default sort:
iddescending
likeWhere
->likeWhere(array $attributes, ?string $searchTerm = null): Builder
Multi-column LIKE search with relation support. Returns the query unmodified if $searchTerm is null or empty.
// Simple columns Post::query()->likeWhere(['name', 'desc'], 'laravel'); // With relation columns (colon syntax: 'relation:column1,column2') Post::query()->likeWhere(['name', 'user:name,email', 'tags:name'], 'search'); // Null search returns query unchanged Post::query()->likeWhere(['name'], null); // No-op
paginates
->paginates(array $columns = ['*'], string $pageName = 'page', ?int $page = null): Paginator
Length-aware pagination using rowsPerPage request parameter.
Post::query()->paginates(); // Default columns Post::query()->paginates(['id', 'name']); // Specific columns Post::query()->paginates(['*'], 'p', 2); // Custom page name & page
- Respects
fast-api.pagination.max_per_page(default: 100) - When
rowsPerPage=0andfast-api.pagination.allow_all=true, returns all records
simplePaginates
->simplePaginates(array $columns = ['*'], string $pageName = 'page', ?int $page = null): Paginator
Simple pagination (no total count) using rowsPerPage request parameter. Same behavior as paginates() but without total count query.
cursorPaginates
->cursorPaginates(array $columns = ['*'], ?string $cursorName = null, ?Cursor $cursor = null): CursorPaginator
Cursor-based pagination using rowsPerPage request parameter. Best for infinite scroll or large datasets.
withAggregates
->withAggregates(array $aggregates): Builder
Apply multiple aggregate functions in a single call.
Post::query()->withAggregates([ 'comments' => 'id', // withAggregate('comments', 'id') 'ratings' => ['score', 'avg'], // withAggregate('ratings', 'score', 'avg') ]);
withCountWhereHas / orWithCountWhereHas
->withCountWhereHas(string $relation, ?Closure $callback = null, string $operator = '>=', int $count = 1): Builder ->orWithCountWhereHas(string $relation, ?Closure $callback = null, string $operator = '>=', int $count = 1): Builder
Adds a conditional withCount that also filters results using whereHas (or orWhereHas).
// Basic — posts that have at least 1 comment, with comment count Post::query()->withCountWhereHas('comments'); // With callback — posts with approved comments Post::query()->withCountWhereHas('comments', function ($q) { $q->where('approved', true); }); // With operator/count — posts with 5+ comments Post::query()->withCountWhereHas('comments', null, '>=', 5); // OR variant — posts with comments OR tags Post::query() ->withCountWhereHas('comments') ->orWithCountWhereHas('tags');
Collection Macros
paginate
->paginate(int $perPage, ?int $total = null, ?int $page = null, string $pageName = 'page'): LengthAwarePaginator
Paginate an in-memory collection.
$items = collect([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); $paginated = $items->paginate(perPage: 5); // Page 1: [1, 2, 3, 4, 5] — auto-resolves current page from request $paginated = $items->paginate(perPage: 3, page: 2); // Page 2: [4, 5, 6] $paginated = $items->paginate(perPage: 5, total: 100); // Override total count (useful for pre-sliced data)
API Responder
The HasApiResponse trait (used by BaseController) provides response helpers for every HTTP status code. You can also use it in any controller:
use Anil\FastApiCrud\Concerns\HasApiResponse; class MyController extends Controller { use HasApiResponse; }
Core Methods
// success(array $data = [], int $code = 200): JsonResponse return $this->success(['key' => 'value']); // {"data": {"key": "value"}} return $this->success(['key' => 'value'], 201); // Same with 201 status // error(string $message = 'Something went wrong', array $data = [], int $status = 400): JsonResponse return $this->error('Something went wrong'); // {"errors": [], "message": "Something went wrong"} return $this->error('Not found', [], 404); // Same with 404 status
The envelope keys (data, errors, message) are configurable via fast-api.response.* config.
Complete Method Reference
All success methods accept array $data = []. All error methods accept string $message and array $data = [].
1xx Informational:
| Method | Status | Default Message |
|---|---|---|
continue() |
100 | — |
switchingProtocols() |
101 | — |
processing() |
102 | — |
earlyHints() |
103 | — |
2xx Success:
| Method | Status | Notes |
|---|---|---|
ok() |
200 | — |
created() |
201 | — |
accepted() |
202 | — |
nonAuthoritativeInformation() |
203 | — |
noContent() |
204 | Returns null body |
resetContent() |
205 | — |
partialContent() |
206 | — |
multiStatus() |
207 | — |
alreadyReported() |
208 | — |
imUsed() |
226 | — |
3xx Redirection:
| Method | Status |
|---|---|
multipleChoices() |
300 |
movedPermanently() |
301 |
found() |
302 |
seeOther() |
303 |
notModified() |
304 |
useProxy() |
305 |
temporaryRedirect() |
307 |
permanentRedirect() |
308 |
4xx Client Error:
| Method | Status | Default Message |
|---|---|---|
badRequest() |
400 | Bad Request |
unauthorized() |
401 | Unauthorized |
paymentRequired() |
402 | Payment Required |
forbidden() |
403 | Forbidden |
notFound() |
404 | Not Found |
methodNotAllowed() |
405 | Method Not Allowed |
notAcceptable() |
406 | Not Acceptable |
proxyAuthenticationRequired() |
407 | Proxy Authentication Required |
requestTimeout() |
408 | Request Timeout |
conflict() |
409 | Conflict |
gone() |
410 | Gone |
lengthRequired() |
411 | Length Required |
preconditionFailed() |
412 | Precondition Failed |
contentTooLarge() |
413 | Content Too Large |
uriTooLong() |
414 | URI Too Long |
unsupportedMediaType() |
415 | Unsupported Media Type |
rangeNotSatisfiable() |
416 | Range Not Satisfiable |
expectationFailed() |
417 | Expectation Failed |
imATeapot() |
418 | I'm a teapot |
misdirectedRequest() |
421 | Misdirected Request |
unprocessableContent() |
422 | Unprocessable Content |
locked() |
423 | Locked |
failedDependency() |
424 | Failed Dependency |
tooEarly() |
425 | Too Early |
upgradeRequired() |
426 | Upgrade Required |
preconditionRequired() |
428 | Precondition Required |
tooManyRequests() |
429 | Too Many Requests |
requestHeaderFieldsTooLarge() |
431 | Request Header Fields Too Large |
unavailableForLegalReasons() |
451 | Unavailable For Legal Reasons |
5xx Server Error:
| Method | Status | Default Message |
|---|---|---|
internalServerError() |
500 | Internal Server Error |
notImplemented() |
501 | Not Implemented |
badGateway() |
502 | Bad Gateway |
serviceUnavailable() |
503 | Service Unavailable |
gatewayTimeout() |
504 | Gateway Timeout |
httpVersionNotSupported() |
505 | HTTP Version Not Supported |
variantAlsoNegotiates() |
506 | Variant Also Negotiates |
insufficientStorage() |
507 | Insufficient Storage |
loopDetected() |
508 | Loop Detected |
notExtended() |
510 | Not Extended |
networkAuthenticationRequired() |
511 | Network Authentication Required |
Helper Functions
Global helper functions available throughout your application (autoloaded via composer).
Date & Time
diffForHumans(?string $date): ?string // diffForHumans('2025-01-15') → "2 months ago" // diffForHumans(null) → null ymdDate(?string $date, string $format = 'Y-m-d'): ?string // ymdDate('2025-01-15 14:30:00') → "2025-01-15" // ymdDate('2025-01-15', 'd/m/Y') → "15/01/2025" dateForReports(?string $date, string $format = 'Y-m-d H:i'): ?string // dateForReports('2025-01-15 14:30:00') → "2025-01-15 14:30" // Returns null on invalid date (doesn't throw) toFormattedDateString(?string $date): ?string // toFormattedDateString('2025-01-15') → "January 15, 2025" toDateString(?string $date): ?string // toDateString('2025-01-15 14:30:00') → "2025-01-15" toDateTimeString(?string $date): ?string // toDateTimeString('2025-01-15 14:30') → "2025-01-15 14:30:00" toTimeString(?string $date): ?string // toTimeString('2025-01-15 14:30:00') → "14:30:00"
Duration
parseTimeToSeconds(string $timeString): int|float // parseTimeToSeconds('01:30:00') → 5400 (H:i:s format) // parseTimeToSeconds('30:45') → 1845 (i:s format) // parseTimeToSeconds('120') → 120 (plain seconds) formatDuration(int|float|null $duration, ?string $format = '%y %mo %d %h %m %s', string $separator = ' '): string // formatDuration(3661) → "1h 1m 1s" // formatDuration(90061) → "1d 1h 1m 1s" // formatDuration(3661, '%h %m') → "1h 1m" // formatDuration(3661, '%h %m', '-') → "1h-1m" // formatDuration(null) → "0s" // formatDuration(0) → "0s" // formatDuration(-3661) → "1h 1m 1s" (absolute value) // Placeholders: %y=years, %mo=months, %d=days, %h=hours, %m=minutes, %s=seconds
Filtering & Sorting
filterValue(string $key = 'date'): ?string // Reads from ?filters={"date":"2025-01-15"} query parameter // filterValue('date') → "2025-01-15" // filterValue('missing') → null arrayFilters(array|string|null $data): array // arrayFilters('{"active":1,"name":""}')→ ['active' => 1] (removes falsy) // arrayFilters(['a' => 1, 'b' => null]) → ['a' => 1] // arrayFilters(null) → [] flattenArray(array $data, int $depth = 0): array // flattenArray(['a' => [1, 2], 'b' => [3]]) → [1, 2, 3] // flattenArray(['a' => [1, [2]]], 1) → [1, [2]] sortDirection(): string // Reads ?descending query parameter // ?descending=true → "ASC" // ?descending=false → "DESC" sortBy(): array|string|null // Reads ?sort query parameter // ?sort=name → "name" // ?sort[]=name&sort[]=id → ["name", "id"] // (not set) → null
Utility
uuid(): UuidInterface // uuid() → Ramsey\Uuid\UuidInterface (v4) slug(?string $text = null): ?string // slug('Hello World') → "hello-world" // slug(null) → null relativePath(string $path): string // relativePath('/var/www/app/Models/Post.php') → "app/Models/Post.php" // (relative to base_path()) classShortName(string $param): ?string // classShortName(App\Models\Post::class) → "Post" // Uses reflection, throws ReflectionException if unresolvable
Introspection
scopeMethods(object $class): array // scopeMethods(new Post) → ["scopeActive", "scopePublished", ...] // Returns all public methods starting with "scope" tableColumns(string|Model $table = 'users'): array // tableColumns('posts') → ["id", "active", "desc", "name", ..., "created_at", "updated_at", "deleted_at"] // tableColumns(Post::class) → Same (accepts class-string) // tableColumns(new Post) → Same (accepts model instance) // Columns sorted alphabetically, with id first and timestamps last fillableCsv(string $model): string // fillableCsv(Post::class) → "name,desc,status,active" // fillableCsv('posts') → All columns from table (if not a model class) columnsCsv(string $model, string $separator = ','): string // columnsCsv(Post::class) → "id,active,desc,name,status,created_at,updated_at" // columnsCsv(Post::class, '|') → "id|active|desc|name|status|created_at|updated_at"
Class Discovery
appClasses(string $path = 'App', array $excluding = []): array // appClasses('Models') → ["App\Models\Post", "App\Models\User", ...] // appClasses('Models', [App\Models\User::class]) → ["App\Models\Post", ...] databaseClasses(?string $directory = null, array $excluding = []): array // databaseClasses('seeders') → ["Database\Seeders\PostSeeder", ...] // databaseClasses() → All classes in database/ directory // databaseClasses('factories', ['Database\Factories\UserFactory']) → Filtered list
Exceptions
ApiException
Custom exception that renders as JSON with debug info in development.
use Anil\FastApiCrud\Exceptions\ApiException; throw new ApiException('Resource not found', 404);
Production response:
{
"error": {
"message": "Resource not found"
}
}
Debug response (when APP_DEBUG=true):
{
"error": {
"message": "Resource not found",
"file": "/app/Http/Controllers/PostController.php",
"line": 42
}
}
Permissions
Automatic Registration
If your model implements HasPermissionSlug and fast-api.permissions.enabled is true, permission middleware is registered automatically in the constructor.
Static Middleware (Laravel 11+ style)
For the modern HasMiddleware interface:
use Illuminate\Routing\Controllers\HasMiddleware; class PostController extends BaseController implements HasMiddleware { public static function middleware(): array { return self::permissionMiddleware('posts'); } public function __construct() { parent::__construct( model: Post::class, storeRequest: StorePostRequest::class, updateRequest: UpdatePostRequest::class, resource: PostResource::class, ); } }
Routes
Full API Route Setup
use App\Http\Controllers\PostController; // Standard CRUD Route::get('posts', [PostController::class, 'index'])->name('posts.index'); Route::post('posts', [PostController::class, 'store'])->name('posts.store'); Route::get('posts/{id}', [PostController::class, 'show'])->name('posts.show'); Route::put('posts/{id}', [PostController::class, 'update'])->name('posts.update'); Route::delete('posts/{id}', [PostController::class, 'destroy'])->name('posts.destroy'); // Bulk delete Route::post('posts/delete', [PostController::class, 'delete'])->name('posts.delete'); // Status & column Route::put('posts/{id}/status-change', [PostController::class, 'changeStatus'])->name('posts.changeStatus'); Route::put('posts/{id}/status-change/{column}', [PostController::class, 'updateColumn'])->name('posts.updateColumn'); // Soft delete operations Route::put('posts/{id}/restore', [PostController::class, 'restore'])->name('posts.restore'); Route::post('posts/restore-all', [PostController::class, 'restoreAll'])->name('posts.restoreAll'); Route::post('posts/{id}/force-delete', [PostController::class, 'permanentDelete'])->name('posts.permanentDelete');
Full Web Route Setup
use App\Http\Controllers\PostController; // Standard resource routes (index, create, store, show, edit, update, destroy) Route::resource('posts', PostController::class); // Extended operations Route::put('posts/{id}/status-change', [PostController::class, 'changeStatus'])->name('posts.changeStatus'); Route::put('posts/{id}/restore', [PostController::class, 'restore'])->name('posts.restore'); Route::post('posts/restore-all', [PostController::class, 'restoreAll'])->name('posts.restoreAll'); Route::post('posts/{id}/force-delete', [PostController::class, 'permanentDelete'])->name('posts.permanentDelete');
Pagination Utility
The Anil\FastApiCrud\Support\Pagination class provides static helpers used internally by the macros. You can also use them directly:
use Anil\FastApiCrud\Support\Pagination; Pagination::defaultPerPage(); // 15 (from config) Pagination::maxPerPage(); // 100 (from config) Pagination::requestedPerPage(15); // Value of ?rowsPerPage or default Pagination::resolvePerPage(); // Effective per-page (respects max, allow_all) // Generic config helpers Pagination::configInt('fast-api.pagination.default_per_page', 15); // int Pagination::configBool('fast-api.pagination.allow_all', true); // bool
Enums
PaginationType
use Anil\FastApiCrud\Enums\PaginationType; PaginationType::LengthAware // 'length-aware' — Standard pagination with total count PaginationType::Simple // 'simple' — Simple pagination without total PaginationType::Cursor // 'cursor' — Cursor-based pagination PaginationType::None // 'none' — No pagination, returns all records
CrudAction
Used for permission middleware registration.
use Anil\FastApiCrud\Enums\CrudAction; CrudAction::View // 'view' CrudAction::Store // 'store' CrudAction::Update // 'update' CrudAction::Delete // 'delete' CrudAction::ChangeStatus // 'change-status' CrudAction::Restore // 'restore'
Full Example
Model
namespace App\Models; use Anil\FastApiCrud\Concerns\HasDateScopes; use Anil\FastApiCrud\Concerns\AnonymizesOnDelete; use Anil\FastApiCrud\Contracts\HasPermissionSlug; use Anil\FastApiCrud\Contracts\Searchable; use Anil\FastApiCrud\Contracts\Sortable; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; class Post extends Model implements Searchable, Sortable, HasPermissionSlug { use SoftDeletes, HasDateScopes, AnonymizesOnDelete; protected $fillable = ['name', 'desc', 'status', 'active', 'user_id']; // --- Contracts --- public function searchableColumns(): array { return ['name', 'desc', 'user:name,email']; } public function sortByDefaults(): array { return ['sortBy' => 'created_at', 'sortByDesc' => true]; } public function getPermissionSlug(): string { return 'posts'; } // --- Relations --- public function user() { return $this->belongsTo(User::class); } public function tags() { return $this->belongsToMany(Tag::class); } // --- Scopes (callable via ?filters={"active":1}) --- public function scopeActive($query, int $active = 1) { return $query->where('active', $active); } // --- Lifecycle Hooks --- public function afterCreate(): void { if (request()->filled('tag_ids')) { $this->tags()->sync(request()->input('tag_ids')); } } public function afterUpdate(): void { if (request()->filled('tag_ids')) { $this->tags()->sync(request()->input('tag_ids')); } } }
API Controller
namespace App\Http\Controllers\Api; use Anil\FastApiCrud\Enums\PaginationType; use Anil\FastApiCrud\Http\Controllers\BaseController; use App\Http\Requests\Post\StorePostRequest; use App\Http\Requests\Post\UpdatePostRequest; use App\Http\Resources\Post\PostResource; use App\Models\Post; class PostController extends BaseController { protected PaginationType $paginationType = PaginationType::LengthAware; protected array $with = ['user', 'tags']; protected array $withCount = ['tags']; protected array $load = ['user', 'tags', 'tags.posts']; protected array $scopes = ['active']; public function __construct() { parent::__construct( model: Post::class, storeRequest: StorePostRequest::class, updateRequest: UpdatePostRequest::class, resource: PostResource::class, ); } }
Web Controller
namespace App\Http\Controllers\Web; use Anil\FastApiCrud\Http\Controllers\BaseWebController; use App\Http\Requests\Post\StorePostRequest; use App\Http\Requests\Post\UpdatePostRequest; use App\Models\Post; class PostController extends BaseWebController { protected array $with = ['user', 'tags']; protected array $load = ['user', 'tags']; public function __construct() { parent::__construct( model: Post::class, storeRequest: StorePostRequest::class, updateRequest: UpdatePostRequest::class, viewPrefix: 'posts', routePrefix: 'posts', resourceName: 'post', collectionName: 'posts', ); } protected function storeSuccessMessage(): string { return __('Post created successfully!'); } }
License
MIT