Reader Stacks

Building a Simple REST API in Laravel 13

A modern Laravel API separates routes, validation, authorization, persistence, resources, authentication and tests instead of returning raw models from controllers.

Building a Simple REST API in Laravel 13

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 UpdatePostRequest
public 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

Topics: APIs & Integrations