Task Manager API with Laravel Sanctum
Build a REST API with token-based authentication and CRUD endpoints.
Introduction
30 Second Summary
Every app you use daily protects your data by requiring you to prove who you are before letting you in. Behind the scenes, this happens through API authentication tokens that act like digital ID badges.
In this project, you will build a Task Manager API with Laravel and Sanctum that issues tokens to authenticated users and rejects unauthorized requests.
What You'll Build
A fully functional REST API powered by Laravel 13 and Sanctum with user registration, login, token-based authentication, and protected CRUD endpoints for managing tasks.
By the end of this project, you'll have:
- A Laravel 13 application with Sanctum-powered API authentication that issues and validates Bearer tokens.
- Protected CRUD endpoints for tasks that only authenticated users can access.
- Hands-on experience issuing, using, and revoking API tokens to control access to your API.
- Secret Mission: Add token abilities (scopes) so that read-only tokens can only view tasks, not create or delete them.
Are there any prerequisites?
You need a basic understanding of what APIs are and how HTTP requests work. No prior PHP or Laravel experience is required.
Install PHP, Composer, and Laravel
Every Laravel project needs three core tools: PHP (the language runtime), Composer (PHP's package manager), and the Laravel framework itself.
You already have PHP, Composer, and the Laravel installer ready on your machine. In this step, you will scaffold a brand-new Laravel 13 project and install Sanctum, the package that handles API token authentication.
In this step, get ready to:
- Create a new Laravel 13 project called task-api.
- Install Laravel Sanctum for API token authentication.
- Verify that your API routing file exists and is ready.
Create a new Laravel application
The laravel new command scaffolds a full project with folder structure, configuration files, and a default SQLite database. It also launches an interactive wizard that asks a few setup questions.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your Laravel project by running:
laravel new task-api
What prompts will I see?
The installer will ask you a series of questions. Answer each one as a separate prompt appears:
- Starter kit: select None (we are building a pure API, not a web UI).
- Testing framework: select Pest.
- Database: select SQLite (zero configuration needed).
- Run default migrations: select yes.
- Once the installer finishes, move into the new project folder by running:
cd task-api
- Confirm you are inside the project by running:
ls
You should see folders like app, database, routes, and files like artisan and composer.json in the output.
Command not found or installer errors?
- If you see a "laravel: command not found" error, close your terminal completely and reopen it. The installer PATH is set during the PHP installation but only takes effect in a new terminal session.
- If the installer hangs on downloading packages, check your internet connection and try again.
Why can't I run the laravel command?
Install Sanctum and enable API routing
Laravel ships without an api.php routes file by default. The install:api Artisan command installs Laravel Sanctum and creates the API routing file in one step.
- Install Sanctum and generate the API routes file by running:
php artisan install:api
What does this command do?
This single command does three things:
- Installs the laravel/sanctum package via Composer.
- Creates a personal_access_tokens migration (the database table that stores issued tokens).
- Generates routes/api.php with a default authenticated user route protected by the auth:sanctum middleware.
- When prompted to run pending migrations, select yes.
- Verify the API routes file was created by running:
cat routes/api.php
You should see a file that includes a /user route protected by the auth:sanctum middleware.
Don't see routes/api.php?
- Make sure you ran php artisan install:api from inside the task-api folder. Running it from the wrong directory will fail silently.
- Check that the command completed without errors. If Composer reported a conflict, run composer update first, then retry php artisan install:api.
Help me fix my Sanctum installation
✔️ Awesome, I've got everything!
Great. Double check you can see routes/api.php in your project, then move on to the next step.
ⓧ I'd like to double check the full code
Your routes/api.php should look like this after running install:api:
<?php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
Route::get('/user', function (Request $request) {
return $request->user();
})->middleware('auth:sanctum');
Your project skeleton is ready with Sanctum installed. Next up, you will create the Task model and database migration that stores your to-do items.
Set Up the Task Model and Migration
Your Laravel project is set up and Sanctum is installed. Now you need a place to store the tasks that users will create through your API.
Every API needs a data layer. In this step, you will generate the Eloquent model, database migration, and controller for tasks. Then you will define the database schema and run it against your SQLite database.
In this step, get ready to:
- Generate the Task model, migration, and controller files.
- Define the database schema for the tasks table.
- Run the migration and configure the Task model with fillable fields and a relationship.
Generate the Task model, migration, and controller
Laravel's Artisan CLI can scaffold multiple files at once. The -m flag generates a migration and the -c flag generates a controller.
- Generate the Task model along with its migration and controller by running this command:
php artisan make:model Task -mc
What does this command do?
This single command creates three files for you:
- app/Models/Task.php is the Eloquent model that represents a task in your database.
- database/migrations/xxxx_xx_xx_create_tasks_table.php is the migration that defines the table structure.
- app/Http/Controllers/TaskController.php is the controller where you will add CRUD logic later.
- Confirm all three files were created by checking the terminal output. You should see three lines confirming each file was created successfully.
Command not found?
Make sure your terminal is inside the task-api directory. If you see an error about php not being recognized, close and reopen your terminal so it picks up the PATH changes from Step 1.
Help me troubleshoot this error.
Define the tasks migration
The generated migration file has placeholder columns. You need to define the actual columns your tasks table will hold.
- Open the migration file in the database/migrations/ folder. It is the file ending in _create_tasks_table.php.
- Find the up method and replace its entire contents with the following:
public function up(): void
{
Schema::create('tasks', function (Blueprint $table) {
$table->id();
// Link each task to the user who created it
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->string('title');
// Description is optional
$table->text('description')->nullable();
// New tasks start as incomplete
$table->boolean('completed')->default(false);
$table->timestamps();
});
}
What does this schema do?
Each column maps to a piece of data on a task:
- foreignId('user_id') creates a column that links each task to the user who owns it. The cascadeOnDelete() means if a user is deleted, all their tasks are automatically removed too.
- string('title') stores the task name as a short text string.
- text('description')->nullable() allows a longer description, but makes it optional.
- boolean('completed')->default(false) tracks whether the task is done, defaulting to not completed.
- Save the migration file.
Not sure which file to open?
The database/migrations/ folder contains several files. Look for the one whose name ends with _create_tasks_table.php. The timestamp prefix at the start of the filename will be different on every machine.
Help me find my migration file.
Run the migration and configure the Task model
With the schema defined, you need to run the migration so the table actually exists in your database. Then you will configure the Task model to control which fields can be written to and how tasks relate to users.
- Apply the migration to your SQLite database by running this command:
php artisan migrate
- You should see output confirming the tasks table was created. Look for a line mentioning create_tasks_table with a DONE status.
Migration failed?
If you see an error about the database not existing, confirm that your .env file has DB_CONNECTION=sqlite set. Laravel's SQLite setup should have created the database/database.sqlite file automatically during project creation.
Help me fix this migration error.
- Now open app/Models/Task.php and replace the entire file contents with the following:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Task extends Model
{
// Only allow these fields to be mass-assigned
protected $fillable = ['title', 'description', 'completed'];
// Each task belongs to one user
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}
What does this model do?
The $fillable array tells Laravel which fields can be set via mass assignment (like when you pass request data directly to create()). This protects against users injecting data into fields they should not control, such as user_id.
The user() method defines a belongsTo relationship. This means every task is owned by exactly one user. You will use this relationship later to scope tasks so each user only sees their own.
- Save app/Models/Task.php.
✔️ Awesome, I've got everything!
Great. Make sure you have saved both the migration file and the Task model.
ⓧ I'd like to double check the full code
Here is the full migration file:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('tasks', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->string('title');
$table->text('description')->nullable();
$table->boolean('completed')->default(false);
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('tasks');
}
};
Here is the full Task model:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Task extends Model
{
protected $fillable = ['title', 'description', 'completed'];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}
Here is the TaskController (it should still be empty at this point):
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class TaskController extends Controller
{
//
}
Your data layer is ready. The tasks table exists in your database and the Task model knows how to interact with it. Next up, you will configure the User model with Sanctum's token trait and build the authentication endpoints.
Configure the User Model and Auth Controller
Your Task model and database table are ready. Now you need a way for users to prove who they are before they can create or view tasks.
In this step, you will update the User model with Sanctum's HasApiTokens trait so it can issue API tokens. Then you will create an AuthController that handles user registration, login, and logout.
In this step, get ready to:
- Add the HasApiTokens trait and a tasks relationship to the User model.
- Build an AuthController with register, login, and logout endpoints.
Update the User model with HasApiTokens
Laravel Sanctum provides a HasApiTokens trait that gives your User model the ability to create and manage API tokens. You also need a relationship so each user can own many tasks.
- Open app/Models/User.php in your editor.
- Replace the entire file contents with the following code:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
use HasApiTokens, HasFactory, Notifiable;
protected $fillable = [
'name',
'email',
'password',
];
protected $hidden = [
'password',
'remember_token',
];
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
}
// Each user can own many tasks
public function tasks()
{
return $this->hasMany(Task::class);
}
}
What does this code do?
- use HasApiTokens gives the User model methods like createToken() and currentAccessToken() for issuing and managing API tokens.
- $fillable lists the fields that can be mass-assigned when creating a user.
- $hidden ensures the password and remember_token are never included in JSON responses.
- The tasks() method defines a one-to-many relationship so you can access all tasks belonging to a user.
- Save app/Models/User.php.
- You won't see a change yet. The next substep builds the AuthController that uses this trait to issue tokens.
Seeing a syntax error when you save?
- Make sure you copied the opening <?php tag at the very top of the file.
- Check that the use Laravel\Sanctum\HasApiTokens; import is above the class declaration, not inside it.
Why is my User model not recognizing HasApiTokens?
Create the AuthController with authentication methods
The AuthController handles three actions: registering a new user, logging in an existing user, and logging out by revoking the current token.
- Generate the AuthController by running this command:
php artisan make:controller AuthController
- Confirm the file was created by checking that app/Http/Controllers/AuthController.php exists in your project.
- Open app/Http/Controllers/AuthController.php and replace its entire contents with the following code:
<?php
namespace App\Http\Controllers;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;
class AuthController extends Controller
{
// Register a new user and return an API token
public function register(Request $request)
{
$request->validate([
'name' => 'required|string|max:255',
'email' => 'required|string|email|max:255|unique:users',
'password' => 'required|string|min:8|confirmed',
]);
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
]);
// Issue a new API token named 'auth-token'
$token = $user->createToken('auth-token')->plainTextToken;
return response()->json([
'user' => $user,
'token' => $token,
], 201);
}
// Log in an existing user and return a fresh API token
public function login(Request $request)
{
$request->validate([
'email' => 'required|email',
'password' => 'required',
]);
$user = User::where('email', $request->email)->first();
// If no user found or password doesn't match, throw a validation error
if (! $user || ! Hash::check($request->password, $user->password)) {
throw ValidationException::withMessages([
'email' => ['The provided credentials are incorrect.'],
]);
}
$token = $user->createToken('auth-token')->plainTextToken;
return response()->json([
'user' => $user,
'token' => $token,
]);
}
// Revoke the current token to log out
public function logout(Request $request)
{
$request->user()->currentAccessToken()->delete();
return response()->json([
'message' => 'Logged out successfully',
]);
}
}
What does this code do?
This controller handles all three authentication actions your API needs.
- register() validates the incoming data, creates a new user with a hashed password, issues a Sanctum token, and returns both the user and token as JSON with a 201 status.
- login() looks up the user by email, checks the password with Hash::check(), and issues a fresh token if credentials are valid.
- logout() calls currentAccessToken()->delete() to revoke the token that was used for this request.
What is token-based authentication?
When a user logs in, your API creates a unique string (a token) and returns it. The client stores this token and sends it with every future request in the Authorization header.
Your API checks the token on each request to identify the user. This avoids sessions and cookies, making it ideal for mobile apps and third-party integrations.
- Save app/Http/Controllers/AuthController.php.
- You won't see a visible change yet because the routes haven't been connected. The next step wires these methods to URL endpoints.
Seeing an error about an undefined class?
- Confirm the use App\Models\User; import is at the top of the file. Without it, PHP cannot find the User class.
- Check that use Illuminate\Support\Facades\Hash; is present. The Hash::make() and Hash::check() calls depend on it.
Help me debug my AuthController imports.
✔️ Awesome, I've got everything!
Great. Double check you have saved both files before moving on.
ⓧ I'd like to double check the full code
Here is the complete app/Models/User.php:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
use HasApiTokens, HasFactory, Notifiable;
protected $fillable = [
'name',
'email',
'password',
];
protected $hidden = [
'password',
'remember_token',
];
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
}
public function tasks()
{
return $this->hasMany(Task::class);
}
}
And the complete app/Http/Controllers/AuthController.php:
<?php
namespace App\Http\Controllers;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;
class AuthController extends Controller
{
public function register(Request $request)
{
$request->validate([
'name' => 'required|string|max:255',
'email' => 'required|string|email|max:255|unique:users',
'password' => 'required|string|min:8|confirmed',
]);
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
]);
$token = $user->createToken('auth-token')->plainTextToken;
return response()->json([
'user' => $user,
'token' => $token,
], 201);
}
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('auth-token')->plainTextToken;
return response()->json([
'user' => $user,
'token' => $token,
]);
}
public function logout(Request $request)
{
$request->user()->currentAccessToken()->delete();
return response()->json([
'message' => 'Logged out successfully',
]);
}
}
Your User model can now issue tokens and your authentication logic is ready. Next up, you will build the TaskController with full CRUD operations and wire everything together with protected routes.
Build the Task Controller and API Routes
Your authentication system is in place. Users can register, log in, and receive API tokens. Now it's time to build the core functionality those tokens protect.
In this step, you'll implement the TaskController with full CRUD operations and wire up the API routes. Every task endpoint will be scoped to the authenticated user, so users can only access their own data.
In this step, get ready to:
- Build CRUD methods in the TaskController for creating, reading, updating, and deleting tasks.
- Define public and protected API routes in routes/api.php.
Implement the TaskController
The TaskController handles all task-related operations. Each method uses $request->user()->tasks() to scope queries to the authenticated user. This means a user can never accidentally see or modify another user's tasks.
- Open app/Http/Controllers/TaskController.php and replace its entire contents with the following code:
<?php
namespace App\Http\Controllers;
use App\Models\Task;
use Illuminate\Http\Request;
class TaskController extends Controller
{
// List all tasks belonging to the authenticated user
public function index(Request $request)
{
return response()->json(
$request->user()->tasks()->latest()->get()
);
}
// Create a new task for the authenticated user
public function store(Request $request)
{
$request->validate([
'title' => 'required|string|max:255',
'description' => 'nullable|string',
]);
$task = $request->user()->tasks()->create($request->only('title', 'description'));
return response()->json($task, 201);
}
// Show a single task (only if it belongs to the user)
public function show(Request $request, Task $task)
{
if ($task->user_id !== $request->user()->id) {
return response()->json(['message' => 'Not found'], 404);
}
return response()->json($task);
}
// Update a task (only if it belongs to the user)
public function update(Request $request, Task $task)
{
if ($task->user_id !== $request->user()->id) {
return response()->json(['message' => 'Not found'], 404);
}
$request->validate([
'title' => 'sometimes|required|string|max:255',
'description' => 'nullable|string',
'completed' => 'sometimes|boolean',
]);
$task->update($request->only('title', 'description', 'completed'));
return response()->json($task);
}
// Delete a task (only if it belongs to the user)
public function destroy(Request $request, Task $task)
{
if ($task->user_id !== $request->user()->id) {
return response()->json(['message' => 'Not found'], 404);
}
$task->delete();
return response()->json(['message' => 'Task deleted']);
}
}
What does this code do?
Each method in the controller handles one CRUD operation, all scoped to the logged-in user:
- index() returns all tasks belonging to the authenticated user, sorted newest first.
- store() validates the request and creates a new task linked to the user.
- show(), update(), and destroy() all check that the task's user_id matches the authenticated user before proceeding. If it doesn't match, they return a 404.
- Save app/Http/Controllers/TaskController.php.
Seeing a syntax error?
Make sure you replaced the entire file contents, including the opening <?php tag. Check that every curly brace is closed properly.
help me fix a syntax error in my TaskController
Define the API routes
Routes connect incoming HTTP requests to your controller methods. You need public routes for registration and login (no token required) and protected routes that require a valid Sanctum token.
- Open routes/api.php and replace its entire contents with this code:
<?php
use App\Http\Controllers\AuthController;
use App\Http\Controllers\TaskController;
use Illuminate\Support\Facades\Route;
// Public routes
Route::post('/register', [AuthController::class, 'register']);
Route::post('/login', [AuthController::class, 'login']);
// Protected routes
Route::middleware('auth:sanctum')->group(function () {
Route::post('/logout', [AuthController::class, 'logout']);
Route::apiResource('tasks', TaskController::class);
});
What does this code do?
The file defines two groups of routes:
- Public routes: POST /register and POST /login are accessible without a token. Users need these to get a token in the first place.
- Protected routes: Everything inside the auth:sanctum middleware group requires a valid Bearer token. This includes logout and all task CRUD operations.
- Route::apiResource('tasks', TaskController::class) generates five routes (index, store, show, update, destroy) automatically, mapped to the matching controller methods.
All routes in routes/api.php are automatically prefixed with /api. So /register becomes /api/register in the final URL.
- Save routes/api.php.
- Verify your routes are registered by running this command:
php artisan route:list
You should see your routes listed, including POST api/register, POST api/login, and the task resource routes.
Not seeing your routes?
Check that the use statements at the top of routes/api.php import both AuthController and TaskController. A missing import causes Laravel to fail silently on those routes.
help me debug my API routes
✔️ Awesome, I've got everything!
Great. Double check you've saved both files before moving on.
ⓧ I'd like to double check the full code
Here is the full TaskController.php:
<?php
namespace App\Http\Controllers;
use App\Models\Task;
use Illuminate\Http\Request;
class TaskController extends Controller
{
public function index(Request $request)
{
return response()->json(
$request->user()->tasks()->latest()->get()
);
}
public function store(Request $request)
{
$request->validate([
'title' => 'required|string|max:255',
'description' => 'nullable|string',
]);
$task = $request->user()->tasks()->create($request->only('title', 'description'));
return response()->json($task, 201);
}
public function show(Request $request, Task $task)
{
if ($task->user_id !== $request->user()->id) {
return response()->json(['message' => 'Not found'], 404);
}
return response()->json($task);
}
public function update(Request $request, Task $task)
{
if ($task->user_id !== $request->user()->id) {
return response()->json(['message' => 'Not found'], 404);
}
$request->validate([
'title' => 'sometimes|required|string|max:255',
'description' => 'nullable|string',
'completed' => 'sometimes|boolean',
]);
$task->update($request->only('title', 'description', 'completed'));
return response()->json($task);
}
public function destroy(Request $request, Task $task)
{
if ($task->user_id !== $request->user()->id) {
return response()->json(['message' => 'Not found'], 404);
}
$task->delete();
return response()->json(['message' => 'Task deleted']);
}
}
And here is the full routes/api.php:
<?php
use App\Http\Controllers\AuthController;
use App\Http\Controllers\TaskController;
use Illuminate\Support\Facades\Route;
// Public routes
Route::post('/register', [AuthController::class, 'register']);
Route::post('/login', [AuthController::class, 'login']);
// Protected routes
Route::middleware('auth:sanctum')->group(function () {
Route::post('/logout', [AuthController::class, 'logout']);
Route::apiResource('tasks', TaskController::class);
});
Your API structure is complete. Every endpoint is defined and protected. Next up, you'll spin up the development server and test everything with Postman to confirm your authentication and CRUD operations work end to end.
Test Your API with Postman
Your models, controllers, and routes are all wired up. The Task Manager API is complete from a code perspective.
Now it's time to prove it works. You'll use Postman to send real HTTP requests and verify that authentication, route protection, and CRUD operations all behave correctly.
In this step, get ready to:
- Start the Laravel development server.
- Register a user and log in to receive a Bearer token.
- Test authenticated and unauthenticated requests to confirm route protection.
Start the development server
Laravel includes a built-in development server you can launch from the terminal. This serves your API locally so you can send requests to it.
- Start the Laravel development server by running this command:
php artisan serve
You should see output confirming the server is running at http://localhost:8000.
Server won't start?
If you see a message about port 8000 being in use, another process is already using that port. Stop the other process, or run the server on a different port:
php artisan serve --port=8080
If you use a different port, replace 8000 with your chosen port in all the URLs that follow.
Help me debug why php artisan serve isn't starting.
Keep this terminal open
The server runs in the foreground and occupies this terminal window. Open a new terminal tab or window if you need to run other commands while the server is running.
Test user registration and login
With the server running, you can now send HTTP requests to your API endpoints. You'll start by registering a new user, which returns a Bearer token you'll use for authenticated requests.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Postman and press Enter to open it.
- Click the + button (or press Ctrl+N) to create a new request.
- Change the HTTP method dropdown from GET to POST.
- Enter this URL in the request URL field:
http://localhost:8000/api/register
- Click the Body tab below the URL field.
- Select raw as the body type.
- Change the dropdown to the right of raw from Text to JSON.
- Paste the following JSON into the body area:
{
"name": "Test User",
"email": "test@example.com",
"password": "password123",
"password_confirmation": "password123"
}
- Click Send.
What should I see?
You should see a 201 Created status code in the response. The response body contains a user object with the registered user's details and a token string.
Copy the token value from the response. You'll need it in the next substep to authenticate your requests.
Now test the login endpoint to confirm it also returns a token.
- Create a new request tab by clicking the + button.
- Set the method to POST.
- Enter this URL:
http://localhost:8000/api/login
- Click the Body tab, select raw, and set the format to JSON.
- Paste the following JSON:
{
"email": "test@example.com",
"password": "password123"
}
- Click Send.
You should see a 200 OK response with the same user and token structure. Copy this new token for the next substep.
Seeing a 500 error?
Check that your Laravel server is still running in the other terminal window. If it stopped, run php artisan serve again.
If you see a validation error about the email already being taken, that means registration worked correctly. Use the login endpoint instead.
Help me debug my Laravel API registration or login error.
Test protected endpoints
The real test of your API's security is whether protected routes reject unauthenticated requests. You'll first create a task using your Bearer token, then confirm that requests without a token get blocked.
- Create a new request tab.
- Set the method to POST.
- Enter this URL:
http://localhost:8000/api/tasks
- Click the Authorization tab below the URL field.
- In the Type dropdown, select Bearer Token.
- Paste the token you copied from the login response into the Token field.
- Click the Body tab, select raw, and set the format to JSON.
- Paste the following JSON:
{
"title": "Learn Laravel Sanctum",
"description": "Build a task API with authentication"
}
- Click Send.
You should see a 201 Created response with your new task object, including an id, title, description, and timestamps.
Now verify that you can retrieve your tasks.
- Create a new request tab.
- Leave the method as GET.
- Enter this URL:
http://localhost:8000/api/tasks
- Click the Authorization tab and set Bearer Token with the same token as before.
- Click Send.
You should see a 200 OK response containing an array with the task you just created.
What does this prove?
The auth:sanctum middleware on your routes validates the Bearer token before allowing access. Only the user who owns that token can see their tasks.
Finally, confirm that unauthenticated requests are rejected.
- Create a new request tab.
- Leave the method as GET.
- Enter the same URL:
http://localhost:8000/api/tasks
- In the Authorization tab, set the Type dropdown to No Auth.
- Click Send.
You should see a 401 Unauthorized response. This confirms that your route protection is working correctly.
Getting 200 instead of 401?
If the unauthenticated request returns a 200, check that your routes/api.php wraps the tasks routes inside the auth:sanctum middleware group.
Also confirm you set the Authorization type to No Auth and not Inherit auth from parent, which may still pass a token from your workspace settings.
Help me debug why my unauthenticated request isn't returning 401.
Secret mission
Add Token Abilities (Scopes)
Right now, every token has full access to every endpoint. In production, you would want to issue limited tokens. For example, a read-only integration should be able to list tasks but never create or delete them. Your mission: implement token abilities so you can issue scoped tokens with granular permissions.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- The task-api project folder on your Desktop (includes all code and the SQLite database).
- The running development server (php artisan serve).
Keep everything running
No action needed. Choose this if you are still actively building or plan to extend the API further.
- Your development server will keep running until you close the terminal window or press Ctrl+C.
- Your SQLite database and all project files remain in the task-api folder.
- PHP, Composer, and the Laravel installer remain installed on your system for future projects.
Pause - I'll come back to this later
Shut down the running development server to free up your terminal. Your project files stay on disk so you can pick up where you left off.
- Switch to the terminal where php artisan serve is running.
- Press Ctrl+C to stop the server.
- When you are ready to resume, open a terminal in the task-api folder and run php artisan serve again.
Delete - I don't want to use this again
Remove all project files and start fresh. This permanently deletes your code and database.
- Switch to the terminal where php artisan serve is running and press Ctrl+C to stop it.
- Delete the project folder by running the appropriate command for your operating system.
macOS
cd ~/Desktop
rm -rf task-api
Windows
cd ~\Desktop
Remove-Item -Recurse -Force task-api
- Confirm the folder is gone by running ls (macOS) or dir (Windows) on your Desktop. You should no longer see task-api listed.
Nice Work!
Nice Work!
You just built a complete authenticated Task Manager API with Laravel 13 and Sanctum.
You've learned how to:
- Built a RESTful API with user registration, login, and token-based authentication using Laravel Sanctum.
- Implemented protected CRUD endpoints for tasks with ownership verification, so users can only access their own data.
- Tested authenticated and unauthenticated requests in Postman to confirm that the auth:sanctum middleware rejects unauthorized access with a 401 response.
- Added token abilities (scopes) to control granular access, issuing read-only tokens that cannot create or delete tasks.
Ready to quiz yourself?