Journal

Building a REST API with Laravel and Sanctum

Laravel Sanctum provides a lightweight authentication system for SPAs, mobile apps, and token-based APIs. No OAuth complexity, no JWTs to manage — just API tokens backed by your database.

Setup

Sanctum ships with Laravel, but you need to publish the migration and add the middleware:

php artisan install:api
php artisan migrate

This creates the personal_access_tokens table and configures the API middleware stack.

The User Model

Make sure your User model uses the HasApiTokens trait:

use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
    use HasApiTokens, HasFactory, Notifiable;
}

Token Issuance

Create an endpoint that issues tokens on valid credentials:

// routes/api.php
Route::post('/auth/token', function (Request $request) {
    $request->validate([
        'email' => 'required|email',
        'password' => 'required',
        'device_name' => 'required',
    ]);

    $user = User::where('email', $request->email)->first();

    if (! $user || ! Hash::check($request->password, $user->password)) {
        throw ValidationException::withMessages([
            'email' => ['The provided credentials are incorrect.'],
        ]);
    }

    return ['token' => $user->createToken($request->device_name)->plainTextToken];
});

Clients send the token in subsequent requests:

Authorization: Bearer 1|abc123def456...

Resource Controllers

Scaffold a resource controller for a typical CRUD endpoint:

php artisan make:controller ArticleController --api --model=Article

This generates index, store, show, update, and destroy methods. Register the routes:

Route::apiResource('articles', ArticleController::class)
    ->middleware('auth:sanctum');

A typical store method:

public function store(Request $request): JsonResponse
{
    $validated = $request->validate([
        'title' => 'required|string|max:255',
        'body' => 'required|string',
        'tags' => 'array',
        'tags.*' => 'string|max:50',
    ]);

    $article = $request->user()->articles()->create($validated);

    return response()->json(
        new ArticleResource($article),
        Response::HTTP_CREATED,
    );
}

API Resources

API Resources transform your models into clean JSON responses:

class ArticleResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'title' => $this->title,
            'body' => $this->body,
            'tags' => $this->tags,
            'author' => new UserResource($this->whenLoaded('user')),
            'created_at' => $this->created_at->toIso8601String(),
            'updated_at' => $this->updated_at->toIso8601String(),
        ];
    }
}

For collections, Laravel automatically wraps them in a data key and includes pagination metadata.

Rate Limiting

Define rate limiters in bootstrap/app.php or a service provider:

RateLimiter::for('api', function (Request $request) {
    return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});

Authenticated users get 60 requests per minute keyed by user ID. Anonymous requests are keyed by IP. Laravel automatically adds X-RateLimit-* headers to responses.

Token Abilities

Scope tokens to specific actions:

$token = $user->createToken('deploy-key', ['articles:read']);

Check abilities in your controller:

if (! $request->user()->tokenCan('articles:write')) {
    abort(403);
}

Or use the middleware:

Route::put('/articles/{article}', [ArticleController::class, 'update'])
    ->middleware(['auth:sanctum', 'ability:articles:write']);

Testing

Laravel’s testing helpers make API testing straightforward:

public function test_authenticated_user_can_create_article(): void
{
    $user = User::factory()->create();

    $response = $this->actingAs($user)
        ->postJson('/api/articles', [
            'title' => 'Test Article',
            'body' => 'Content here.',
        ]);

    $response->assertCreated()
        ->assertJsonPath('data.title', 'Test Article');
}

Sanctum hits the sweet spot for most API projects: simple to set up, easy to reason about, and secure by default. Save OAuth for when you’re actually building a platform that third parties need to integrate with.