
April 13, 2026
14 min read
By Kokil Thapa | Last reviewed: September 2026
Most Laravel APIs I review on client projects work in Postman on day one, then fracture under real load: inconsistent JSON shapes, missing versioning, weak auth scopes, and no tests. Following laravel api best practices from the start turns an API from a fragile integration layer into something mobile apps, SaaS dashboards, and third-party partners can depend on for years. Whether you are shipping a booking backend for a Nepal travel platform or a headless storefront, the same rules apply on Laravel 12 and 13 with PHP 8.3+. This guide walks through the patterns I use on production API development projects — architecture first, then auth, validation, errors, security, performance, and testing.
How should you structure a Laravel API for production?
Structure is the contract your consumers memorize. Break it once and every client app needs an emergency release. On production Laravel applications I maintain, I treat the API layer as a separate surface from the web UI — even when both live in the same codebase.
Version your routes from the first deploy
URL prefix versioning (/api/v1/) is the default I recommend. It is easy to test in curl, easy to cache at the CDN, and easy to explain to non-technical stakeholders. Header-based versioning works for large public APIs but adds friction for mobile teams. Query-parameter versioning (?version=1) breaks HTTP caching — avoid it.
// routes/api.php
Route::prefix('v1')->group(function () {
Route::apiResource('orders', V1\OrderController::class);
Route::apiResource('products', V1\ProductController::class);
});
Route::prefix('v2')->group(function () {
Route::apiResource('orders', V2\OrderController::class);
}); When you outgrow a single controller file, namespace controllers under App\Http\Controllers\Api\V1 and keep route names prefixed (api.v1.orders.show). For a deeper versioning strategy — deprecation headers, sunset dates — see the dedicated guide on Laravel API versioning strategy.
Use a consistent JSON response envelope
Pick one envelope and never deviate. I use a small trait shared across API controllers:
// app/Traits/ApiResponse.php
trait ApiResponse
{
protected function success(mixed $data = null, string $message = 'Success', int $code = 200)
{
return response()->json([
'success' => true,
'message' => $message,
'data' => $data,
], $code);
}
protected function error(string $message = 'Error', int $code = 400, mixed $errors = null)
{
return response()->json([
'success' => false,
'message' => $message,
'errors' => $errors,
], $code);
}
} When integrating with clients that expect RFC 7807 Problem Details, you can extend this pattern — see API error handling with RFC 7807 for the migration path.
Transform output with API Resources, never raw models
Returning Eloquent models directly leaks hidden attributes, exposes internal column names, and couples your database schema to every mobile app in the wild. API Resources give you a stable public contract:
// app/Http/Resources/OrderResource.php
class OrderResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'reference' => $this->reference,
'total' => $this->total,
'status' => $this->status,
'created_at' => $this->created_at->toIso8601String(),
'customer' => new UserResource($this->whenLoaded('customer')),
'items' => OrderItemResource::collection($this->whenLoaded('items')),
'links' => [
'self' => route('api.v1.orders.show', $this->id),
],
];
}
} The official Laravel Eloquent API Resources documentation covers conditional fields, resource collections, and pagination wrapping. On the Nepal Gift Card platform — a Laravel API backing digital gift card purchases — Resources let us rename internal fields without forcing a mobile app resubmission to app stores.
How do you authenticate a Laravel API with Sanctum or Passport?
Authentication is where most hobby APIs cut corners. A token that never expires and grants full admin access is a breach waiting to happen.
Sanctum vs Passport — pick once, document why
| Package | Best for | Token type |
|---|---|---|
| Sanctum | First-party SPAs, mobile apps, simple token APIs | Personal access tokens, session cookies |
| Passport | OAuth2 server, third-party developer access | OAuth2 access + refresh tokens |
For most Laravel APIs in 2026, Sanctum is the right default. It ships with Laravel, supports SPA cookie auth and mobile bearer tokens, and stays simpler to operate than a full OAuth2 server. Choose Passport only when external developers need client credentials or authorization-code flows. Read the full comparison in Laravel Passport vs Sanctum, and the official Laravel Sanctum documentation for installation steps on Laravel 12+.
Scope tokens with abilities and expiration
// Issue a scoped token
$token = $user->createToken('mobile-app', ['orders:read', 'orders:write']);
// Check scope in Form Request or Policy
if ($request->user()->tokenCan('orders:write')) {
// proceed
}
// config/sanctum.php
'expiration' => 60 * 24, // 24 hours — never null in production Token security checklist I apply on every API:
- HTTPS only — terminate TLS at Nginx or your load balancer.
- Set expiration in
config/sanctum.php— non-expiring tokens are unacceptable. - Scope with abilities — a read-only dashboard token should not delete records.
- Revoke on logout and password change via
$request->user()->currentAccessToken()->delete(). - Never log bearer tokens — strip Authorization headers from log aggregators.
Authorisation beyond token scope belongs in Policies. Pair Sanctum authentication with Laravel Policies for resource-level checks — covered in depth in the Laravel policies and gates guide.
How do you validate requests and paginate responses in Laravel?
Validation belongs in Form Request classes, not controller methods. Controllers should orchestrate; they should not contain twenty lines of inline rules.
Form Requests for every write endpoint
// app/Http/Requests/Api/V1/StoreOrderRequest.php
class StoreOrderRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()->tokenCan('orders:write');
}
public function rules(): array
{
return [
'customer_id' => ['required', 'integer', 'exists:customers,id'],
'items' => ['required', 'array', 'min:1'],
'items.*.product_id' => ['required', 'integer', 'exists:products,id'],
'items.*.quantity' => ['required', 'integer', 'min:1', 'max:99'],
'notes' => ['nullable', 'string', 'max:500'],
];
}
}
// Controller
public function store(StoreOrderRequest $request): JsonResponse
{
$order = $this->orderService->create($request->validated());
return $this->success(new OrderResource($order), 'Order created', 201);
} Laravel automatically returns 422 Unprocessable Entity with a JSON error payload when validation fails on API routes. That is the behaviour mobile clients expect — do not catch validation exceptions and flatten them into 400 responses.
Always paginate collections
Unbounded ->get() calls are how APIs die at scale. Paginate every list endpoint and cap per_page:
public function index(Request $request)
{
$perPage = min((int) $request->input('per_page', 15), 100);
$orders = Order::query()
->with(['customer', 'items.product'])
->when($request->status, fn ($q, $s) => $q->where('status', $s))
->latest()
->paginate($perPage);
return OrderResource::collection($orders);
} Offset pagination works for most admin dashboards. When you expect tables with millions of rows and deep page numbers, cursor pagination avoids expensive OFFSET scans — see the cursor vs offset pagination deep dive. While prototyping payloads, paste sample JSON through the JSON formatter tool to catch structural mistakes early.
How should Laravel APIs return errors and HTTP status codes?
HTTP status codes are part of your API contract. Using 200 for everything and hiding errors in a JSON flag forces clients to parse bodies on every response — that is a design failure, not a convenience.
| Code | Meaning | When to use |
|---|---|---|
| 200 | OK | Successful GET, PUT, PATCH |
| 201 | Created | Successful POST that creates a resource |
| 204 | No Content | Successful DELETE with no body |
| 401 | Unauthenticated | Missing or invalid token |
| 403 | Forbidden | Authenticated but not authorised |
| 404 | Not Found | Resource does not exist |
| 422 | Unprocessable | Validation failed |
| 429 | Too Many Requests | Rate limit exceeded |
| 500 | Server Error | Unhandled exception — should trigger alerting |
Centralise exception rendering for API routes
In Laravel 11 and 12, configure API exception responses in bootstrap/app.php:
->withExceptions(function (Exceptions $exceptions) {
$exceptions->render(function (NotFoundHttpException $e, Request $request) {
if ($request->is('api/*')) {
return response()->json([
'success' => false,
'message' => 'Resource not found',
], 404);
}
});
$exceptions->render(function (AuthenticationException $e, Request $request) {
if ($request->is('api/*')) {
return response()->json([
'success' => false,
'message' => 'Unauthenticated',
], 401);
}
});
$exceptions->render(function (AuthorizationException $e, Request $request) {
if ($request->is('api/*')) {
return response()->json([
'success' => false,
'message' => 'Forbidden',
], 403);
}
});
}) Never expose stack traces in production API responses. Log the full exception server-side; return a generic 500 message to the client. For a complete REST foundation including routing conventions, see how to build a REST API in Laravel the right way.
What security and performance practices protect a Laravel API?
Security and performance overlap more than teams admit. An unrate-limited search endpoint is both a DDoS vector and a database killer.
Rate limiting per user and per IP
// bootstrap/app.php
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Facades\RateLimiter;
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});
RateLimiter::for('auth', function (Request $request) {
return Limit::perMinute(5)->by($request->ip());
}); Apply the auth limiter to login and password-reset routes. Return 429 with a Retry-After header when limits trigger. Advanced patterns — sliding windows, tiered plans, IP blocklists — are covered in the guide on API rate limiting and abuse prevention and rate limiting in Laravel.
CORS, input hygiene, and OWASP basics
// config/cors.php
return [
'paths' => ['api/*'],
'allowed_origins' => [env('FRONTEND_URL', 'https://app.example.com')],
'allowed_methods' => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
'allowed_headers' => ['Content-Type', 'Authorization', 'Accept'],
'max_age' => 86400,
]; Never set 'allowed_origins' => ['*'] on authenticated endpoints. Validate file uploads with MIME rules and size caps. Eloquent uses parameterised queries by default — do not bypass it with raw concatenated SQL. For a practical OWASP checklist applied to Laravel, read secure Laravel: OWASP Top 10 in practice.
Eager loading, caching, and query discipline
// Bad — N+1 on every row
$orders = Order::paginate(15);
// Good — fixed query count
$orders = Order::with(['customer', 'items.product'])->paginate(15);
// Cache expensive read endpoints
$featured = Cache::remember('api.featured-products', 3600, fn () =>
Product::where('featured', true)->limit(10)->get()
); Add database indexes on columns used in WHERE, ORDER BY, and foreign keys. Select only needed columns on large tables. For Redis-backed caching patterns, see Redis caching for Laravel and the broader guide on caching strategies for web performance.
How do you test and document a production Laravel API?
An API without tests is a promise without proof. An API without docs is a product nobody can integrate.
Feature tests for auth, validation, and happy paths
class OrderApiTest extends TestCase
{
use RefreshDatabase;
public function test_guest_cannot_list_orders(): void
{
$this->getJson('/api/v1/orders')->assertStatus(401);
}
public function test_authenticated_user_lists_orders(): void
{
Order::factory()->count(3)->create();
$user = User::factory()->create();
$this->actingAs($user, 'sanctum')
->getJson('/api/v1/orders')
->assertStatus(200)
->assertJsonStructure([
'data' => ['*' => ['id', 'reference', 'status']],
'links', 'meta',
]);
}
public function test_validation_rejects_empty_order(): void
{
$user = User::factory()->create();
$this->actingAs($user, 'sanctum')
->postJson('/api/v1/orders', [])
->assertStatus(422)
->assertJsonValidationErrors(['items']);
}
} Run with php artisan test --parallel in CI. Test at minimum: unauthenticated access (401), forbidden access (403), validation failures (422), successful CRUD (200/201/204), and not-found (404). For manual and automated API testing workflows, see API testing with Postman, Newman, and Insomnia.
Generate documentation from code
Undocumented endpoints become Slack threads and guesswork. I standardise on Scribe or OpenAPI:
- Scribe — scans routes, Form Requests, and Resources to generate HTML docs. See API documentation with Scribe for Laravel.
- OpenAPI 3.x — industry-standard spec shareable with any client generator.
- Postman collections — export from OpenAPI for QA and partner onboarding.
Document every endpoint with method, URL, auth requirements, request schema, response examples, and all error codes. These practices apply whether you are building a simple content API or a full REST API with Laravel. For broader application-level conventions, see modern Laravel architecture best practices.
On the Adventure Third Pole Trek booking platform — Laravel with Livewire and a companion API — we run GitLab CI pipelines that lint, test, and deploy through Deployer 7 with PHP-FPM reload after each symlink swap. That same discipline applies to any API serving mobile clients or partner integrations.
Key Takeaways
- Prefix routes with
/api/v1/from the first release so breaking changes never strand existing clients. - Use Sanctum with scoped abilities and token expiration unless you genuinely need OAuth2 server features from Passport.
- Validate every write through Form Request classes; return 422 with structured errors, never silent 400s.
- Wrap list endpoints in pagination, eager-load relationships, and cache expensive reads with Redis.
- Rate-limit by user ID and IP; return 429 with
Retry-Afteron auth and search endpoints. - Write feature tests for auth, validation, and CRUD on every endpoint; generate docs with Scribe or OpenAPI.
People Also Ask
Should I use Sanctum or Passport for my Laravel API?
Use Sanctum for first-party mobile apps, SPAs, and internal token APIs — it is simpler and ships with Laravel. Choose Passport only when you need a full OAuth2 server for third-party developer access with client credentials or authorization-code flows. Most production Laravel APIs in 2026 never need Passport.
What is the best JSON response format for a Laravel API?
Use a consistent envelope: { "success": true, "message": "...", "data": { ... } } for happy paths and { "success": false, "message": "...", "errors": { ... } } for failures. Wrap single models and collections in API Resources so the data key always contains a predictable shape regardless of database changes.
How do I version a Laravel API without breaking clients?
Route prefix versioning (/api/v1/, /api/v2/) is the most practical approach. Keep v1 controllers running while v2 ships breaking changes. Add deprecation headers to v1 responses and set a sunset date before removing old routes. Never change response field names in-place on a live version.
Do Laravel APIs need rate limiting in production?
Yes — always. Apply a default limit (typically 60 requests per minute per user or IP) on all API routes, tighter limits on authentication endpoints (5 per minute), and stricter caps on expensive search or export endpoints. Return HTTP 429 with a Retry-After header so clients can back off gracefully.
Ship APIs your clients can trust
Following these laravel api best practices does not require a large team or a microservices rewrite. Version your routes, scope your tokens, validate every input, paginate every list, test every endpoint, and document what you ship. That is the difference between an API that survives three years of mobile app updates and one that becomes a rewrite project. If you need help architecting or hardening a production API — for a booking system, eCommerce backend, or client portal — get in touch or review our Nepal Gift Card API project for a real-world Laravel reference.
Frequently Asked Questions
0 Comments
Leave a comment
Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

