This commit resolves the version mismatch for 'grazulex/laravel-apiroute' by regenerating the lock file. Additionally, it upgrades 'pestphp/pest' and 'pestphp/pest-plugin-laravel' to v4 to ensure compatibility with Laravel 12 and PHPUnit 12, fixing dependency conflicts. The lock file was out of sync with composer.json, causing installation failures. Upgrading to Pest v4 is necessary as Pest v3 conflicts with PHPUnit 12, which is required by Laravel 12.
Laravel API Kit
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.
Features
- 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
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",
"data": {
"user": {
"id": 1,
"name": "John Doe",
"email": "john@example.com",
"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"
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 | 60/min |
| GET | /me | Yes | Get current user profile | 60/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 with versioning
├── 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 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/ - Update
routes/api.php:
use Grazulex\ApiRoute\Facades\ApiRoute;
// Version 2 - New current version
ApiRoute::version('v2', function () {
Route::post('register', [V2\AuthController::class, 'register']);
// ... more routes
})->current();
// Version 1 - Mark as deprecated
ApiRoute::version('v1', function () {
Route::post('register', [V1\AuthController::class, 'register']);
// ... existing routes
})->deprecated('2025-06-01')->sunset('2025-12-01');
Deprecation Headers
When accessing deprecated versions, responses include RFC-compliant headers:
Deprecation: @1717200000
Sunset: Sun, 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
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);
});
Development Commands
# 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: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.php
ApiRoute::version('v1', function () {
// ... existing routes
Route::middleware('auth:sanctum')->group(function () {
Route::apiResource('posts', PostController::class);
});
})->current();
- 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
- Pest PHP - Testing Framework