Programming Intermediate 16 min

How to Build a REST API in Laravel with Sanctum

Laravel Sanctum is the lightest path to token-based API authentication. Unlike Passport, it ships with Laravel and needs no OAuth infrastructure — just a single table and a trait. It is the right tool for SPAs, mobile clients, and third-party integrations where you control both ends.

This guide walks you from a fresh install through a fully protected API: resources, JSON responses, proper HTTP status codes, and token revocation. Every step assumes you already have a working Laravel app and a configured database.

Step-by-step

  1. 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_tokens table is created.

    bash
    composer require laravel/sanctum
    
    php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
    
    php artisan migrate
  2. 2

    Add HasApiTokens to User

    Add the HasApiTokens trait to your User model. This gives the model the ability to create and revoke tokens. The trait must sit alongside any existing traits like HasFactory and Notifiable.

    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. 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 the auth:sanctum middleware 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. 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. 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:resource and override the toArray method.

    bash
    php artisan make:resource UserResource
  6. 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. Use ResourceCollection for 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. 7

    Return Correct HTTP Status Codes

    HTTP status codes are part of the contract. Use them precisely: 200 for successful reads, 201 for successful creation, 422 for validation failures (Laravel does this by default), 404 when a record does not exist, and 401 for 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. 8

    Test Authentication Flow

    Send the token in the Authorization header as a Bearer token on every request to a protected route. Sanctum will resolve the user automatically via the auth:sanctum middleware 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. 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_tokens table.

    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.

#Laravel #API #Sanctum
Back to all guides

Need Help With Your Project?

Book a free 30-minute consultation to discuss your technical challenges and explore solutions together.