How to Build a REST API with Laravel 13: Complete Guide with Sanctum and API Resources
Building a REST API with Laravel is one of the most in-demand skills in 2026: any mobile app, SPA frontend, or AI agent that connects to your system will do so through an API. In this guide we’ll set up a fully functional REST API with Laravel 13, including routes, controllers, Form Requests for validation, API Resources for serialization, and Sanctum for token-based authentication.
What is a REST API?
A REST API (Representational State Transfer) is an interface that exposes resources accessible over HTTP, almost always returning JSON. The key principles: each resource has a unique URL, operations use standard HTTP verbs (GET, POST, PUT/PATCH, DELETE), responses are stateless, and the structure follows predictable conventions.
1. Create a New Laravel 13 Application
If you don’t have a project yet, check out How to Install Laravel 13 first. In short:
laravel new my-api
cd my-api
In Laravel 11+, the routes/api.php file is not created by default. To enable it, run:
php artisan install:api
This command creates routes/api.php, automatically registers the /api prefix, and installs Laravel Sanctum for token-based authentication.
2. Configure the Database
Edit the .env file with your credentials:
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=my_api
DB_USERNAME=root
DB_PASSWORD=
To get started quickly, you can also use SQLite:
DB_CONNECTION=sqlite
Then run the migrations:
php artisan migrate
3. Create the Model, Migration, and Factory
We’ll build a “products” CRUD. Generate everything in one command:
php artisan make:model Product -mfr
The -mfr flags create a migration, factory, and resource controller. Edit the generated migration:
public function up(): void
{
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->text('description')->nullable();
$table->decimal('price', 10, 2);
$table->integer('stock')->default(0);
$table->timestamps();
});
}
And in the app/Models/Product.php model, declare the $fillable fields:
class Product extends Model
{
use HasFactory;
protected $fillable = ['name', 'description', 'price', 'stock'];
}
Run the migration:
php artisan migrate
4. Create the API Resource
API Resources are Laravel’s idiomatic way to control exactly which fields your API exposes:
php artisan make:resource ProductResource
Edit app/Http/Resources/ProductResource.php:
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'description' => $this->description,
'price' => (float) $this->price,
'stock' => $this->stock,
'created_at' => $this->created_at->toIso8601String(),
];
}
5. Create Form Requests for Validation
Instead of validating inside the controller, the idiomatic approach in 2026 is to use dedicated Form Requests:
php artisan make:request StoreProductRequest
php artisan make:request UpdateProductRequest
Edit StoreProductRequest:
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'description' => ['nullable', 'string'],
'price' => ['required', 'numeric', 'min:0'],
'stock' => ['required', 'integer', 'min:0'],
];
}
6. Implement the Controller
app/Http/Controllers/ProductController.php already has the skeleton. Fill it in like this:
use App\Http\Requests\StoreProductRequest;
use App\Http\Requests\UpdateProductRequest;
use App\Http\Resources\ProductResource;
use App\Models\Product;
class ProductController extends Controller
{
public function index()
{
return ProductResource::collection(Product::latest()->paginate(20));
}
public function store(StoreProductRequest $request)
{
$product = Product::create($request->validated());
return new ProductResource($product);
}
public function show(Product $product)
{
return new ProductResource($product);
}
public function update(UpdateProductRequest $request, Product $product)
{
$product->update($request->validated());
return new ProductResource($product);
}
public function destroy(Product $product)
{
$product->delete();
return response()->noContent();
}
}
Notice two key patterns of modern Laravel: route model binding (Product $product is already resolved from the URL) and response()->noContent() (correctly returns 204 No Content).
7. Define the Routes
Edit routes/api.php:
use App\Http\Controllers\ProductController;
use Illuminate\Support\Facades\Route;
Route::apiResource('products', ProductController::class);
A single line registers all five REST routes: GET /api/products, POST /api/products, GET /api/products/{id}, PUT/PATCH /api/products/{id}, and DELETE /api/products/{id}.
Verify with:
php artisan route:list --path=api
8. Authentication with Laravel Sanctum
When you ran php artisan install:api, Sanctum was installed. To issue tokens, add the HasApiTokens trait to App\Models\User:
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
use HasApiTokens, HasFactory, Notifiable;
// ...
}
Create a login endpoint that returns the token:
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
Route::post('/login', function (Request $request) {
$request->validate([
'email' => ['required', 'email'],
'password' => ['required'],
]);
$user = User::where('email', $request->email)->first();
if (! $user || ! Hash::check($request->password, $user->password)) {
return response()->json(['message' => 'Invalid credentials'], 401);
}
return response()->json([
'token' => $user->createToken('api')->plainTextToken,
'user' => $user,
]);
});
And protect the product routes:
Route::middleware('auth:sanctum')->group(function () {
Route::apiResource('products', ProductController::class);
});
Now every request to /api/products requires the header:
Authorization: Bearer {token}
Sanctum issues opaque tokens, but if your API uses JSON Web Tokens (JWT) you can inspect their header and payload and verify the signature with this tool:
9. Testing the API
Use Bruno, Thunder Client, Postman, or curl directly:
# Login
curl -X POST http://localhost:8000/api/login \
-H "Content-Type: application/json" \
-d '{"email":"[email protected]","password":"password"}'
# Create a product (replace TOKEN with the one returned above)
curl -X POST http://localhost:8000/api/products \
-H "Authorization: Bearer TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"Laptop","price":1500,"stock":10}'
10. Tests with PHPUnit (or Pest)
Laravel 13 ships with Pest integrated by default. A basic test:
use App\Models\User;
use Laravel\Sanctum\Sanctum;
it('lists products for authenticated users', function () {
Sanctum::actingAs(User::factory()->create());
$this->getJson('/api/products')
->assertOk()
->assertJsonStructure(['data', 'meta', 'links']);
});
it('rejects requests without a token', function () {
$this->getJson('/api/products')->assertStatus(401);
});
Run the tests:
php artisan test
Conclusion
In 2026, building a REST API with Laravel is practically “run install:api, create a resource controller, and you’re done.” What separates a toy API from a professional one is the details: API Resources for clean serialization, Form Requests for validation, Sanctum for token-based authentication, and tests to avoid breaking anything with each release.
Once your API is up and running, two natural next steps are: optimizing performance with How to Optimize Your Laravel Project’s Performance and, if you want to expose your logic to AI agents, reading How to Build an AI Agent with Laravel and MCP.
If this guide was useful, don’t forget to share it. Cheers!