opscale-co/nova-api
A Laravel Nova package that adds a customizable REST API layer for your Nova resources, with authentication, filtering, sorting, pagination, and relationship support for exposing Nova-managed data to external apps.
1. Install and publish config
composer require opscale-co/nova-api
php artisan vendor:publish --provider="Opscale\NovaApi\NovaApiServiceProvider"
If the provider name differs, run php artisan vendor:publish with no arguments and pick the nova-api entry from the list — that also tells you the exact config tag for future upgrades.
2. Read the config file first, in this order
The published config is the whole API surface of this package. Look at:
api/nova, api/v1/admin, or a separate subdomain. Changing this later means changing every client.throttle: and your own token-scoping middleware here.3. Confirm the Nova guard resolves a user
The package reuses Nova policies, which means the authenticated principal must be a user Nova can authorize against. Before writing any client code, prove that a token-authenticated request lands on a user whose policies actually run. A 403 on a resource you know is permitted usually means the guard is resolving null or the wrong model, not that the package is broken.
4. First use case: one read-only resource
Pick a low-risk, read-mostly resource — Ticket, Order, Invoice. Expose only index and show in config, point a token at it, and hit it:
curl -H "Authorization: Bearer $TOKEN" \
"https://app.test/api/nova/tickets?page=1&perPage=25&search=acme"
Validate three things in that one response before going further:
fieldsForIndex / fieldsForDetail (not fields()).Once that round-trips cleanly, store/update on the same resource is a small increment; actions are the real second milestone.
Treat the config's resource list as an allow-list and grow it deliberately. The tempting shortcut — expose everything and rely on policies — means the shape of every Nova resource in the app becomes reachable HTTP surface, including ones with computed fields that do expensive work or leak derived data. Expose per resource, per verb.
The package's value proposition is that Nova's field, filter, and policy metadata are the API definition. Lean into that rather than fighting it:
OrderPolicy, where Nova staff already benefit from it. Note the method-name mapping — viewAny gates index, view gates show, create gates store, update gates update, delete gates destroy.fields(). Use onlyOnIndex / onlyOnDetail and hideFromIndex / hideFromDetail to shape list vs. detail payloads. This is the intended lever for payload shaping — reach for it before reaching for custom controllers.A single token type with full Nova access is fine for an internal service and wrong for a partner. Insert a middleware after auth that narrows what the token can touch:
// config/nova-api.php (shape varies — check the published file)
'middleware' => [
'api',
'auth:nova-api',
\App\Http\Middleware\ScopeNovaApiToken::class,
'throttle:60,1',
],
Have that middleware resolve the token's allowed resource list and reject mismatches with 403 before the policy layer runs. This keeps the config allow-list as the global ceiling and the token as the per-client ceiling — two independent gates, both auditable.
v1.3.0 bumps opscale-co/actions to ^4.0, which confirms action invocation is a first-class part of this package, not an afterthought. The pattern that pays off:
handle(), so both paths are covered by one implementation.This is the single biggest "weeks not quarters" win in the package — it's the difference between "we built an API" and "we exposed a workflow." Do a regression pass over every action endpoint when you bump the dependency, and read the upstream changelog for opscale-co/actions before you do.
throttle in front. Rate limiting isn't the package's job. Nothing here will stop a partner's retry loop./api/v1/nova/... costs nothing today and buys you a migration path when a Nova field rename silently changes your response shape.$this->withToken($token)->getJson(...) catches guard misconfiguration that a direct controller test never will.assertJsonStructure) for both index and show. This is your only early warning when someone edits fields() for a UI reason and breaks a partner integration.BelongsTo, HasMany, and BelongsToMany fields behave differently in an API context than in the Nova UI, where the front-end does extra work. Expect IDs or stubbed objects where you wanted nested data, and resolveUsing closures that assume $this->resource is fully loaded. Decide explicitly per relationship: nested payload (via a computed field or a resource-level resolver) or ID + separate lookup endpoint. Don't discover this in production.
A Text::make('Lifetime Value', fn () => $this->calculateLtv()) that's tolerable on a 25-row Nova page becomes a latency cliff on a paginated API hit by a client polling every 30 seconds. Anything with a query inside or an external call should be onlyOnDetail or excluded from the API entirely.
If your app serves assets from a non-default disk or behind a CDN/signed URLs, field URLs may come back wrong or unsigned in API responses. Check one response per file-backed resource in a non-local environment, not just on your machine.
403 vs 404 will cost you an afternoonNova's authorization habits mean a missing policy method can surface as a 403 rather than a 404, and a misconfigured guard surfaces as a 403 on everything. When debugging, work outward: is there a user? does the policy method exist? does the policy return true for this user? Assert on that chain with Nova::actingAs() in a test before blaming the package.
Nova fields carry rules, but also creationRules and updateRules. Confirm which set store and update actually apply — if the package leans on the field's base rules, your unique-email-ignore-self constraint on update may not fire. Test an update that should fail and make sure it does.
Routes here are config-driven, so they participate in php artisan route:cache like any others. Changing the exposure list or prefix in production requires a route cache rebuild — and if you're mid-deploy with config:cache on, a stale cached config is a very plausible explanation for "the new endpoint 404s."
If your app already uses auth:sanctum or auth:api globally on the api group, stacking the package's token middleware on top can produce confusing "authenticated but unauthorized" states. Verify which guard actually wins, and keep the package's auth config to exactly one guard.
searchable?search= only queries columns listed in the resource's $searchable array. If a client says search "doesn't work" on a field, it's almost always a missing $searchable entry, not a package bug. Same class of issue for filters: a filter that works in the Nova UI may expect an argument shape the query string can't express cleanly — document the exact param shape per filter for your clients.
onlyOnIndex, resolveUsing, computed fields. Still declarative, still one source of truth.This is a low-star package whose most recent release is a single-line transitive major bump (opscale-co/actions to ^4.0) with no migration notes. For something sitting in your request path:
composer.json rather than floating, and read the diff before bumping.How can I help you explore Laravel packages today?