Opinionated Laravel API starter kit for token-based authentication with strong defaults around API design, security, localization, documentation, and testing.
- PHP
8.5+ Laravel13 - SQLite-first local development (
DB_CONNECTION=sqlite) - Sanctum personal access token authentication with least-privilege abilities and expiration
- Versioned API routes with no
/apiprefix (/v1/...) - JSON:API entity responses (
application/vnd.api+json) with token metadata - RFC 9457 Problem Details error responses (RFC 7807 successor)
- Invokable controllers, Form Request validation + DTO payloads, Action classes
- Full auth flow: register, login, logout, current user, token management, email verification, password reset
- API localization via
Accept-Language+Content-Language(English, Spanish, Brazilian Portuguese) - Scribe attribute-based API docs + OpenAPI generation
- OpenAPI contract tests to keep docs and runtime behavior in sync
- Sunset middleware to deprecate and retire endpoints safely
- GitHub Actions for CI tests and daily dependency update PRs
- Laravel Framework:
^13 - PHP:
^8.5 - Auth:
laravel/sanctum - Docs/OpenAPI:
knuckleswtf/scribe(attributes, not docblocks) - Test Runner: Pest + Laravel test tooling
- Static Analysis / Quality: PHPStan (Larastan, level 10), Pint, Rector
composer installcp .env.example .env
php artisan key:generate
touch database/database.sqlite
php artisan migrateOr run the bundled setup script:
composer run setupcomposer run devAPI base path is versioned and has no global /api prefix:
http://127.0.0.1:8000/v1/...
Routing is intentionally split:
routes/api/routes.php— top-level entry pointroutes/api/v1/auth.php— one file per resource, versioned
Framework routing is configured with apiPrefix: '' in bootstrap/app.php, so URLs stay clean.
| Method | Path | Auth | Purpose |
|---|---|---|---|
| POST | /v1/auth/register |
No | Register and issue token |
| POST | /v1/auth/login |
No | Login and issue token |
| GET | /v1/auth/me |
Bearer auth:me |
Current authenticated user |
| POST | /v1/auth/logout |
Bearer auth:logout |
Revoke current token |
| GET | /v1/auth/tokens |
Bearer auth:tokens:read |
List personal access tokens |
| DELETE | /v1/auth/tokens |
Bearer auth:tokens:delete |
Revoke all tokens |
| DELETE | /v1/auth/tokens/{token_id} |
Bearer auth:tokens:delete |
Revoke one token |
| POST | /v1/auth/email/verification-notification |
Bearer auth:verification:send |
Send/resend verification email |
| GET | /v1/auth/email/verify/{id}/{hash} |
Signed URL | Verify email |
| POST | /v1/auth/password/forgot |
No | Request reset email (anti-enumeration) |
| GET | /v1/auth/password/reset/{token} |
No | Return reset payload for API clients |
| POST | /v1/auth/password/reset |
No | Reset password |
Register:
curl -X POST http://127.0.0.1:8000/v1/auth/register \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"name": "Jane Doe",
"email": "jane@example.com",
"password": "Password123!",
"password_confirmation": "Password123!",
"device_name": "cli"
}'Login:
curl -X POST http://127.0.0.1:8000/v1/auth/login \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"email": "jane@example.com",
"password": "Password123!",
"device_name": "cli"
}'Use token on protected route:
curl http://127.0.0.1:8000/v1/auth/me \
-H "Accept: application/json" \
-H "Authorization: Bearer <TOKEN>"Localized response (Brazilian Portuguese):
curl -X POST http://127.0.0.1:8000/v1/auth/password/forgot \
-H "Accept: application/json" \
-H "Accept-Language: pt-BR" \
-H "Content-Type: application/json" \
-d '{"email":"unknown@example.com"}'Entities are returned using Laravel's JsonApiResource (application/vnd.api+json) with a top-level meta block carrying the issued token:
{
"data": {
"id": "01HZX3W3T4J8Q57XNZD5BPHJ92",
"type": "users",
"attributes": {
"name": "Jane Doe",
"email": "jane@example.com",
"email_verified_at": null,
"created_at": "2026-01-01T00:00:00+00:00",
"updated_at": "2026-01-01T00:00:00+00:00"
}
},
"meta": {
"token": "1|example-token",
"token_type": "Bearer",
"expires_at": "2026-01-01T02:00:00+00:00"
}
}Every error is a ProblemResponse (Content-Type: application/problem+json):
{
"type": "https://example.com/problems/validation-error",
"title": "Validation Error",
"status": 422,
"detail": "The given data was invalid.",
"errors": {
"email": ["The email field is required."]
}
}The global exception handler in bootstrap/app.php normalizes validation, auth, authorization, rate-limit, signature, not-found, and server errors — never an HTML page.
Tokens are issued with least-privilege abilities from config/sanctum.php (abilities.default):
auth:me, auth:logout, auth:verification:send, auth:tokens:read, auth:tokens:delete
Routes enforce them via the abilities: middleware. No token carries a wildcard * by default.
SANCTUM_EXPIRATION (default 120 minutes) controls token lifetime. The expires_at value is returned in the response meta.
Register/login accept a device_name used as the token name, making the token list human-readable.
Locale resolution is API-first:
- Middleware reads
Accept-Language - Locale is resolved against
APP_SUPPORTED_LOCALES - Response includes
Content-Language - Unsupported locales fall back to
APP_FALLBACK_LOCALE
Supported out of the box: en, es, pt_BR.
Relevant config/env:
APP_LOCALEAPP_FALLBACK_LOCALEAPP_SUPPORTED_LOCALES(default:en,es,pt_BR)SANCTUM_EXPIRATION(default:120minutes)
- ULID primary keys for users
- Password hashing via model casts
- Email verification required model contract (
MustVerifyEmail) - Rate limits configured in
AppServiceProvider:auth-register: 10/minute per IPauth-login: 10/minute per IP + emailauth-password: 5/minute per IP + emailauth-protected: 60/minute per authenticated user
- Verification endpoints use signed URLs and throttling
- Write endpoints enforce JSON payloads (
application/json) →415otherwise - API responses include baseline hardening headers (
nosniff,DENY,no-referrer) - API responses include an
X-Request-Idheader (propagated or generated) - Security-sensitive auth/token actions emit structured
security.auditlog events - Critical write endpoints support
Idempotency-Keyreplay/conflict handling - Configurable transport hardening for HTTPS enforcement, HSTS, trusted proxies/hosts, and strict CORS origins
- Production boot checks fail fast on unsafe config (
APP_DEBUG, HTTPS, CORS wildcard, trusted hosts)
App\Http\Middleware\Sunset adds deprecation metadata and can enforce retirement.
Route::middleware('sunset:2030-01-01,https://api.example.com/v2/auth/login,true')
->post('/v1/auth/login', LoginController::class);Behavior:
- Adds
DeprecationandSunsetheaders - Adds
Link: <...>; rel="successor-version"when successor URL is valid - Can return
410 Goneafter sunset date when enforcement is enabled
Scribe is configured for this no-prefix API shape:
- Route matching uses
v1/*prefixes (config/scribe.php) - Endpoints are documented via PHP attributes
- OpenAPI output is generated to
public/docs/openapi.yaml
Generate docs/spec:
php artisan scribe:generate --no-interactionGenerated artifacts:
public/docs/index.htmlpublic/docs/openapi.yamlpublic/docs/collection.json
composer test # Pest suite (parallel)
composer lint # Pint (check only)
composer pint # Pint (apply)
composer stan # PHPStan level 10 (Larastan)Feature tests cover:
- Token/auth flows (register, login, logout, me, token management)
- Email verification and password reset workflows
- Abilities enforcement (403 on missing scope)
- Idempotency replay/conflict
- Localization behavior (en/es/pt_BR)
- Sunset middleware behavior
- Security hardening (headers, media type, HTTPS/HSTS, request id)
- OpenAPI generation and contract verification
GitHub Actions workflows:
.github/workflows/ci-tests.yml— runs tests on every push and pull request.github/workflows/dependency-updates.yml— dailycomposer updatePR.github/workflows/security-gate.yml— Composer security audit + Gitleaks secret scan
app/
Actions/Auth/ ← business logic (register, login, tokens, ...)
Http/
Controllers/Api/V1/Auth/ ← single-action invokable controllers
Middleware/ ← AttachRequestId, SetRequestLocale, ...
Payloads/Auth/ ← DTO payloads
Requests/Auth/V1/ ← Form Requests with payload()
Resources/ ← JSON:API resources
Responses/ProblemResponse.php ← RFC 9457
Models/ ← User (ULID, MustVerifyEmail)
Support/ ← SecurityAudit, ProductionSecurityChecks
routes/
api/
routes.php
v1/auth.php
lang/
en/
es/
pt_BR/
tests/
Feature/
config/
sanctum.php
security.php
scribe.php
.github/
workflows/
MIT