joselfonseca/laravel-api
A Laravel package for building JSON APIs with reusable controllers, resources, response helpers, and consistent error handling. It reduces boilerplate and helps keep API endpoints structured and predictable.
This is a project skeleton, not a composer require-able library. You consume it by generating a fresh app from it, so the first decision is whether the tag's framework line is one you can live with.
As of V12.0.0 the skeleton targets Laravel 9 (bumped from the Laravel 8 line in 11.x) and therefore requires PHP 8.0+. That's the substantive change in this release — there are no new features, no new endpoints, and no changes to the Dingo/Fractal/JWT architecture. The repo remains archived and unmaintained; V12 is a compatibility bump, not a revival. Laravel 9 itself is already past its security-fix window, so this is still an architecture reference first and a foundation second.
Minimal path:
laravel/framework / php constraints in composer.json before composer create-project — the install fails hard on a mismatch, and V12 raises the floor to PHP 8.0.composer create-project joselfonseca/laravel-api my-api V12.0.0 — pin the tag explicitly rather than dev-master, and pin V12.0.0 specifically if you're on PHP 8.0+ (11.x is Laravel 8 / PHP 7.3+)..env from .env.example, set DB credentials, then php artisan key:generate.tymon/jwt-auth, and without it every login/token call throws a "secret not set"-class error: php artisan jwt:secret.php artisan migrate --seed, then php artisan serve (or the bundled Docker setup if present).php artisan api:routes, not route:list — routes are registered through Dingo, and Laravel's own lister still won't show them on Laravel 9 either.Where to look first, in this order:
routes/api.php or app/Api/routes.php) — a $api->version('v1', function ($api) { ... }) block that defines prefix, middleware, and throttling.config/api.php — prefix, version, standardsTree, subtype, and the error-format settings. These, not your routes, determine the final URL shape.Exception handler — this is where JWT issuing and API error responses live.app/Transformers and app/Http/Requests — the response-shaping and validation layers you'll extend daily.First use case: exercise the full stack end-to-end before writing any code. Seed a user, POST to the login endpoint, copy the token out of the JSON, and call a protected endpoint with Authorization: Bearer <token> plus Accept: application/json. That single loop validates routing, versioning, JWT config, middleware, transformer output, and exception formatting all at once. Endpoint paths drift between tags, so confirm them from api:routes rather than trusting the README.
The core unit of work is a four-file vertical slice. For any new resource, the intended flow is: a FormRequest for validation → a controller method → a Fractal transformer → a response built with Dingo's $this->response->item(...) / ->collection(...) / ->paginator(...) via the Dingo\Api\Routing\Helpers trait. Keep controllers thin; anything past validation and response assembly belongs in a service or repository, otherwise the slice becomes the whole app.
Versioning is per-group, not per-file. Add $api->version('v2', [...]) alongside the v1 block and give it its own namespace and its own transformers. Never mutate a shipped transformer to add fields for a new client — that's a silent breaking change for v1 consumers. Copy the transformer, extend it, and let the version group pick which one to resolve.
Transformers carry the relationship contract. Use $defaultIncludes for cheap, always-present relations and includeX() methods for everything else, so clients opt in with ?include=comments,author. This is the single biggest ergonomic win in the kit and the part most worth preserving if you later migrate off Fractal.
Auth pattern: login returns a JWT, the client stores it, protected routes sit behind the Dingo auth middleware, and a refresh endpoint issues a new token from a still-valid one. Read the authenticated user through the API guard (auth('api')->user()), not the default guard — mixing them is the most common source of confusing "user is null but I'm authenticated" bugs.
Throttling and errors belong at the group level. Apply the throttle middleware to the $api->version() group so new routes inherit it, and standardize error output with Dingo's $this->response->errorNotFound() / errorUnprocessableEntity() helpers rather than ad-hoc response()->json(['error' => ...]). Consistent error envelopes matter more for APIs than for web apps, and the kit's whole value proposition is that consistency.
Testing: always set Accept: application/json and any version headers in test requests. Dingo's exception handling and content negotiation key off those headers, so a test that passes in the browser can fail under $this->json() if the headers differ.
Upgrading an existing 11.x app to V12: treat it as a hard-cut major. Because the bump moves Laravel 8 → 9 and drags the transitive stack (Symfony Mailer replacing SwiftMailer, tymon/jwt-auth 2.x, Dingo API's Laravel 9 branch) forward with it, budget for the framework's own 8→9 upgrade guide alongside this skeleton's changes. Diff composer.json and config/api.php between the two tags first — they tell you more about what you'll need to reconcile than the release notes do.
routes/api.php route groups, and API Resources. If you're inheriting an app built on it, budget for a migration rather than a rewrite.composer.json change when reviewing the tag.--ignore-platform-reqs will "work" and then fail at runtime in ways that are far harder to debug than a clean install error.php artisan config:clear after any .env edit if config is cached. The Dingo settings (API_PREFIX, API_DEBUG, API_STRICT, API_STANDARDS_TREE) live in env, and stale caches produce routes that look "missing" or prefixes that look wrong.API_PREFIX=api and a prefix on the version group yields /api/v1/v1/.... Set the prefix in exactly one place.Accept header governs everything. Without Accept: application/json, Dingo falls back to HTML error pages and debugging gets surreal. Set API_DEBUG=true locally, and be aware that API_STRICT changes whether a missing version throws or is inferred from the Accept header — behavior differs between environments if that env var drifts.JWT_BLACKLIST_ENABLED and the storage driver before you claim sessions are revocable.route:list lies. Use api:routes — on Laravel 9 as much as on 8. Likewise, route caching and Dingo don't always compose cleanly, and Laravel 9's route-cache changes make this worth re-testing on V12 rather than assuming 11.x behaviour carried over. If routes vanish after route:cache, that's the cause.How can I help you explore Laravel packages today?