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.