opanegro/nova-custom-controller
A Laravel Nova package that lets you attach custom controllers to Nova resources, offering flexibility beyond Nova's default CRUD handling. Useful for custom actions, workflows, and tailored admin behavior while keeping Nova's familiar UI.
opanegro/nova-custom-controller exists to solve one narrow problem: Nova encourages you to build pages and tools as Vue SPAs, but sometimes you just need a plain Laravel controller behind Nova's auth — a CSV export, a webhook receiver, a bulk action endpoint, a file download. This package gives you a supported way to register those controllers inside Nova's route/middleware stack instead of hand-rolling it.
Minimal steps:
composer require opanegro/nova-custom-controllerbootstrap/providers.php on Laravel 11+, or config/app.php on older versions). If not, add Opanegro\NovaCustomController\...ServiceProvider manually.vendor/opanegro/nova-custom-controller/src/ to see exactly which routes and middleware the provider registers. The package is thin enough to audit in five minutes, which matters given its age.App\Http\Controllers\Controller, add the AuthorizesRequests trait, and point the package's registration config at it.Nova.request() (or a plain <a href> for downloads).First use case: an "Export CSV" button on a resource index. You add a controller action that streams User::all() as CSV, register it through the package, and call it from a custom button or a Nova action's handle() via redirect. That is the 80% case for this package.
Route-per-action, not resource controllers. Nova endpoints are usually verbs against a resource collection, not CRUD. Keep controllers small and single-purpose (ExportUsersController, ImportOrdersController) and register one route each. It reads better in route:list under the nova-vendor/ prefix.
Return JSON for SPA consumption. Anything a Vue component calls should return response()->json(...) and be fetched with Nova.request().get('/nova-vendor/opanegro/...'). Nova.request() already carries the XSRF token and Nova's axios config, so POSTs work without extra setup — provided the route sits in the web middleware group.
Downloads and streams. For exports use response()->streamDownload(fn () => ..., 'users.csv') and hit the URL with a normal anchor tag; don't route it through Nova.request(), which tries to parse the body.
Authorization inside the controller. The package puts you behind Nova's auth middleware, but that only proves someone is logged in. Add explicit policy checks:
public function __invoke(Request $request)
{
$this->authorize('viewAny', User::class);
// ...
}
Extract business logic into a service. If the same export is triggered from a Nova Action and from a custom button, both should call UserExporter — the controller becomes a thin HTTP adapter. This also makes the logic testable without Nova booted.
Testing. Use $this->actingAs($admin, config('nova.guard')) in feature tests; testing against the default guard will 302-redirect and confuse you.
This package predates Nova 4. Last release was December 2019, which puts it firmly in the Nova 2/3 era. Nova 4 moved the SPA to Inertia and changed route prefixes (/nova-api), middleware groups, and the request/response contract. Verify against your Nova version before installing; on Nova 4+ you will likely need to fork it or just register the routes yourself. The entire package is essentially:
Route::middleware(['nova'])
->prefix('nova-vendor/opanegro/custom')
->group(function () {
Route::get('/export', ExportController::class);
});
If the package fights you, copy that pattern into your own provider. You lose nothing.
Auth guard mismatches. If your users authenticate on a non-default guard, Nova routes will 401/redirect. Check config('nova.guard') and ensure the nova middleware group is picking it up. This is the number-one cause of "route exists but I get bounced to login."
CSRF on POSTs. Routes registered outside the web group skip VerifyCsrfToken, so Nova.request() POSTs will fail with 419. Confirm the package includes web in the middleware stack — if not, add it in your own route registration.
Route caching. If the package's config accepts closures or dynamic route definitions, php artisan route:cache will fail in production. Use controller class strings only.
Don't expose these routes without the Nova middleware. A controller registered as a plain web route is a public endpoint with only your authorize() call standing between an attacker and your data. Audit route:list --path=nova after every change.
Debugging: php artisan route:list --path=nova-vendor is faster than reading source. Then Log::info() at the top of the controller to confirm the request even arrives — middleware rejection produces silent redirects that look like routing bugs.
Maintenance risk. 14 stars, single author, four-plus years stale. Treat it as a reference implementation rather than a dependency you can rely on across Nova upgrades. Pinning it is fine; building a long-term feature on its API is not.
Extension points are minimal. There's no event system or plugin surface here — the provider registers routes and gets out of the way. Any customization happens in your controller and your route registrations, which is the right place for it anyway.
How can I help you explore Laravel packages today?