specialtactics/laravel-api-boilerplate
A Laravel API boilerplate that gives you a clean, ready-to-use foundation for JSON APIs. It wires up common API essentials like authentication, authorization, validation, and consistent responses, so you can skip repetitive setup and start building features.
This is a project template, not a runtime dependency — the fastest path is to clone it or scaffold from it rather than composer require it into an existing app.
git clone https://github.com/specialtactics/laravel-api-boilerplate my-api
cd my-api && rm -rf .git
composer install
cp .env.example .env
php artisan key:generate
# configure DB in .env, then:
php artisan migrate --seed
php artisan serve # or ./vendor/bin/sail up if using the bundled Docker setup
Where to look first, in this order:
routes/api.php — the versioned route groups (/api/v1/...) and auth middleware wiring.app/Http/Controllers/Api/V1/ — controller conventions, how responses are returned.app/Transformers/ — the Fractal transformers that shape every JSON payload.app/Exceptions/Handler.php — centralized JSON error rendering (validation, 404, 500, auth).app/Providers/ApiServiceProvider.php or ResponseMacroServiceProvider.php) — this is what gives you the consistent response envelope.tests/Feature/ — copy an existing test as your scaffolding pattern for new endpoints.First use case — prove the auth + response loop works end-to-end:
# register
curl -X POST http://localhost:8000/api/v1/auth/register \
-H 'Content-Type: application/json' \
-d '{"name":"Dev","email":"[email protected]","password":"secret123","password_confirmation":"secret123"}'
# login → token
curl -X POST http://localhost:8000/api/v1/auth/login \
-H 'Content-Type: application/json' \
-d '{"email":"[email protected]","password":"secret123"}'
# authenticated call
curl http://localhost:8000/api/v1/me -H "Authorization: Bearer <token>"
Once you see a consistent { "data": ... } (or project-specific envelope) from all three calls and a clean JSON validation error, you understand the boilerplate's contract. Everything else is repetition of that pattern.
The boilerplate rewards a rigid, repeatable slice for every new API resource. Do these five steps in order:
php artisan make:model Post -m # migration + model
php artisan make:controller Api/V1/PostController --api
php artisan make:request StorePostRequest
# hand-create app/Transformers/PostTransformer.php
$fillable/$casts.ValidationException into a standardized 422.v1 group in routes/api.php, behind auth:sanctum (or auth:api on Passport-based versions) unless it's truly public.Do not return response()->json([...]) ad hoc — you'll break client expectations and your own tests. Use the macro/helper the boilerplate exposes (names vary by version, commonly respondWithItem, respondWithCollection, or $this->response->item(...)):
public function show(Post $post)
{
return $this->respondWithItem($post, new PostTransformer());
}
public function index()
{
return $this->respondWithCollection(
Post::paginate(20),
new PostTransformer()
);
}
This is the pattern that makes pagination meta, errors, and success responses mutually consistent. If a new endpoint needs a shape the macros don't provide, add a macro — don't bypass it.
Login/register controllers issue a token (Sanctum personal access token or Passport token depending on version). In tests and clients, treat the token as opaque and always send Authorization: Bearer <token> plus Accept: application/json.
// Feature test pattern
$user = User::factory()->create();
$token = $user->createToken('test')->plainTextToken;
$this->withHeader('Authorization', "Bearer {$token}")
->getJson('/api/v1/posts')
->assertOk()
->assertJsonStructure(['data' => [['id', 'title']]]);
The Accept: application/json header is what triggers the JSON exception path; without it you'll get HTML error pages and confusing test failures.
Transformers are cheap; N+1 queries are not. Whenever a transformer references a relationship ($post->author->name), eager-load it in the controller:
Post::with(['author', 'tags'])->paginate(20);
Use Fractal's ?include=author,tags query param only where you've explicitly wired $availableIncludes and default-included the safe ones. Treat include as a public API surface — adding one is a backwards-compatible change, removing one is not.
Treat routes/api.php's v1 group as frozen once clients ship. New breaking behavior goes in v2 with its own controllers/transformers directory. Shared logic goes in app/Services or model methods, so v1 and v2 controllers can both call it.
The boilerplate's Handler maps Laravel exceptions to a stable error schema. Extend it by adding a render() branch for your domain exception classes rather than throwing generic \Exception with a message — clients will depend on the HTTP code and error code field.
composer update that brings in upstream fixes. Fork it mentally on day one; cherry-picking upstream changes later is manual and will conflict. Pin your Laravel version deliberately.config/app.php provider registration and any bootstrap/providers.php (Laravel 11+) equivalents.php artisan passport:install plus key files in storage/. Newer tags use Sanctum and don't. Read the version tag's README before assuming which middleware alias (auth:api vs auth:sanctum) is correct.Accept: application/json is the #1 cause of "my API returns HTML" and of 404s leaking HTML in tests. Set it globally in your test base class ($this->withHeader('Accept', 'application/json')) and in every HTTP client.Accept header is present. Verify this once per resource — it's easy to ship a resource that returns a clean error for validation failures but HTML for a missing ID.config/cors.php must include every header your client sends (Authorization, Content-Type, Accept, plus any custom X- headers) and the right paths (api/*). New clients failing preflight almost always means a missing header here, not an auth bug.RateLimiter::for('api', ...)). The default is usually 60/min keyed by user or IP. If you add heavy endpoints, define a dedicated limiter name and attach it per-route; don't raise the global limit.422 and 401/403 responses early. If they differ, normalize in the handler before clients are built against them — this is a cheap fix now and a breaking change later.RefreshDatabase + factories. withoutExceptionHandling() is useful locally for stack traces but masks whether your error envelope is correct — keep at least one test per endpoint that asserts the JSON error shape (e.g., assertJsonValidationErrors).APP_DEBUG=true in production leaks stack traces past your carefully designed error schema. Assert APP_DEBUG=false in your deploy checklist; the boilerplate's handler is good, but debug mode bypasses it in places.render() branches (for domain errors), and app/Services (for anything longer than ~15 lines in a controller). Avoid editing the boilerplate's internal traits unless you're prepared to reapply that change on every future version bump.How can I help you explore Laravel packages today?