A production-ready, API-only Laravel 12 starter kit following the 2024-2025 REST API ecosystem best practices. No frontend dependencies - purely headless API for mobile apps, SPAs, or microservices.
- API-Only - No Blade, Vite, or frontend assets
- Token Authentication - Laravel Sanctum for mobile/SPA auth
- 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
- Rate Limiting - Configurable per-route rate limiters
- Standardized Responses - Consistent JSON response format
- Docker & Docker Compose
- Or: PHP 8.3+, Composer 2.x
# 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# 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/pestOnce running, access the auto-generated documentation:
- Swagger UI: http://localhost:8080/docs/api
- OpenAPI JSON: http://localhost:8080/docs/api.json
This kit uses Laravel Sanctum with token-based authentication (ideal for mobile apps and third-party API consumers).
curl -X POST http://localhost:8080/api/v1/register \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"name": "John Doe",
"email": "[email protected]",
"password": "password123",
"password_confirmation": "password123"
}'Response:
{
"success": true,
"message": "User registered successfully",
"data": {
"user": {
"id": 1,
"name": "John Doe",
"email": "[email protected]",
"created_at": "2025-01-15T10:30:00+00:00"
},
"token": "1|abc123..."
}
}curl -X POST http://localhost:8080/api/v1/login \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"email": "[email protected]",
"password": "password123"
}'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"curl -X POST http://localhost:8080/api/v1/logout \
-H "Authorization: Bearer 1|abc123..." \
-H "Accept: application/json"| 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 | 60/min |
| GET | /me | Yes | Get current user profile | 60/min |
All API responses follow a consistent format:
{
"success": true,
"message": "Operation successful",
"data": {
// Response data here
}
}{
"success": false,
"message": "Error description",
"errors": {
"field": ["Validation error message"]
}
}| 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 |
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
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
- 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',
],
],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"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
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));
}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 |
// 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
});Responses include rate limit information:
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 59
Retry-After: 60 # When limit exceededThis 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// 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 formatting (Laravel Pint)
docker compose run --rm app ./vendor/bin/pint
# Check code style without fixing
docker compose run --rm app ./vendor/bin/pint --test
# 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:exportKey .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- 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
# 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/- 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');
});- 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
This project is open-sourced software licensed under the MIT license.
- 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
- Pest PHP - Testing Framework