Step-by-step
-
1
Install and Configure Sanctum
Sanctum is included in Laravel by default since version 11. For earlier versions install it with Composer. After installation publish the config and migration files so the
personal_access_tokenstable is created.bashcomposer require laravel/sanctum php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider" php artisan migrate -
2
Add HasApiTokens to User
Add the
HasApiTokenstrait to yourUsermodel. This gives the model the ability to create and revoke tokens. The trait must sit alongside any existing traits likeHasFactoryandNotifiable.php<?php namespace App\Models; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use Laravel\Sanctum\HasApiTokens; class User extends Authenticatable { use HasApiTokens, HasFactory, Notifiable; } -
3
Define API Routes
API routes live in
routes/api.php. Public endpoints — like login and register — sit outside any middleware. Protected endpoints are wrapped in theauth:sanctummiddleware group. Laravel automatically prefixes every route in this file with/api.php<?php use App\Http\Controllers\Api\AuthController; use App\Http\Controllers\Api\UserController; use Illuminate\Support\Facades\Route; // Public Route::post('/register', [AuthController::class, 'register']); Route::post('/login', [AuthController::class, 'login']); // Protected Route::middleware('auth:sanctum')->group(function () { Route::get('/user', [UserController::class, 'show']); Route::post('/logout', [AuthController::class, 'logout']); Route::apiResource('/posts', PostController::class); }); -
4
Build the Login Endpoint
The login endpoint validates credentials, creates a token via
createToken(), and returns it in the response. The token name is arbitrary — use something that identifies the consuming client. On failure return 401, not 422; wrong password is an authentication error, not a validation error.php<?php namespace App\Http\Controllers\Api; use App\Models\User; use Illuminate\Http\Request; use Illuminate\Support\Facades\Hash; use Illuminate\Validation\ValidationException; class AuthController extends Controller { public function login(Request $request) { $request->validate([ 'email' => 'required|email', 'password' => '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.'], ]); } $token = $user->createToken($request->device_name ?? 'api')->plainTextToken; return response()->json(['token' => $token], 200); } public function logout(Request $request) { $request->user()->currentAccessToken()->delete(); return response()->json(['message' => 'Logged out'], 200); } } -
5
Create an API Resource
API Resources transform Eloquent models into JSON. They let you shape exactly what fields the client receives — and they decouple your database schema from your API contract. Generate one with
make:resourceand override thetoArraymethod.bashphp artisan make:resource UserResource -
6
Define the Resource Shape
Inside
toArray()list only the fields the API should expose. Never pass through the whole model — you will leak password hashes, internal flags, and anything else that happens to be on the model. UseResourceCollectionfor lists.php<?php namespace App\Http\Resources; use Illuminate\Http\Resources\Json\JsonResource; class UserResource extends JsonResource { public function toArray($request): array { return [ 'id' => $this->id, 'name' => $this->name, 'email' => $this->email, 'created_at' => $this->created_at->toISOString(), ]; } } -
7
Return Correct HTTP Status Codes
HTTP status codes are part of the contract. Use them precisely:
200for successful reads,201for successful creation,422for validation failures (Laravel does this by default),404when a record does not exist, and401for unauthenticated requests. Never return 200 for an error.php<?php namespace App\Http\Controllers\Api; use App\Http\Resources\PostResource; use App\Models\Post; use Illuminate\Http\Request; class PostController extends Controller { public function index() { return PostResource::collection(Post::latest()->paginate(20)); // 200 implicit } public function store(Request $request) { $data = $request->validate([ 'title' => 'required|string|max:255', 'content' => 'required|string', ]); $post = Post::create($data); return new PostResource($post); // 201 // return (new PostResource($post))->response()->setStatusCode(201); } public function show(Post $post) { return new PostResource($post); // 200 } public function destroy(Post $post) { $post->delete(); return response()->json(null, 204); // No Content } } -
8
Test Authentication Flow
Send the token in the
Authorizationheader as a Bearer token on every request to a protected route. Sanctum will resolve the user automatically via theauth:sanctummiddleware and make it available through$request->user().bash# 1. Login and capture the token curl -s -X POST https://example.com/api/login \ -H 'Accept: application/json' \ -H 'Content-Type: application/json' \ -d '{"email":"user@example.com","password":"secret","device_name":"curl"}' # Response: {"token":"1|abc123..."} # 2. Use the token curl -s https://example.com/api/user \ -H 'Accept: application/json' \ -H 'Authorization: Bearer 1|abc123...' -
9
Revoke Tokens
Revoking a single token (logout) deletes the current token. Revoking all tokens signs the user out of every device — useful after a password change or a security incident. Both operations are just Eloquent deletes on the
personal_access_tokenstable.php// Revoke current token (single device logout) $request->user()->currentAccessToken()->delete(); // Revoke all tokens (sign out everywhere) $request->user()->tokens()->delete();
Tips & gotchas
- Always send <code>Accept: application/json</code> from the client — without it, Laravel returns HTML error pages instead of JSON.
- Scope tokens when creating them: <code>createToken('mobile', ['posts:read', 'posts:write'])</code>. Check with <code>$request->user()->tokenCan('posts:write')</code>.
- Set a token expiration in <code>config/sanctum.php</code> via <code>expiration</code> (in minutes). Combine with a refresh-token pattern for long-lived sessions.
- Never put sensitive logic in routes — keep controllers thin and push business rules into service classes or action classes.
- Use <code>Route::apiResource()</code> instead of <code>Route::resource()</code> — it skips the HTML form routes (<code>create</code>, <code>edit</code>) that APIs do not need.
Wrapping up
Sanctum gives you production-ready token authentication in under an hour. Keep your resources focused, your status codes correct, and your token lifetimes short — and you will have an API that is both secure and easy to consume.