A mobile app, a React front end or a partner consumes your data as JSON through an API. Laravel makes this very simple, provided you control what you expose.
Enabling API routes
Since Laravel 11, the skeleton no longer contains routes/api.php. You add it with:
php artisan install:api
This command creates routes/api.php (all its routes are prefixed with /api) and installs Laravel Sanctum, used in the next lesson.
// routes/api.php
use App\Http\Controllers\Api\ProductController;
Route::apiResource('products', ProductController::class);
// GET /api/products, POST /api/products, GET|PUT|PATCH|DELETE /api/products/{product}
API Resources
Returning Product::all() would expose all columns, including internal fields, and would tie your JSON format to the database schema. An API Resource explicitly describes the public representation:
php artisan make:resource ProductResource
class ProductResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'nom' => $this->nom,
'prix' => $this->prix_centimes / 100, // format suited to the client
'en_stock' => $this->stock > 0,
'categorie' => new CategoryResource($this->whenLoaded('category')),
'cree_le' => $this->created_at->toIso8601String(),
];
}
}
whenLoaded('category') only includes the category if it was loaded with with('category'): the resource never triggers an extra query behind your back.
The API controller
class ProductController extends Controller
{
public function index()
{
return ProductResource::collection(
Product::with('category')->latest()->paginate(20)
); // { "data": [...], "links": {...}, "meta": {...} }
}
public function store(StoreProductRequest $request)
{
$product = Product::create($request->validated());
return (new ProductResource($product))->response()->setStatusCode(201);
}
public function show(Product $product)
{
return new ProductResource($product->load('category'));
}
public function destroy(Product $product)
{
$product->delete();
return response()->noContent(); // 204
}
}
A paginated collection automatically produces three keys: data (the items), links (first, last, prev, next) and meta (current_page, last_page, per_page, total…).
Status codes and errors
| Situation | Code |
|---|---|
| Successful read | 200 |
| Creation | 201 |
| Deletion with no content | 204 |
| Unauthenticated / forbidden | 401 / 403 |
| Missing resource | 404 |
| Validation failed | 422 |
In a Laravel 13 project, bootstrap/app.php already asks to render api/* route exceptions as JSON ($exceptions->shouldRenderJsonWhen(...)): a validation error there produces a 422 JSON response. The client must still send the Accept: application/json header: without it, the authentication middleware tries to redirect an unauthenticated client to a login route instead of responding 401. Validation errors take the form {"message": "...", "errors": {"nom": ["..."]}}.
Test quickly with curl:
curl -s -H "Accept: application/json" http://127.0.0.1:8000/api/products?page=2
In summary: dedicated API routes, explicit resources, deliberately loaded relationships and correct HTTP codes. This is what makes an API predictable for those who consume it.
Exercises
Solutions and tests stay on the server. In the student space, your code is run and checked, and the tutor gives hints before the solution.
Write a resource in plain PHP
Write
produitResource(array $p): array, the equivalent of aProductResource. From a row['id', 'nom', 'prix_centimes', 'stock', 'cout_achat', ...], return:id,nom;prixin euros (float rounded to 2 decimals);en_stock(boolean);categorie(['id', 'nom']) only if thecategoriekey is present (simulatingwhenLoaded).
Other fields (like
cout_achat) must never appear.function produitResource(array $p): array { }Paginated response in Laravel format
Write
reponsePaginee(array $items, int $page, int $parPage, string $url): arraythat returns['data' => ..., 'links' => ['first', 'last', 'prev', 'next'], 'meta' => ['current_page', 'last_page', 'per_page', 'total']].- Links have the form
$url . '?page=N';prev/nextarenullif they do not exist. last_pageis at least 1; the requested page is clamped between 1 andlast_page.
function reponsePaginee(array $items, int $page, int $parPage, string $url): array { }- Links have the form
A products API with apiResource
In your Laravel project: run
php artisan install:api, create theProductmodel (nom, prix_centimes, stock), aProductResourceand anApi\ProductControllercontroller withindex,store,showanddestroy. DeclareRoute::apiResource('products', ...)and test each route with curl, sendingAccept: application/json.- GET /api/products returns data, links and meta
- A valid POST /api/products returns 201 and the product in the resource format
- An invalid POST returns 422 with the errors key
- DELETE /api/products/{id} returns 204 and a GET on the same id then returns 404
The right HTTP code
Which code should you return after successfully deleting a resource, with no response body?
- 200
- 204
- 404
End-of-lesson quiz
What does php artisan install:api create?
- The routes/api.php file and the installation of Sanctum
- A new Laravel project
- OpenAPI documentation
What is the main benefit of an API Resource?
- Speeding up the database
- Explicitly defining the exposed JSON fields, independently of the schema
- Replacing validation
What does $this->whenLoaded(‘category’) do in a resource?
- Loads the category if needed
- Only includes the category if the relationship has already been loaded
- Throws an error if the category is missing
The rest of the course
- API Authentication with Sanctum and Authorization20 min
- Queues and Jobs20 min
- Performance: N+1 Queries, Eager Loading and Caching20 min
- Testing a Laravel Application with Pest and PHPUnit20 min