API resource routes — apiResource(), stateless conventions
Concept
Route::apiResource() is the stateless twin of Route::resource(). It registers only five routes instead of seven, omitting create and edit — the two routes that serve HTML forms. REST APIs don't need dedicated "show form" endpoints; consumers construct their own UI. This keeps your route table clean and makes it explicit that the controller is JSON-only.
| Action | Verb | URI | Controller Method | Route Name |
|---|---|---|---|---|
| List | GET | /users | index | users.index |
| Store | POST | /users | store | users.store |
| Show | GET | /users/{user} | show | users.show |
| Update | PUT/PATCH | /users/{user} | update | users.update |
| Destroy | DELETE | /users/{user} | destroy | users.destroy |
Internally, Router::apiResource() calls resource() with ->except(['create', 'edit']) automatically. There is no separate registrar class; it's a convenience wrapper. Route::apiResources() accepts an associative array to register multiple API resources in one call.
The stateless convention goes beyond just omitting routes. API resource controllers should not use sessions, flash data, or redirect back with input. Validation failures should return 422 Unprocessable Entity with a JSON error body — which FormRequest does automatically when the request expects JSON. Successful mutations return 200 with the updated resource or 201 Created with the new resource and a Location header.
Content negotiation is the companion concern: controllers should call $request->expectsJson() or rely on the fact that API routes are inside the api middleware group, which sets the Accept header expectation. Response macros like response()->json() and API Resources (JsonResource) are the natural pairing for apiResource() controllers.
A common real-world pattern for SaaS APIs: Route::apiResources() registers all top-level resources in routes/api.php, nested inside a versioned prefix group and a auth:sanctum middleware group. Each controller extends a base ApiController that handles pagination conventions, error format, and CORS headers uniformly.
Code Example
<?php
use App\Http\Controllers\Api\UserController;
use App\Http\Controllers\Api\PostController;
use App\Http\Controllers\Api\CommentController;
use Illuminate\Support\Facades\Route;
// API resource — 5 routes, no create/edit
Route::apiResource('users', UserController::class);
// Multiple resources in one call
Route::apiResources([
'users' => UserController::class,
'posts' => PostController::class,
'comments' => CommentController::class,
]);
// Typical API structure with versioning and auth
Route::prefix('/v1')
->middleware(['auth:sanctum', 'throttle:60,1'])
->name('api.v1.')
->group(function () {
Route::apiResource('users', UserController::class);
Route::apiResource('users.posts', PostController::class)->shallow();
});
// API resource controller — no create() or edit() methods needed
class UserController extends Controller
{
public function index(Request $request): AnonymousResourceCollection
{
$users = User::paginate($request->integer('per_page', 15));
return UserResource::collection($users);
}
public function store(StoreUserRequest $request): JsonResponse
{
$user = User::create($request->validated());
return (new UserResource($user))
->response()
->setStatusCode(201)
->header('Location', route('api.v1.users.show', $user));
}
public function show(User $user): UserResource
{
return new UserResource($user->load('roles'));
}
public function update(UpdateUserRequest $request, User $user): UserResource
{
$user->update($request->validated());
return new UserResource($user->fresh());
}
public function destroy(User $user): Response
{
$user->delete();
return response()->noContent(); // 204 No Content
}
}Interview Q&A
Q: Why does a REST API not need create and edit routes, and what replaces the information they would have provided?
create and edit exist to serve HTML forms with pre-populated data (field names, validation constraints, current values). A REST API client is responsible for knowing the resource schema through documentation, an OpenAPI spec, or a /schema discovery endpoint. The client renders its own form independently. The server only cares about the submitted data (store, update) and whether it is valid. Removing these routes enforces the stateless constraint of REST and keeps the API contract explicit and minimal.
Q: What HTTP status codes should a store action return on success versus a validation failure, and what does Laravel do by default?
A successful store should return 201 Created with the created resource in the body and a Location header pointing to the new resource's URL. By default, controllers returning a model or resource return 200; you must explicitly set ->setStatusCode(201). A validation failure should return 422 Unprocessable Entity with a JSON body listing field-level errors. FormRequest does this automatically when it detects the request expects JSON (Accept: application/json header), redirecting back only for non-JSON requests.
Q: How do you handle partial updates (PATCH) versus full replacement (PUT) in a single update controller method?
In practice, most Laravel APIs treat both PUT and PATCH identically in the controller — they both call $model->update($request->validated()) with whatever was submitted. Eloquent's update() only sets the keys present in the array, so a PATCH with partial data works correctly. To strictly enforce PUT semantics (all required fields must be present), you can create a separate UpdateUserRequest for PUT that marks fields as required versus a PatchUserRequest that marks them as sometimes|required. Route ->methods(['PUT']) and ->methods(['PATCH']) would point to different FormRequest classes but the same controller method, checking $request->method() to decide which FormRequest rules to apply.