REST API with Laravel and API Resources

First lesson of the Advanced Laravel (API, Queues, Tests) course, free to read: about 20 minutes, 4 exercises and 3 quiz questions. This is the catalogue's base lesson; in the student space, it is rewritten for your profile.

Objectives

  • Expose a resource as a REST API with apiResource
  • Control the JSON format with API Resources
  • Return the right HTTP codes and paginated collections

API routesAPI ResourcesHTTP codes

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.

  1. CodeDifficulty 2Server sandbox2 hints

    Write a resource in plain PHP

    Write produitResource(array $p): array, the equivalent of a ProductResource. From a row ['id', 'nom', 'prix_centimes', 'stock', 'cout_achat', ...], return:

    • id, nom;
    • prix in euros (float rounded to 2 decimals);
    • en_stock (boolean);
    • categorie (['id', 'nom']) only if the categorie key is present (simulating whenLoaded).

    Other fields (like cout_achat) must never appear.

    function produitResource(array $p): array
    {
    }
  2. CodeDifficulty 3Server sandbox2 hints

    Paginated response in Laravel format

    Write reponsePaginee(array $items, int $page, int $parPage, string $url): array that returns ['data' => ..., 'links' => ['first', 'last', 'prev', 'next'], 'meta' => ['current_page', 'last_page', 'per_page', 'total']].

    • Links have the form $url . '?page=N'; prev/next are null if they do not exist.
    • last_page is at least 1; the requested page is clamped between 1 and last_page.
    function reponsePaginee(array $items, int $page, int $parPage, string $url): array
    {
    }
  3. TaskDifficulty 22 hints

    A products API with apiResource

    In your Laravel project: run php artisan install:api, create the Product model (nom, prix_centimes, stock), a ProductResource and an Api\ProductController controller with index, store, show and destroy. Declare Route::apiResource('products', ...) and test each route with curl, sending Accept: 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
  4. Multiple choiceDifficulty 11 hint

    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

  1. What does php artisan install:api create?

    • The routes/api.php file and the installation of Sanctum
    • A new Laravel project
    • OpenAPI documentation
  2. 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
  3. 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

  1. API Authentication with Sanctum and Authorization20 min
  2. Queues and Jobs20 min
  3. Performance: N+1 Queries, Eager Loading and Caching20 min
  4. Testing a Laravel Application with Pest and PHPUnit20 min

Advanced Laravel (API, Queues, Tests) · Advanced · 4 h 10 min