joy/voyager-api
Joy/Voyager API is a Laravel package that adds RESTful endpoints to Voyager’s admin panel, exposing BREAD/CRUD resources as JSON APIs for SPAs and external clients. Configure models and query Voyager-managed data with minimal setup.
joy/voyager-api layers REST endpoints and Swagger docs on top of Voyager's BREAD system. It's not a standalone package — you need Voyager (and its prerequisite Laravel app) already in place, plus the joy-voyager family it expects.
Minimal path:
composer require joy/voyager-api — verify the constraint resolves against your Laravel version; the last release is Feb 2024, so Laravel 10/11 is the safe zone.Joy\VoyagerApi\VoyagerApiServiceProvider):
php artisan vendor:publish --provider="Joy\VoyagerApi\VoyagerApiServiceProvider"
This yields a config/joy-voyager-api.php (or similar) that controls middleware, routing prefix, pagination limits, and which BREADs are exposed.darkaonline/l5-swagger installed and its config published — check composer.json dependencies in the repo before assuming it's transitive.posts table is the easiest smoke test), and enable the API toggles (list/show/create/update/delete).php artisan route:list | grep api.GET /api/posts for JSON, and /api/documentation for the Swagger UI.First use case: expose an existing Voyager-managed table (e.g. posts) as a read-only JSON resource, then progressively enable write verbs. The mental model is "BREAD == API contract" — if it isn't a BREAD, it isn't an endpoint.
BREAD is your API definition language. Every controller, route, request validation rule, and Swagger annotation is derived from the BREAD record. Day-to-day, this means your workflow is editor-driven: add a column → run migrations → refresh BREAD → hit /api/documentation to see the new field appear. Treat the BREAD editor as the source of truth for the API contract, not the migration.
Custom controllers when BREAD isn't enough. The BREAD editor lets you specify a controller class per resource. The common pattern is to extend the package's base controller (to inherit filtering, sorting, pagination, response shaping) and override index, store, update, destroy, or the validation rules. This is where you put business logic that doesn't fit generic CRUD — computed fields, event dispatching, authorization calls beyond middleware.
Conventional query parameters. Expect the usual Laravel-ish surface:
GET /api/posts?filter[status]=published&sort=-created_at&per_page=25&page=2
GET /api/posts/1?include=author,comments
The filter, sort, include, per_page, and search conventions are what most Joy packages follow; verify against the generated Swagger page for exact syntax, since parameter naming is the most likely thing to drift between versions.
Authentication via middleware, not the package. The config typically exposes a middleware array applied to the API route group. Standard patterns:
['api', 'auth:sanctum'].['api'] only for public read endpoints — split routes into public vs. protected groups using Route::group after publishing the routes file.Voyager's role/permission system is not automatically enforced by the API layer in most versions of this family; add it explicitly via middleware or policy checks in a custom controller.
Customizing Swagger output. Use the L5-Swagger flow — annotate your custom controller classes with @OA\ blocks, then php artisan l5-swagger:generate. For fields already covered by BREAD, rely on the package's auto-annotations; for computed/derived fields on a custom controller, add manual annotations or you'll get undocumented response shapes.
Soft deletes and media. DELETE /api/posts/1 respects the model's SoftDeletes trait if present (returns a soft-delete). Voyager media fields surface as URLs/arrays depending on multiple — verify the serialization shape in Swagger before shipping a client.
Versioning. Publish the routes file and wrap the group in Route::prefix('api/v1') rather than relying on the default prefix. Given the small surface and rapid breaking changes from schema edits, version pinning is cheap insurance.
Zero-contract-churn hazard. Adding a column in Voyager and refreshing BREAD silently changes your public API. There's no deprecation window. Mitigation: snapshot the generated OpenAPI JSON in CI and diff it — any unreviewed schema change fails the build.
Caching is the #1 "why isn't my change showing up?" cause. Voyager caches BREAD definitions and permissions; the API layer reads from that cache. After editing BREAD or config, run:
php artisan config:clear
php artisan cache:clear
php artisan l5-swagger:generate
If endpoints still 404, the BREAD record itself is likely missing an API flag — the package won't synthesize routes from the schema alone.
Middleware defaults are the biggest security footgun. The default middleware is often ['api'] with no auth — meaning every BREAD you mark as "API visible" becomes publicly readable. Audit config/joy-voyager-api.php on first install, before deploying, and again before any schema change goes live.
Primary key and route binding assumptions. The package will assume the BREAD's key field is the route segment. UUIDs, composite keys, or slugs need explicit configuration or a custom findRoute/controller override. Symptoms are 404s on valid rows or id type errors from the database.
Validation lives in BREAD, and it's easy to break. Validation rules set in the BREAD editor are applied to store/update requests. If a rule doesn't match the database type (e.g. integer rule on a string column), you get a 422 that's hard to trace unless you know where to look. Prefer form-request overrides in a custom controller for nontrivial rules; you get proper stack traces and testability.
Swagger docs go stale unless you regenerate them. The OpenAPI spec is a build artifact. Add php artisan l5-swagger:generate to your deploy script — otherwise the "live" docs reflect a schema the running app no longer has.
Small-community reality. 19 stars means roughly zero Stack Overflow coverage, and the README is likely incomplete. Budget time to read the source in vendor/joy/voyager-api/. When something doesn't work, the answer is almost always in the service provider (route registration), the base controller (query semantics), or the BREAD model (which has slightly different columns in each Voyager major version).
Version pinning is non-negotiable. This package sits on top of Voyager, which itself lags behind Laravel releases. Pin joy/voyager-api, joy-voyager, and tcg/voyager together in composer.json, and add an integration test that hits one endpoint per resource — it's the cheapest way to catch upstream drift.
Testing needs BREAD seeders. Your feature tests won't see the API unless BREAD records exist. Seed them in RefreshDatabase setup with the API flags enabled; otherwise every test route returns 404 and you'll waste an afternoon on the wrong layer.
Extension points when you outgrow the package. Two clean exits: (1) custom controller per resource — still uses the package's routing/docs but gives you full control of the handler; (2) Route::apiResource beside the package's routes for the handful of endpoints that need real business logic, leaving the package to handle passive CRUD. Don't try to patch vendor code — publish the routes file and the config, keep the rest untouched.
How can I help you explore Laravel packages today?