backpack/crud
Backpack for Laravel is a powerful CRUD package for building admin panels quickly. Create, read, update, and delete resources with minimal code, customize fields, filters, and widgets, and manage users and permissions through a clean, flexible interface.
A developer should treat Backpack CRUD as a framework for building admin panels, not a pre-built admin. The first hour should be spent proving one simple model end-to-end: list, create, update, delete, validation, and access control.
Minimal setup:
composer require backpack/crud
php artisan backpack:install
php artisan migrate
php artisan storage:link
Then create an admin user if the installer did not already do so, and log in at the configured admin path, usually /admin.
Where to look first:
config/backpack/base.php — route prefix, admin guard, middleware, branding.config/backpack/crud.php — default CRUD behaviour, pagination, field/column defaults.routes/backpack/custom.php — where CRUD routes are registered.app/Http/Controllers/Admin/ — generated CRUD controllers.app/Http/Requests/ — generated form requests for create/update validation.resources/views/vendor/backpack/ — only if you need to override UI.First use case: a simple internal admin CRUD for a Product model.
php artisan backpack:crud Product
Wire the route:
// routes/backpack/custom.php
Route::crud('product', \App\Http\Controllers\Admin\ProductCrudController::class);
The generated controller usually looks like this:
class ProductCrudController extends CrudController
{
use \Backpack\CRUD\app\Http\Controllers\Operations\ListOperation;
use \Backpack\CRUD\app\Http\Controllers\Operations\CreateOperation;
use \Backpack\CRUD\app\Http\Controllers\Operations\UpdateOperation;
use \Backpack\CRUD\app\Http\Controllers\Operations\DeleteOperation;
use \Backpack\CRUD\app\Http\Controllers\Operations\ShowOperation;
public function setup()
{
CRUD::setModel(\App\Models\Product::class);
CRUD::setRoute(config('backpack.base.route_prefix').'/product');
CRUD::setEntityName('product');
}
protected function setupListOperation()
{
CRUD::column('name');
CRUD::column('price');
CRUD::column('created_at');
}
protected function setupCreateOperation()
{
CRUD::field('name')->type('text');
CRUD::field('price')->type('number');
CRUD::field('description')->type('textarea');
}
protected function setupUpdateOperation()
{
$this->setupCreateOperation();
}
}
Once that works, add the next layer deliberately: a filter, a relationship field, a file upload, a permission rule, and a custom column. That sequence exposes most of Backpack’s day-to-day surface area without starting with a complex domain.
Use setup() for shared model/route/entity configuration, and keep setupListOperation(), setupCreateOperation(), setupUpdateOperation(), and setupShowOperation() focused on UI configuration. Avoid putting business logic in these methods. Validation belongs in FormRequests; domain logic belongs in services, actions, or model methods.
A common pattern is to define the create form once and reuse it for update:
protected function setupCreateOperation()
{
CRUD::field('name')->type('text');
CRUD::field('slug')->type('text')->hint('Leave blank to auto-generate');
CRUD::field('price')->type('number');
CRUD::field('status')->type('select')->values([
'draft' => 'Draft',
'published' => 'Published',
]);
}
protected function setupUpdateOperation()
{
$this->setupCreateOperation();
}
Override individual fields only where the update form genuinely differs.
List screens are where Backpack saves the most time: columns, search, filters, pagination, and row buttons. Use column types instead of raw HTML where possible:
CRUD::column('name');
CRUD::column('price')->type('number')->prefix('$');
CRUD::column('status')->type('select')->values([
'draft' => 'Draft',
'published' => 'Published',
]);
CRUD::column('created_at')->type('datetime');
For computed values, use a closure:
CRUD::column('display_name')
->label('Name')
->type('text')
->value(fn ($entry) => $entry->first_name.' '.$entry->last_name);
Add filters for the fields support teams actually search by:
CRUD::filter('status')
->type('select')
->values(['draft' => 'Draft', 'published' => 'Published'])
->whenActive(function ($value) {
$this->crud->addClause('where', 'status', $value);
});
Eager-load relationships used in list columns to avoid N+1 queries:
$this->crud->with(['category', 'author']);
Expose only the columns needed for triage. A 30-column list is slower and harder to use than a focused list plus a details page.
For belongsTo, a select field is usually enough:
CRUD::field('category_id')
->type('select')
->entity('category')
->attribute('name')
->model(\App\Models\Category::class);
For belongsToMany, use select2_multiple:
CRUD::field('tags')
->type('select2_multiple')
->entity('tags')
->attribute('name')
->model(\App\Models\Tag::class)
->pivot(true);
For large relationship sets, use AJAX-backed selects or FetchOperation endpoints rather than loading every record into the page. For hasMany, use relationship fields or inline create flows only when the child record is genuinely simple; otherwise link to a separate CRUD.
Use upload or upload_multiple, not a plain file input, and validate through the FormRequest:
CRUD::field('image')->type('upload')->label('Image')->disk('public');
CRUD::field('documents')->type('upload_multiple')->disk('public');
In the request, call withFiles() so Backpack processes the upload correctly:
protected function withFiles()
{
return ['image', 'documents'];
}
Display uploads with the appropriate column type:
CRUD::column('image')->type('image');
Keep disk configuration, file size limits, MIME validation, and cleanup rules explicit. Uploads are one of the most common sources of silent admin-panel bugs.
When the action is not create/read/update/delete, add a custom operation or button rather than hiding logic inside a list closure. Typical examples: publish, approve, export, sync, or send invoice. Backpack’s operation classes, routes, and button configuration keep these actions discoverable and testable.
Use FetchOperation for AJAX data endpoints, especially dependent selects and large typeaheads. Use save actions for cross-cutting form behaviour such as slug generation, audit logging, or recalculation.
Backpack does not automatically give you a secure admin panel. Choose one approach and apply it consistently:
allowAccess() / denyAccess() logic in the controller.For example:
public function setup()
{
parent::setup();
if (! backpack_user()->can('manage products')) {
$this->crud->denyAccess(['list', 'create', 'update', 'delete']);
}
}
Also hide fields and buttons the user cannot use. Hiding a button is not authorization; the route and request must still be protected.
Use Backpack’s test generators where available:
php artisan backpack:tests
At minimum, test:
Use model factories and avoid relying on production-like seed data.
category will not automatically work if the relationship method is categoryRelation or the foreign key is category_id.name, entity, attribute, and model. The most common mistake is mixing the foreign key column with the relationship method. For belongsTo, the field name is often category_id, while entity is category.nullable or sometimes. Always use a FormRequest rather than inline validation.storage:link, withFiles(), MIME/size validation, and whether the column expects a path or URL. Test uploads in the same environment where they will run.php artisan route:list, inspect routes/backpack/custom.php, clear config/view caches, and check the admin guard. If assets or menus are missing, rerun the installer.$this->crud while developing. Temporarily dump the CRUD configuration in a setup method to understand which fields, columns, and operations are active. Remove the dump before committing.config/backpack/base.php controls the admin route prefix and guard. If login works but CRUD routes redirect or 403, start there.How can I help you explore Laravel packages today?