Laravel makes it almost too easy to return JSON. A route closure with return $users; technically works β and that convenience is exactly how APIs end up leaking database columns, breaking mobile clients on every refactor, and falling over under a burst of traffic. Building an API you can support for years requires four deliberate decisions: how you shape responses, how you authenticate, how you version, and how you throttle. Let's walk through each.
Start with install:api
Modern Laravel applications no longer ship with API routes by default. The first step on a fresh project is a single command:
php artisan install:api
This creates routes/api.php, registers it in bootstrap/app.php with the /api prefix, and installs Laravel Sanctum along with its migration for the personal access tokens table. From there, every route you define in that file is automatically stateless and wrapped in the api middleware group.
Shape every response with API resources
The single most important habit for API longevity is never returning Eloquent models directly. An API resource is a transformation layer that gives you an explicit contract between your database schema and your public payload:
class OrderResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'status' => $this->status,
'total' => $this->total_cents / 100,
'placed_at' => $this->created_at->toIso8601String(),
'customer' => new CustomerResource($this->whenLoaded('customer')),
'items' => OrderItemResource::collection($this->whenLoaded('items')),
];
}
}
Two details in that example carry most of the value. First, the payload is curated β internal columns like internal_notes or stripe_customer_id simply never appear, so adding a column to the table can never accidentally expose it. Second, whenLoaded() only includes relationships that were explicitly eager loaded, which means your resource layer can never trigger an N+1 query on its own. Pair it with OrderResource::collection($orders) for lists, and pagination metadata (links, per-page counts) is added automatically when you pass a paginator.
Authenticate with Sanctum tokens
For first-party SPAs, mobile apps, and simple third-party integrations, Sanctum hits the sweet spot between "no auth" and a full OAuth2 server like Passport. Issuing a token is one line, and abilities give you coarse-grained scoping:
$token = $user->createToken('mobile', ['orders:read', 'orders:create']);
return response()->json(['token' => $token->plainTextToken]);
Protect routes with the auth:sanctum middleware, and check abilities inside controllers or policies with $request->user()->tokenCan('orders:create'). A few practices worth adopting from day one:
- Name tokens meaningfully ("mobile", "zapier-integration") so users can review and revoke them individually.
- Set an expiration via the
expirationoption inconfig/sanctum.php, or pass a per-tokenexpiresAtβ long-lived tokens that never rotate are a liability. - Use cookie-based SPA authentication instead of tokens when the consumer is your own first-party SPA on the same domain; you get CSRF protection and HttpOnly cookies for free.
- Prune expired tokens on a schedule with
Schedule::command('sanctum:prune-expired')->daily().
Version from the very first release
Versioning feels like premature ceremony right up until the day a mobile release in app-store review depends on a payload you want to change. Adding a version prefix costs nothing now and saves a migration project later. The pragmatic approach in Laravel is URI versioning with grouped route files:
// bootstrap/app.php
->withRouting(
api: __DIR__.'/../routes/api.php',
apiPrefix: 'api/v1',
)
// or explicit groups inside routes/api.php
Route::prefix('v1')->group(base_path('routes/api_v1.php'));
Route::prefix('v2')->group(base_path('routes/api_v2.php'));
Keep versioning at the edges: version your routes and your resource classes (V1\OrderResource, V2\OrderResource), but let controllers, actions, and models stay version-agnostic wherever possible. Most v2s change response shape, not business logic, so the resource layer is usually the only thing that genuinely forks.
Rate limiting that matches reality
Laravel's throttle middleware is backed by named rate limiters, and the default api limiter (60 requests per minute per user or IP) is defined for you. Real APIs usually need more nuance β cheaper limits for anonymous traffic, generous limits for authenticated users, and strict limits on expensive endpoints:
RateLimiter::for('api', function (Request $request) {
return $request->user()
? Limit::perMinute(120)->by($request->user()->id)
: Limit::perMinute(20)->by($request->ip());
});
RateLimiter::for('exports', fn (Request $request) =>
Limit::perHour(10)->by($request->user()->id)
);
Clients receive standard X-RateLimit-Remaining headers and a 429 with a Retry-After when they exceed the limit, so well-behaved consumers can back off automatically. If you need finer control, recent Laravel releases also support per-second limits, which are useful for smoothing bursts rather than capping volume.
Consistent errors are part of the contract
Laravel automatically renders validation failures as 422 JSON responses with an errors object, which is a good baseline. Extend the same discipline to the rest of your error surface: register custom renderings in bootstrap/app.php with $exceptions->render() so domain exceptions map to stable status codes and machine-readable error codes. A client that can rely on {"message": ..., "code": "insufficient_stock"} for every failure is dramatically easier to build against than one parsing prose.
Wrapping up
None of these four pillars is difficult in isolation β Laravel gives you first-class tools for each. The discipline is in adopting all of them before you have consumers: resources so your schema stays private, Sanctum with expiring scoped tokens, a v1 prefix from the first deploy, and rate limits tuned per audience. Do that, and the API you ship this month will still be pleasant to maintain when v3 rolls around.
Related Service
π» Web Development
Custom websites and web applications built with PHP, Laravel, WordPress, and React β fast, secure, scalable, and tailored to your business goals.
Explore Web Development →