A modern Laravel 13 REST API should keep routing, validation, authorization, persistence, and JSON representation separate. For a small CRUD API, use Route::apiResource(), Form Requests, Eloquent, API Resources, pagination, Sanctum authentication when needed, explicit rate limits, and feature tests. Laravel 13 documents php artisan install:api for installing API/Sanctum support. This is a clean application-layer baseline, not a complete production architecture by itself.
1. Install API support when needed
php artisan install:api
Laravel 13's Sanctum documentation explicitly provides this command. In an existing project, review the generated/configured files before committing changes.
2. Define API resource routes
use App\Http\Controllers\Api\PostController;
use Illuminate\Support\Facades\Route;
Route::apiResource('posts', PostController::class);
apiResource() registers API resource actions while omitting browser form routes such as create and edit.
3. Public reads and authenticated writes
Route::apiResource('posts', PostController::class)
->only(['index', 'show']);
Route::middleware('auth:sanctum')->group(function () {
Route::apiResource('posts', PostController::class)
->only(['store', 'update', 'destroy']);
});
This split is an example, not a universal rule. If posts are private, reads need authentication/authorization too.
4. Validate with Form Requests
php artisan make:request StorePostRequest
php artisan make:request UpdatePostRequestpublic function rules(): array
{
return [
'title' => ['required', 'string', 'max:200'],
'body' => ['required', 'string'],
];
}
Validation answers “is this input structurally acceptable?” Authorization answers “may this user perform this action?” Keep those concerns distinct.
5. Make the writable surface explicit
public function store(StorePostRequest $request)
{
$post = $request->user()->posts()->create(
$request->safe()->only([
'title',
'body',
])
);
return (new PostResource($post))
->response()
->setStatusCode(201);
}
Do not accept user_id, moderation flags, roles, billing state, or other server-owned fields because database columns happen to exist. Model mass-assignment policy and Form Request validation should reflect the public write contract.
6. Shape output with API Resources
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class PostResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'title' => $this->title,
'body' => $this->body,
'created_at' => $this->created_at?->toIso8601String(),
'author' => new UserResource(
$this->whenLoaded('author')
),
];
}
}
An API Resource decouples the public JSON contract from the table schema and can conditionally expose relationships that the query deliberately loaded.
7. Eager-load before serialization
public function show(Post $post)
{
$post->load('author');
return new PostResource($post);
}
public function index()
{
$posts = Post::query()
->with('author')
->latest()
->paginate(20);
return PostResource::collection($posts);
}
Laravel's resource layer preserves pagination links/meta for paginated collections. Bound any client-controlled page size rather than allowing unlimited result sets.
8. Use HTTP status codes consistently
- 200: successful reads and many updates.
- 201: successful creation.
- 204: successful deletion with no response body.
- 401: authentication required but missing/invalid.
- 403: authenticated but not authorized.
- 404: route-model-bound resource does not exist.
- 422: Laravel JSON validation failure.
9. Update and delete with authorization
public function update(UpdatePostRequest $request, Post $post)
{
$this->authorize('update', $post);
$post->update(
$request->safe()->only([
'title',
'body',
])
);
return new PostResource($post->refresh());
}
public function destroy(Post $post)
{
$this->authorize('delete', $post);
$post->delete();
return response()->noContent();
}
The exact policy rules depend on the application, but the example makes authorization explicit instead of assuming validated input implies permission.
10. Sanctum: choose the auth mode that matches the client
Sanctum supports first-party SPA authentication and API tokens. Choose based on the client architecture. Never put real tokens or secrets into documentation, source code, screenshots, CI logs, or shell history; use appropriate environment/secret-management systems.
11. Rate limiting
Laravel provides rate-limiter APIs and middleware integration. Do not copy a universal requests-per-minute number from a tutorial. Limits should reflect endpoint cost, authentication identity, abuse risk, expected automation, burst tolerance, and downstream constraints. Login/token, expensive search/reporting, public reads, and normal CRUD often need different policies.
12. Error handling
Laravel already renders validation and many framework exceptions appropriately for JSON requests. If the product needs a custom error envelope, customize exception rendering centrally rather than returning unrelated hand-written error shapes from every controller.
13. Feature-test the API contract
public function test_authenticated_user_can_create_a_post(): void
{
$user = User::factory()->create();
$response = $this
->actingAs($user)
->postJson('/api/posts', [
'title' => 'Example post',
'body' => 'Example body',
]);
$response
->assertStatus(201)
->assertJsonPath('data.title', 'Example post');
$this->assertDatabaseHas('posts', [
'user_id' => $user->id,
'title' => 'Example post',
]);
}
Add tests for validation failure, unauthenticated/forbidden access, missing resources, pagination, update/delete, and any rate-limit/auth behavior your clients rely on.
14. Production boundaries this tutorial does not solve
- API versioning and backwards compatibility;
- idempotency for retryable writes;
- concurrent/optimistic update handling;
- tenant isolation and fine-grained authorization;
- audit/compliance logging;
- queues and long-running jobs;
- observability, tracing, alerting, and incident response;
- schema compatibility during rolling deployments;
- secret rotation and infrastructure/network controls.
Use this article as an application-layer foundation, then design these system concerns for the actual service.
Minimal scaffolding sequence
php artisan install:api
php artisan make:controller Api/PostController --api
php artisan make:request StorePostRequest
php artisan make:request UpdatePostRequest
php artisan make:resource PostResource
These commands create scaffolding; they do not make an API secure or production-ready by themselves.
Related guides
Sources and further reading
- Laravel 13.x: Sanctum
- Laravel 13.x: Routing
- Laravel 13.x: Validation
- Laravel 13.x: API Resources
- Laravel 13.x: Rate Limiting
- Laravel 13.x: HTTP Tests