Advanced patterns

API Integration Patterns

This guide covers RESTful API patterns with Laravolt v7, including authentication, resources, and exposing Thunderclap-generated CRUD as APIs.

Overview

Laravolt applications can expose APIs for:

  • Mobile applications
  • Third-party integrations
  • Microservices communication
  • Headless CMS scenarios
  • Webhook consumers

Key Principles:

  • Use Laravel Sanctum for authentication
  • Transform data with API Resources
  • Version your APIs (/api/v1/...)
  • Rate limit to prevent abuse
  • Document with OpenAPI/Swagger

Authentication with Sanctum

Step 1: Install and Configure Sanctum

Bash
# Install Sanctum
composer require laravel/sanctum
# Publish config
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
# Run migrations
php artisan migrate

Step 2: Configure API Authentication

PHP
// config/sanctum.php
return [
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
'%s%s',
'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
env('APP_URL') ? ','.parse_url(env('APP_URL'), PHP_URL_HOST) : ''
))),
'guard' => ['web'],
'expiration' => null, // Tokens never expire (or set minutes)
'middleware' => [
'verify_csrf_token' => App\Http\Middleware\VerifyCsrfToken::class,
'encrypt_cookies' => App\Http\Middleware\EncryptCookies::class,
],
];

Step 3: Add Sanctum to User Model

PHP
<?php
namespace App\Models;
use Laravel\Sanctum\HasApiTokens;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable
{
use HasApiTokens;
// ... existing code
}

Step 4: Create Token Issuance Endpoint

PHP
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use App\Models\User;
class AuthController extends Controller
{
public function login(Request $request)
{
$request->validate([
'email' => 'required|email',
'password' => 'required',
'device_name' => 'required',
]);
$user = User::where('email', $request->email)->first();
if (!$user || !Hash::check($request->password, $user->password)) {
return response()->json([
'message' => 'Invalid credentials',
], 401);
}
$token = $user->createToken($request->device_name)->plainTextToken;
return response()->json([
'token' => $token,
'user' => $user,
]);
}
public function logout(Request $request)
{
$request->user()->currentAccessToken()->delete();
return response()->json([
'message' => 'Logged out successfully',
]);
}
public function user(Request $request)
{
return response()->json($request->user());
}
}

Step 5: Define API Routes

PHP
// routes/api.php
use App\Http\Controllers\Api\AuthController;
Route::post('/login', [AuthController::class, 'login']);
Route::middleware('auth:sanctum')->group(function () {
Route::post('/logout', [AuthController::class, 'logout']);
Route::get('/user', [AuthController::class, 'user']);
});

Step 6: Test Authentication

Bash
# Login and get token
curl -X POST http://localhost:8000/api/login \
-H "Content-Type: application/json" \
-d '{
"email": "admin@example.com",
"password": "password",
"device_name": "mobile-app"
}'
# Response:
# {
# "token": "1|abc123...",
# "user": { ... }
# }
# Use token for authenticated requests
curl http://localhost:8000/api/user \
-H "Authorization: Bearer 1|abc123..."

API Resources and Transformers

Step 1: Generate API Resource

Bash
php artisan make:resource ProductResource
php artisan make:resource ProductCollection

Step 2: Define Resource Transformation

PHP
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class ProductResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'sku' => $this->sku,
'price' => [
'amount' => $this->price,
'currency' => 'USD',
'formatted' => '$' . number_format($this->price, 2),
],
'description' => $this->description,
'stock' => $this->stock_quantity,
'category' => new CategoryResource($this->whenLoaded('category')),
'images' => ImageResource::collection($this->whenLoaded('images')),
'created_at' => $this->created_at->toIso8601String(),
'updated_at' => $this->updated_at->toIso8601String(),
'links' => [
'self' => route('api.products.show', $this->id),
'category' => route('api.categories.show', $this->category_id),
],
];
}
}

Step 3: Define Collection Resource

PHP
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
class ProductCollection extends ResourceCollection
{
public function toArray(Request $request): array
{
return [
'data' => $this->collection,
'meta' => [
'total' => $this->total(),
'per_page' => $this->perPage(),
'current_page' => $this->currentPage(),
'last_page' => $this->lastPage(),
],
'links' => [
'first' => $this->url(1),
'last' => $this->url($this->lastPage()),
'prev' => $this->previousPageUrl(),
'next' => $this->nextPageUrl(),
],
];
}
}

Exposing Thunderclap CRUD as API

Step 1: Generate Thunderclap Resources

Bash
# Generate full CRUD with API support
php artisan thunderclap:generate Product \
--fields="name:string,sku:string:unique,price:decimal,description:text" \
--api

This generates:

  • app/Domains/Product/Models/Product.php
  • app/Domains/Product/Http/Controllers/ProductController.php
  • app/Domains/Product/Http/Controllers/Api/ProductApiController.php
  • app/Domains/Product/Http/Resources/ProductResource.php
  • app/Domains/Product/Http/Requests/StoreProductRequest.php
  • app/Domains/Product/Http/Requests/UpdateProductRequest.php

Step 2: API Controller (Generated)

PHP
<?php
namespace App\Domains\Product\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Domains\Product\Models\Product;
use App\Domains\Product\Http\Requests\StoreProductRequest;
use App\Domains\Product\Http\Requests\UpdateProductRequest;
use App\Domains\Product\Http\Resources\ProductResource;
use App\Domains\Product\Http\Resources\ProductCollection;
class ProductApiController extends Controller
{
public function index()
{
$products = Product::with(['category', 'images'])
->paginate(15);
return new ProductCollection($products);
}
public function store(StoreProductRequest $request)
{
$product = Product::create($request->validated());
return new ProductResource($product);
}
public function show(Product $product)
{
$product->load(['category', 'images']);
return new ProductResource($product);
}
public function update(UpdateProductRequest $request, Product $product)
{
$product->update($request->validated());
return new ProductResource($product);
}
public function destroy(Product $product)
{
$product->delete();
return response()->json(null, 204);
}
}

Step 3: Define API Routes

PHP
// routes/api.php
use App\Domains\Product\Http\Controllers\Api\ProductApiController;
Route::middleware('auth:sanctum')->group(function () {
Route::apiResource('products', ProductApiController::class);
});

Step 4: Test API Endpoints

Bash
# List products (paginated)
curl http://localhost:8000/api/products \
-H "Authorization: Bearer {token}"
# Create product
curl -X POST http://localhost:8000/api/products \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{
"name": "Laptop",
"sku": "LAP-001",
"price": 999.99,
"description": "High-performance laptop"
}'
# Get single product
curl http://localhost:8000/api/products/1 \
-H "Authorization: Bearer {token}"
# Update product
curl -X PUT http://localhost:8000/api/products/1 \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{
"name": "Gaming Laptop",
"price": 1299.99
}'
# Delete product
curl -X DELETE http://localhost:8000/api/products/1 \
-H "Authorization: Bearer {token}"

Rate Limiting

Step 1: Configure Rate Limits

PHP
// app/Providers/RouteServiceProvider.php
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Facades\RateLimiter;
protected function configureRateLimiting()
{
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});
RateLimiter::for('api-strict', function (Request $request) {
return Limit::perMinute(10)->by($request->user()?->id ?: $request->ip());
});
}

Step 2: Apply Rate Limiting

PHP
// routes/api.php
Route::middleware(['auth:sanctum', 'throttle:api'])->group(function () {
Route::apiResource('products', ProductApiController::class);
});
// Stricter limits for expensive operations
Route::middleware(['auth:sanctum', 'throttle:api-strict'])->group(function () {
Route::post('/products/bulk-import', [ProductApiController::class, 'bulkImport']);
});

Step 3: Handle Rate Limit Exceeded

PHP
// app/Exceptions/Handler.php
use Illuminate\Http\Exceptions\ThrottleRequestsException;
public function render($request, Throwable $exception)
{
if ($exception instanceof ThrottleRequestsException) {
return response()->json([
'message' => 'Too many requests. Please try again later.',
'retry_after' => $exception->getHeaders()['Retry-After'] ?? 60,
], 429);
}
return parent::render($request, $exception);
}

API Versioning

PHP
// routes/api.php
Route::prefix('v1')->group(function () {
Route::apiResource('products', ProductApiController::class);
});
Route::prefix('v2')->group(function () {
Route::apiResource('products', ProductV2ApiController::class);
});

Strategy 2: Header Versioning

PHP
// app/Http/Middleware/ApiVersion.php
public function handle(Request $request, Closure $next)
{
$version = $request->header('Accept-Version', 'v1');
config(['api.version' => $version]);
return $next($request);
}

Complete Example: Product API with Laravolt

Generate Resources

Bash
# Generate Thunderclap CRUD with API
php artisan thunderclap:generate Product \
--fields="name:string,sku:string:unique,price:decimal,stock_quantity:integer,description:text" \
--relationships="belongsTo:Category" \
--api \
--resource

API Routes

PHP
// routes/api.php
use App\Domains\Product\Http\Controllers\Api\ProductApiController;
Route::post('/login', [AuthController::class, 'login']);
Route::middleware('auth:sanctum')->prefix('v1')->group(function () {
Route::apiResource('products', ProductApiController::class);
// Custom endpoints
Route::get('/products/{product}/stock', [ProductApiController::class, 'checkStock']);
Route::post('/products/{product}/restock', [ProductApiController::class, 'restock']);
});

Custom API Methods

PHP
// app/Domains/Product/Http/Controllers/Api/ProductApiController.php
public function checkStock(Product $product)
{
return response()->json([
'sku' => $product->sku,
'stock_quantity' => $product->stock_quantity,
'in_stock' => $product->stock_quantity > 0,
'low_stock' => $product->stock_quantity < 10,
]);
}
public function restock(Request $request, Product $product)
{
$request->validate([
'quantity' => 'required|integer|min:1',
]);
$product->increment('stock_quantity', $request->quantity);
return new ProductResource($product->fresh());
}

Test Complete Flow

Bash
# 1. Login
TOKEN=$(curl -s -X POST http://localhost:8000/api/login \
-H "Content-Type: application/json" \
-d '{"email":"admin@example.com","password":"password","device_name":"test"}' \
| jq -r '.token')
# 2. Create product
curl -X POST http://localhost:8000/api/v1/products \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Laptop",
"sku": "LAP-001",
"price": 999.99,
"stock_quantity": 50,
"description": "High-performance laptop"
}'
# 3. Check stock
curl http://localhost:8000/api/v1/products/1/stock \
-H "Authorization: Bearer $TOKEN"
# 4. Restock
curl -X POST http://localhost:8000/api/v1/products/1/restock \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"quantity": 25}'
# 5. List all products
curl http://localhost:8000/api/v1/products \
-H "Authorization: Bearer $TOKEN"

Best Practices

1. Always Use API Resources

Transform models before returning to ensure consistent structure and hide internal fields.

2. Validate All Input

Use Form Requests for validation, even in APIs.

3. Return Proper HTTP Status Codes

  • 200 OK - Successful GET/PUT
  • 201 Created - Successful POST
  • 204 No Content - Successful DELETE
  • 400 Bad Request - Validation errors
  • 401 Unauthorized - Missing/invalid token
  • 403 Forbidden - Authenticated but not authorized
  • 404 Not Found - Resource doesn't exist
  • 429 Too Many Requests - Rate limit exceeded
  • 500 Internal Server Error - Server error

4. Implement Pagination

Always paginate list endpoints to prevent performance issues.

5. Document Your API

Use tools like Scribe or L5-Swagger to generate API documentation.

Bash
composer require knuckleswtf/scribe
php artisan scribe:generate

6. Version Your API

Use URL versioning (/api/v1/...) for clarity and backward compatibility.

7. Secure Sensitive Operations

Require additional authentication (2FA, confirmation) for destructive actions.


Verification Commands

Bash
# Test authentication
php artisan test --filter=AuthenticationTest
# Test API endpoints
php artisan test --filter=ProductApiTest
# Check routes
php artisan route:list --path=api
# Verify rate limiting
php artisan tinker
>>> RateLimiter::for('api', fn() => Limit::perMinute(60));

Next Steps


Related:

Previous
Multi-tenancy