Originally published on laravel.learnio.dev — part of the Laravel best practices course. View this lesson on the original site.
Design stateless endpoints with clear contracts
API routing in Laravel is about predictable JSON endpoints, explicit versioning, and clean auth boundaries.
Use routes/api.php for API surfaces
routes/api.php is stateless and uses the api middleware group by default. Laravel also applies an /api URI prefix automatically.
Stateless means Laravel does not treat API calls like browser page navigation with session-driven flow. Each request should carry the context it needs, typically auth tokens and payload data, without relying on prior server-side session state.
Simple rule of thumb:
- web flow often says "the user has a session, send them to another page",
- API flow says "the client sent a request, return a self-contained JSON result".
In practice, this means:
- no redirect-based form UX expectations,
- no session-first assumptions for authentication,
- API clients (mobile, SPA, third-party service) must send credentials on each request,
- responses should be JSON contracts that stand on their own.
So, if two identical API requests arrive with the same token and payload, they should produce equivalent outcomes regardless of what happened in a previous page interaction.
Example:
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
Route::get('/user', function (Request $request) {
return $request->user();
})->middleware('auth:sanctum');
This route resolves as /api/user.
Group and version your API routes
Route::prefix('v1')->name('api.v1.')->group(function () {
Route::get('/products', [ApiProductController::class, 'index'])
->name('products.index');
Route::post('/products', [ApiProductController::class, 'store'])
->name('products.store');
});
Versioning avoids breaking older clients during future API changes.
Return JSON intentionally
Laravel can auto-convert arrays and models to JSON, but explicit responses stay clear:
return response()->json([
'data' => $products,
'meta' => ['count' => $products->count()],
]);
Bad way vs better way
Bad way (mixing browser and API concerns):
Route::get('/products', [ProductController::class, 'index']); // web route
Better way (API-specific route file and middleware):
Route::middleware(['auth:sanctum'])
->prefix('v1')
->group(function () {
Route::get('/products', [ApiProductController::class, 'index']);
});
Why better:
- keeps session and browser flow separate from API contracts,
- makes auth strategy explicit for clients,
- reduces accidental HTML responses in API contexts.
Practical API routing checklist
- define API routes in
routes/api.php, - use prefixes for versioning (
v1,v2), - keep JSON response shapes consistent,
- apply auth and rate limiting at route group level.
API route flow
flowchart LR
A[API request] --> B[api middleware group]
B --> C[Versioned route match]
C --> D[Controller action]
D --> E[JSON response]
Next, you will connect API routing with error and exception patterns so failures stay predictable for clients.
From Laravel best practices — Routing, Controllers & Request Flow