hendabastian / laracrud-apigen
Generate complete CRUD APIs for Laravel with Repository pattern, Spatie QueryBuilder, JSON:API pagination, and DTOs.
Requires
- php: ^8.2
- illuminate/console: ^11.0|^12.0
- illuminate/filesystem: ^11.0|^12.0
- illuminate/support: ^11.0|^12.0
- spatie/laravel-data: ^4.0
- spatie/laravel-json-api-paginate: ^2.0
- spatie/laravel-query-builder: ^6.0
This package is auto-updated.
Last update: 2026-04-27 02:46:40 UTC
README
Generate complete CRUD APIs for Laravel with a single Artisan command.
Features
- API Controller with full CRUD operations
- Form Request validation classes (Create & Update)
- API Resource for JSON transformation
- Repository pattern (Interface + Implementation) with listing logic in the repository layer
- DTO generation using Spatie Laravel Data
- Spatie Query Builder integration (filters, sorts, includes) — encapsulated in the repository
- JSON:API pagination via Spatie JSON API Paginate (config-driven parameter names and defaults)
- OpenAPI documentation attributes driven by
json-api-paginateconfig (supportspage[number]/page[size]and cursor pagination) - Automatic route registration
- Automatic service provider binding
- Smart column type detection (MySQL, PostgreSQL, SQLite, SQL Server) with automatic
Carboncasting for date/datetime/timestamp columns
Requirements
- PHP 8.2+
- Laravel 11 or 12
Optional (recommended)
spatie/laravel-query-builder— for query filtering/sortingspatie/laravel-json-api-paginate— for JSON:API paginationspatie/laravel-data— for DTO generation
Installation
composer require hendabastian/laracrud-apigen --dev
Usage
php artisan generate:crud-api {ModelName}
Examples
# Generate CRUD for User model php artisan generate:crud-api User # Generate CRUD for a multi-word model php artisan generate:crud-api JobApplication # Overwrite existing files php artisan generate:crud-api User --force # Skip repository pattern php artisan generate:crud-api User --skip-repository # Skip DTO generation php artisan generate:crud-api User --skip-dto # Skip route registration php artisan generate:crud-api User --skip-routes
Generated Files
For php artisan generate:crud-api Company:
| Component | Path |
|---|---|
| Controller | app/Http/Controllers/Api/CompanyController.php |
| Create Request | app/Http/Requests/Api/Company/CompanyCreateRequest.php |
| Update Request | app/Http/Requests/Api/Company/CompanyUpdateRequest.php |
| Resource | app/Http/Resources/Api/CompanyResource.php |
| Repository Interface | app/Repositories/Contracts/CompanyRepositoryInterface.php |
| Repository | app/Repositories/CompanyRepository.php |
| DTO | app/DTO/CompanyDTO.php |
| Routes | routes/api.php (appended) |
Generated API Endpoints
GET /api/companies — List (with filters, sorts, pagination)
POST /api/companies — Create
GET /api/companies/{id} — Show
PUT /api/companies/{id} — Update
DELETE /api/companies/{id} — Delete
Configuration
Publish the config file:
php artisan vendor:publish --tag=crud-generator-config
Available options in config/crud-generator.php:
return [ 'model_namespace' => 'App\\Models', 'controller_namespace' => 'App\\Http\\Controllers\\Api', 'request_namespace' => 'App\\Http\\Requests\\Api', 'resource_namespace' => 'App\\Http\\Resources\\Api', 'repository_namespace' => 'App\\Repositories', 'dto_namespace' => 'App\\DTO', 'route_file' => 'routes/api.php', 'service_provider' => 'app/Providers/AppServiceProvider.php', 'excluded_columns' => ['id', 'created_at', 'updated_at', ...], 'use_query_builder' => true, 'use_json_api_paginate' => true, 'use_spatie_data' => true, ];
Customizing Business Logic
The generated code is yours to modify. The repository pattern keeps all data-access logic (including listing with filters, sorts, and pagination) in the repository layer, keeping controllers thin.
Generated repository structure
The generated repository includes a list() method that encapsulates query builder logic:
// app/Repositories/CompanyRepository.php (generated) <?php namespace App\Repositories; use App\Models\Company; use App\Repositories\Contracts\CompanyRepositoryInterface; use Spatie\QueryBuilder\QueryBuilder; class CompanyRepository extends BaseRepository implements CompanyRepositoryInterface { public function __construct(Company $model) { parent::__construct($model); } public function list(): mixed { return QueryBuilder::for($this->model->query()) ->allowedFilters(['name', 'email', ...]) ->allowedSorts(['name', 'email', ..., 'created_at', 'updated_at']) ->allowedIncludes([...]) ->jsonPaginate(); } }
The controller's index() simply delegates to the repository:
public function index(): AnonymousResourceCollection { $companies = $this->repository->list(); return CompanyResource::collection($companies); }
Overriding the list method
Customize filtering, sorting, or add scopes by overriding list():
// app/Repositories/OrderRepository.php public function list(): mixed { return QueryBuilder::for($this->model->query()) ->allowedFilters([ AllowedFilter::exact('status'), AllowedFilter::scope('date_range'), ]) ->allowedSorts(['total', 'created_at']) ->allowedIncludes(['customer', 'items']) ->where('archived', false) // always exclude archived ->jsonPaginate(); }
Adding custom methods to a repository
First, define the method in the interface:
// app/Repositories/Contracts/OrderRepositoryInterface.php <?php namespace App\Repositories\Contracts; use Illuminate\Database\Eloquent\Collection; interface OrderRepositoryInterface extends BaseRepositoryInterface { public function list(): mixed; public function findByStatus(string $status): Collection; public function cancelExpiredOrders(): int; }
Then implement it in the repository:
// app/Repositories/OrderRepository.php <?php namespace App\Repositories; use App\Models\Order; use App\Repositories\Contracts\OrderRepositoryInterface; use Illuminate\Database\Eloquent\Collection; class OrderRepository extends BaseRepository implements OrderRepositoryInterface { public function __construct(Order $model) { parent::__construct($model); } public function findByStatus(string $status): Collection { return $this->model->where('status', $status)->get(); } public function cancelExpiredOrders(): int { return $this->model ->where('status', 'pending') ->where('expires_at', '<', now()) ->update(['status' => 'cancelled']); } }
Overriding CRUD behavior in a repository
Override any base method to add custom logic:
// app/Repositories/UserRepository.php <?php namespace App\Repositories; use App\Models\User; use App\Repositories\Contracts\UserRepositoryInterface; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Facades\Hash; class UserRepository extends BaseRepository implements UserRepositoryInterface { public function __construct(User $model) { parent::__construct($model); } public function create(array $data): Model { if (isset($data['password'])) { $data['password'] = Hash::make($data['password']); } return parent::create($data); } public function update(int $id, array $data): Model { if (isset($data['password'])) { $data['password'] = Hash::make($data['password']); } return parent::update($id, $data); } }
Adding custom controller actions
Add new methods to the generated controller and register the routes manually:
// app/Http/Controllers/Api/OrderController.php // Add this method to the generated controller: public function cancel(Order $order): JsonResponse { if ($order->status === 'shipped') { return response()->json(['message' => 'Cannot cancel a shipped order.'], 422); } $this->repository->update($order->id, ['status' => 'cancelled']); return response()->json(['message' => 'Order cancelled successfully.']); }
// routes/api.php Route::patch('orders/{order}/cancel', [\App\Http\Controllers\Api\OrderController::class, 'cancel']);
Customizing validation rules
Edit the generated request classes to add conditional or complex rules:
// app/Http/Requests/Api/Order/OrderCreateRequest.php public function rules(): array { return [ 'customer_id' => ['required', 'integer', 'exists:customers,id'], 'total' => ['required', 'numeric', 'min:0.01'], 'status' => ['required', 'string', 'in:pending,confirmed,shipped'], 'notes' => ['nullable', 'string', 'max:1000'], 'items' => ['required', 'array', 'min:1'], 'items.*.product_id' => ['required', 'integer', 'exists:products,id'], 'items.*.quantity' => ['required', 'integer', 'min:1'], ]; }
Customizing the API Resource
Add computed fields, conditional relationships, or hide fields:
// app/Http/Resources/Api/OrderResource.php public function toArray(Request $request): array { return [ 'id' => $this->id, 'customer_id'=> $this->customer_id, 'total' => number_format($this->total, 2), 'status' => $this->status, 'is_editable'=> in_array($this->status, ['pending', 'confirmed']), 'customer' => new CustomerResource($this->whenLoaded('customer')), 'items' => OrderItemResource::collection($this->whenLoaded('items')), 'created_at' => $this->created_at, 'updated_at' => $this->updated_at, ]; }
JSON:API Pagination
When use_json_api_paginate is enabled, the generated OpenAPI documentation attributes automatically read from your config/json-api-paginate.php configuration:
// config/json-api-paginate.php (published via spatie/laravel-json-api-paginate) return [ 'max_results' => 30, 'default_size' => 30, 'number_parameter' => 'number', 'size_parameter' => 'size', 'cursor_parameter' => 'cursor', 'pagination_parameter' => 'page', 'use_cursor_pagination'=> false, ];
The generator uses these values to produce the correct Scramble #[QueryParameter] attributes:
page[number]andpage[size]by default (JSON:API spec compliant)page[cursor]andpage[size]whenuse_cursor_paginationis enabled- Parameter names and example values adapt if you customize the config
When use_json_api_paginate is disabled, the generator falls back to standard Laravel page and per_page parameters.
Prerequisites
Make sure your bootstrap/app.php includes the API routes:
->withRouting( web: __DIR__.'/../routes/web.php', api: __DIR__.'/../routes/api.php', commands: __DIR__.'/../routes/console.php', )
License
MIT