Laravel Api Kit
#Laravel API Kit
A production-ready, API-only Laravel 13 starter kit following the 2025-2026 REST API ecosystem best practices. No frontend dependencies - purely headless API for mobile apps, SPAs, or microservices.
#Features
- API-Only - No Blade, Vite, or frontend assets
- Token Authentication - Laravel Sanctum for mobile/SPA auth
- Email Verification - Built-in email verification flow with signed URLs
- Password Reset - Secure password reset with token-based flow
- API Versioning - URI-based versioning with deprecation support via grazulex/laravel-apiroute
- Query Building - Filtering, sorting, includes via spatie/laravel-query-builder
- Data Objects - Type-safe DTOs via spatie/laravel-data
- Auto Documentation - Zero-annotation OpenAPI 3.1 via dedoc/scramble
- Modern Testing - Pest PHP with Laravel HTTP testing
- Code Quality - PHPStan (max level), Rector, and Pint with strict rules
- Rate Limiting - Configurable per-route rate limiters
- Reusable Middleware - ForceJsonResponse, LogApiRequests, EnsureEmailVerified
- Standardized Responses - Consistent JSON response format
- Optional: API Idempotency - RFC-compliant idempotency via grazulex/laravel-api-idempotency
- Optional: Smart Rate Limiting - Plan-aware throttling with quotas via grazulex/laravel-api-throttle-smart
#Requirements
- Docker & Docker Compose
- Or: PHP 8.3+, Composer 2.x
#Quick Start
#With Docker (Recommended)
# Clone the repository git clone https://github.com/grazulex/laravel-api-kit.git cd laravel-api-kit # Copy environment file cp .env.example .env # Build and start containers docker compose build docker compose up -d # Install dependencies docker compose run --rm app composer install # Generate application key docker compose run --rm app php artisan key:generate # Run migrations docker compose run --rm app php artisan migrate # Run tests to verify installation docker compose run --rm app ./vendor/bin/pest
#Without Docker
# Clone and install git clone https://github.com/grazulex/laravel-api-kit.git cd laravel-api-kit composer install # Configure cp .env.example .env php artisan key:generate # Database (SQLite by default) touch database/database.sqlite php artisan migrate # Verify ./vendor/bin/pest
#API Documentation
Once running, access the auto-generated documentation:
- Swagger UI: http://localhost:8080/docs/api
- OpenAPI JSON: http://localhost:8080/docs/api.json
#Authentication
This kit uses Laravel Sanctum with token-based authentication (ideal for mobile apps and third-party API consumers).
#Register a New User
curl -X POST http://localhost:8080/api/v1/register \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ -d '{ "name": "John Doe", "email": "john@example.com", "password": "password123", "password_confirmation": "password123" }'
Response:
{ "success": true, "message": "User registered successfully. Please check your email to verify your account.", "data": { "user": { "id": 1, "name": "John Doe", "email": "john@example.com", "email_verified_at": null, "created_at": "2025-01-15T10:30:00+00:00" }, "token": "1|abc123..." } }
#Login
curl -X POST http://localhost:8080/api/v1/login \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ -d '{ "email": "john@example.com", "password": "password123" }'
#Using the Token
Include the token in the Authorization header for protected routes:
curl -X GET http://localhost:8080/api/v1/me \ -H "Authorization: Bearer 1|abc123..." \ -H "Accept: application/json"
#Logout
curl -X POST http://localhost:8080/api/v1/logout \ -H "Authorization: Bearer 1|abc123..." \ -H "Accept: application/json"
#Email Verification
After registration, users receive a verification email. The kit integrates with Laravel's MustVerifyEmail contract.
Verify Email (via signed URL from email):
curl -X POST "http://localhost:8080/api/v1/email/verify/{id}/{hash}?signature=..." \ -H "Authorization: Bearer 1|abc123..." \ -H "Accept: application/json"
Resend Verification Email:
curl -X POST http://localhost:8080/api/v1/email/resend \ -H "Authorization: Bearer 1|abc123..." \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ -d '{"email": "john@example.com"}'
Response:
{ "success": true, "message": "Verification email sent successfully", "data": null }
#Password Reset
Request Password Reset Link:
curl -X POST http://localhost:8080/api/v1/forgot-password \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ -d '{"email": "john@example.com"}'
Response:
{ "success": true, "message": "Password reset link sent to your email", "data": null }
Reset Password (with token from email):
curl -X POST http://localhost:8080/api/v1/reset-password \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ -d '{ "email": "john@example.com", "token": "reset-token-from-email", "password": "newpassword123", "password_confirmation": "newpassword123" }'
Response:
{ "success": true, "message": "Password reset successfully", "data": null }
Note: After a successful password reset, all user tokens are revoked for security.
#API Endpoints
#Version 1 (/api/v1)
| Method | Endpoint | Auth | Description | Rate Limit |
|---|---|---|---|---|
| POST | /register | No | Register new user | 5/min |
| POST | /login | No | Get authentication token | 5/min |
| POST | /logout | Yes | Revoke current token | 120/min |
| GET | /me | Yes | Get current user profile | 120/min |
| POST | /email/verify/{id}/{hash} | Yes | Verify email address | 120/min |
| POST | /email/resend | Yes | Resend verification email | 6/min |
| POST | /forgot-password | No | Request password reset link | 6/min |
| POST | /reset-password | No | Reset password with token | 6/min |
#Response Format
All API responses follow a consistent format:
#Success Response
{ "success": true, "message": "Operation successful", "data": { // Response data here } }
#Error Response
{ "success": false, "message": "Error description", "errors": { "field": ["Validation error message"] } }
#HTTP Status Codes
| Code | Description |
|---|---|
| 200 | Success |
| 201 | Resource created |
| 204 | No content |
| 400 | Bad request |
| 401 | Unauthorized |
| 403 | Forbidden |
| 404 | Not found |
| 422 | Validation error |
| 429 | Too many requests |
| 500 | Server error |
#Project Structure
laravel-api-kit/ ├── app/ │ ├── Actions/ # Single-purpose action classes │ ├── DTOs/ # Data Transfer Objects (spatie/laravel-data) │ ├── Http/ │ │ ├── Controllers/ │ │ │ └── Api/ │ │ │ ├── ApiController.php # Base controller with ApiResponse │ │ │ └── V1/ # Version 1 controllers │ │ │ └── AuthController.php │ │ ├── Requests/ │ │ │ └── Api/V1/ # Form Requests per version │ │ │ ├── LoginRequest.php │ │ │ └── RegisterRequest.php │ │ └── Resources/ # API Resources │ │ └── UserResource.php │ ├── Models/ │ │ └── User.php # With HasApiTokens trait │ ├── Providers/ │ │ └── AppServiceProvider.php # Rate limiting config │ ├── Services/ # Business logic services │ └── Traits/ │ └── ApiResponse.php # Standardized responses ├── config/ │ ├── apiroute.php # API versioning config │ ├── cors.php # CORS settings │ ├── sanctum.php # Token auth config │ └── scramble.php # API docs config ├── routes/ │ ├── api.php # API routes entry point │ └── api/ │ └── v1.php # Version 1 routes ├── tests/ │ └── Feature/Api/V1/ │ └── AuthTest.php # Authentication tests ├── docker-compose.yml ├── Dockerfile └── CLAUDE.md # AI assistant instructions
#API Versioning
This kit uses grazulex/laravel-apiroute v2.x for API versioning with support for:
- URI Path (default):
/api/v1/users,/api/v2/users - Header:
X-API-Version: 2 - Query Parameter:
?api_version=2 - Accept Header:
Accept: application/vnd.api.v2+json
#Adding a New API Version
- Create controllers in
app/Http/Controllers/Api/V2/ - Create requests in
app/Http/Requests/Api/V2/ - Create route file
routes/api/v2.php:
<?php use App\Http\Controllers\Api\V2\AuthController; use Illuminate\Support\Facades\Route; Route::post('register', [AuthController::class, 'register']); // ... more routes
- Update
config/apiroute.php:
'versions' => [ 'v1' => [ 'routes' => base_path('routes/api/v1.php'), 'status' => 'deprecated', 'deprecated_at' => '2025-06-01', 'sunset_at' => '2025-12-01', 'successor' => 'v2', ], 'v2' => [ 'routes' => base_path('routes/api/v2.php'), 'status' => 'active', ], ],
#Deprecation Headers
When accessing deprecated versions, responses include RFC-compliant headers:
Deprecation: Sun, 01 Jun 2025 00:00:00 GMT Sunset: Mon, 01 Dec 2025 00:00:00 GMT Link: </api/v2>; rel="successor-version"
#Query Building
Use spatie/laravel-query-builder for filtering, sorting, and including relationships:
use Spatie\QueryBuilder\QueryBuilder; use Spatie\QueryBuilder\AllowedFilter; // In your controller $users = QueryBuilder::for(User::class) ->allowedFilters([ 'name', 'email', AllowedFilter::exact('id'), AllowedFilter::scope('active'), ]) ->allowedSorts(['name', 'created_at']) ->allowedIncludes(['posts', 'comments']) ->paginate(); return UserResource::collection($users);
Request examples:
GET /api/v1/users?filter[name]=john GET /api/v1/users?sort=-created_at GET /api/v1/users?include=posts,comments GET /api/v1/users?filter[name]=john&sort=name&include=posts
#Data Transfer Objects
Use spatie/laravel-data for type-safe DTOs:
// app/DTOs/UserData.php use Spatie\LaravelData\Data; class UserData extends Data { public function __construct( public string $name, public string $email, public ?string $password = null, ) {} } // In controller - validates and transforms automatically public function store(UserData $data): JsonResponse { $user = User::create($data->toArray()); return $this->created(UserResource::make($user)); }
#Rate Limiting
Configured in app/Providers/AppServiceProvider.php:
| Limiter | Limit | Use Case |
|---|---|---|
api |
60/min | Default for all API routes |
auth |
5/min | Login/register (brute force protection) |
authenticated |
120/min | Logged-in users |
#Applying Rate Limiters
// In routes/api.php Route::middleware('throttle:auth')->group(function () { Route::post('login', [AuthController::class, 'login']); Route::post('register', [AuthController::class, 'register']); }); Route::middleware(['auth:sanctum', 'throttle:authenticated'])->group(function () { // Protected routes with higher limits });
#Rate Limit Headers
Responses include rate limit information:
X-RateLimit-Limit: 60 X-RateLimit-Remaining: 59 Retry-After: 60 # When limit exceeded
#Optional Packages
The following packages are suggested (not required) and can be installed individually to extend the kit's capabilities. They are fully opt-in and will not affect existing behavior.
#API Idempotency
grazulex/laravel-api-idempotency provides RFC-compliant idempotency for your API endpoints. It prevents duplicate operations when clients retry requests (critical for payments, order creation, etc.).
Install:
composer require grazulex/laravel-api-idempotency
Publish config (optional):
php artisan vendor:publish --tag="api-idempotency-config"
Usage — apply the middleware to mutation routes:
// routes/api/v1.php Route::middleware(['auth:sanctum', 'throttle:authenticated'])->group(function () { Route::post('orders', [OrderController::class, 'store']) ->middleware('idempotent'); Route::post('payments', [PaymentController::class, 'store']) ->middleware('idempotent:required'); // Require Idempotency-Key header });
Client-side — include the Idempotency-Key header:
curl -X POST http://localhost:8080/api/v1/orders \ -H "Authorization: Bearer 1|abc123..." \ -H "Idempotency-Key: order_unique_key_123" \ -H "Content-Type: application/json" \ -d '{"product_id": 1, "quantity": 2}'
Attention:
- Only apply the
idempotentmiddleware to mutation routes (POST, PUT, PATCH). GET requests are naturally idempotent.- The default storage driver is
cache. For production with multiple servers, use theredisordatabasedriver.- Keys are scoped per user by default. Two different users can use the same key without conflict.
#Smart Rate Limiting
grazulex/laravel-api-throttle-smart provides plan-aware rate limiting with quotas, multiple algorithms (fixed window, sliding window, token bucket), and multi-tenant support. Ideal for SaaS APIs with subscription tiers.
Install:
composer require grazulex/laravel-api-throttle-smart
Publish config:
php artisan vendor:publish --tag="throttle-smart-config"
Usage — apply to routes where plan-based limiting is needed:
// routes/api/v1.php Route::middleware(['auth:sanctum', 'throttle.smart'])->group(function () { Route::apiResource('posts', PostController::class); });
Attention:
- This package coexists with Laravel's built-in
throttle:middleware. You do not need to remove the existing rate limiters.- If you want to replace the native throttle on specific routes, swap
throttle:authenticatedwiththrottle.smarton those routes only.- Do not apply both
throttle:authenticatedandthrottle.smarton the same route group — choose one per group to avoid double rate limiting.- The default driver is
cache. For production,redisis recommended for performance and distributed consistency.- Configure your subscription plans in
config/throttle-smart.phpto match your business model (Free, Pro, Enterprise, etc.).
#Middleware
The kit includes three production-ready middleware patterns that you can apply to your routes as needed.
#Available Middleware
| Alias | Class | Description |
|---|---|---|
force.json |
ForceJsonResponse |
Ensures all responses are JSON formatted |
log.api |
LogApiRequests |
Logs API requests with timing information |
verified |
EnsureEmailVerified |
Requires verified email to access route |
#ForceJsonResponse
Automatically sets Accept: application/json header and converts non-JSON responses to JSON format.
Route::middleware('force.json')->group(function () { // All responses will be JSON });
#LogApiRequests
Logs API requests with detailed information and adds X-Response-Time header to responses.
Logged data: timestamp, method, URL, IP, user ID, status code, duration (ms), user agent.
Enable logging via environment:
APP_LOG_API_REQUESTS=true
Route::middleware('log.api')->group(function () { // Requests will be logged });
#EnsureEmailVerified
Protects routes that require a verified email address. Returns 403 if email is not verified.
Route::middleware(['auth:sanctum', 'verified'])->group(function () { // Only users with verified emails can access });
Response when email not verified:
{ "success": false, "message": "Your email address is not verified. Please verify your email to continue." }
#Testing
This kit uses Pest PHP for testing:
# Run all tests docker compose run --rm app ./vendor/bin/pest # Run specific test file docker compose run --rm app ./vendor/bin/pest tests/Feature/Api/V1/AuthTest.php # Run with coverage docker compose run --rm app ./vendor/bin/pest --coverage # Run in parallel docker compose run --rm app ./vendor/bin/pest --parallel
#Writing Tests
// tests/Feature/Api/V1/UserTest.php use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; uses(RefreshDatabase::class); it('lists users for authenticated user', function () { $user = User::factory()->create(); $token = $user->createToken('test')->plainTextToken; User::factory()->count(5)->create(); $response = $this->withHeader('Authorization', "Bearer {$token}") ->getJson('/api/v1/users'); $response->assertStatus(200) ->assertJsonStructure([ 'success', 'data' => [ '*' => ['id', 'name', 'email'] ] ]); }); it('requires authentication', function () { $this->getJson('/api/v1/users') ->assertStatus(401); });
#Code Quality
This kit includes strict code quality tools configured following nunomaduro/laravel-starter-kit standards.
#Tools
| Tool | Purpose | Config |
|---|---|---|
| PHPStan + Larastan | Static analysis (level max) | phpstan.neon |
| Rector | Automated refactoring | rector.php |
| Pint | Code style (strict rules) | pint.json |
#Composer Scripts
# Apply all fixes (Rector + Pint) composer lint # Check without fixing (CI mode) composer test:lint # Static analysis only composer test:types # Unit tests only composer test:unit # Full test suite (lint + types + unit) composer test
#With Docker
docker compose exec app composer lint docker compose exec app composer test
#Strict Rules Applied
declare(strict_types=1)on all filesfinalclasses by default- Type declarations enforced
- Dead code removal
- Early returns
- Strict comparisons
#GitHub Actions
Tests run automatically on push/PR to main via .github/workflows/tests.yml.
#Development Commands
# List all routes docker compose run --rm app php artisan route:list # Clear all caches docker compose run --rm app php artisan optimize:clear # Generate IDE helper files (if using Laravel IDE Helper) docker compose run --rm app php artisan ide-helper:generate docker compose run --rm app php artisan ide-helper:models -N # Export OpenAPI spec to file docker compose run --rm app php artisan scramble:export
#Environment Configuration
Key .env variables:
# Application APP_NAME="Laravel API Kit" APP_ENV=local APP_DEBUG=true APP_URL=http://localhost:8080 # Database (SQLite for development) DB_CONNECTION=sqlite DB_DATABASE=/var/www/database/database.sqlite # For MySQL/PostgreSQL # DB_CONNECTION=mysql # DB_HOST=mysql # DB_PORT=3306 # DB_DATABASE=laravel_api_kit # DB_USERNAME=laravel # DB_PASSWORD=secret # Sanctum SANCTUM_STATEFUL_DOMAINS=localhost,localhost:3000,127.0.0.1 # API Versioning API_VERSION_STRATEGY=uri API_DEFAULT_VERSION=latest # Rate Limiting API_RATE_LIMIT=60 # Documentation API_DOCS_URL=http://localhost:8080/docs/api
#Deployment
#Production Checklist
- Set
APP_ENV=productionandAPP_DEBUG=false - Configure proper database (MySQL/PostgreSQL)
- Set
APP_URLto your production URL - Configure
SANCTUM_STATEFUL_DOMAINSfor your frontend domains - Review and tighten CORS settings in
config/cors.php - Set up proper rate limiting for production load
- Configure caching (Redis recommended)
- Set up queue worker for background jobs
- Enable HTTPS and update URLs
#Docker Production
# Example production Dockerfile additions FROM php:8.3-fpm-alpine # Install opcache for performance RUN docker-php-ext-install opcache # Production PHP settings COPY docker/php/opcache.ini /usr/local/etc/php/conf.d/ COPY docker/php/php.ini /usr/local/etc/php/conf.d/
#Extending the Kit
#Adding a New Resource (CRUD Example)
- Create Model & Migration:
docker compose run --rm app php artisan make:model Post -m
- Create Controller:
// app/Http/Controllers/Api/V1/PostController.php namespace App\Http\Controllers\Api\V1; use App\Http\Controllers\Api\ApiController; use App\Http\Resources\PostResource; use App\Models\Post; use Spatie\QueryBuilder\QueryBuilder; class PostController extends ApiController { public function index() { $posts = QueryBuilder::for(Post::class) ->allowedFilters(['title', 'status']) ->allowedSorts(['title', 'created_at']) ->allowedIncludes(['author', 'comments']) ->paginate(); return $this->success(PostResource::collection($posts)); } public function show(Post $post) { return $this->success(new PostResource($post)); } // ... store, update, destroy methods }
- Create Resource:
// app/Http/Resources/PostResource.php namespace App\Http\Resources; use Illuminate\Http\Resources\Json\JsonResource; class PostResource extends JsonResource { public function toArray($request): array { return [ 'id' => $this->id, 'title' => $this->title, 'content' => $this->content, 'author' => new UserResource($this->whenLoaded('author')), 'created_at' => $this->created_at?->toIso8601String(), ]; } }
- Add Routes:
// routes/api/v1.php Route::middleware('auth:sanctum')->group(function () { // ... existing routes Route::apiResource('posts', PostController::class); });
- Create Tests:
// tests/Feature/Api/V1/PostTest.php uses(RefreshDatabase::class); it('lists posts', function () { $user = User::factory()->create(); Post::factory()->count(3)->create(); $this->actingAs($user) ->getJson('/api/v1/posts') ->assertStatus(200) ->assertJsonCount(3, 'data'); });
#Contributing
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
#License
This project is open-sourced software licensed under the MIT license.
#Credits
- Laravel - The PHP Framework
- Laravel Sanctum - API Token Authentication
- grazulex/laravel-apiroute - API Versioning
- spatie/laravel-query-builder - Query Building
- spatie/laravel-data - Data Transfer Objects
- dedoc/scramble - API Documentation
- grazulex/laravel-api-idempotency - API Idempotency (optional)
- grazulex/laravel-api-throttle-smart - Smart Rate Limiting (optional)
- Pest PHP - Testing Framework