JSON APIs with resources and Sanctum

Laravel · Practical · 15 min

Most Laravel apps also serve mobile apps or SPAs, and resources keep the API format stable as models change.

Run php artisan install:api to add routes/api.php (routes prefixed with /api, stateless) and install Sanctum for authentication.

Returning models directly works, but it couples your API to your database columns. An API resource is a transformation layer: php artisan make:resource PostResource creates a class whose toArray() decides exactly what clients see:

return [
    'id' => $this->id,
    'title' => $this->title,
    'author' => new UserResource($this->whenLoaded('author')),
    'comments_count' => $this->whenCounted('comments'),
];

$this proxies to the model. whenLoaded includes a relation only if it was eager-loaded, so a resource never triggers N+1 queries by itself. when($cond, $value) adds conditional fields, for example for admins only.

Return new PostResource($post) for one item and PostResource::collection($posts) for many. Output is wrapped in a data key; with a paginator, links and meta (current page, total and so on) are added automatically. Set status codes with ->response()->setStatusCode(201).

Sanctum tokens: $user->createToken('mobile', ['posts:write'])->plainTextToken issues a token once; clients send it as Authorization: Bearer <token>. Protect routes with the auth:sanctum middleware and check abilities with $request->user()->tokenCan('posts:write'). For a first-party SPA on the same domain, Sanctum can use cookie sessions instead.

Throttle API routes, version them (/api/v1), and return consistent error shapes.

Example

<?php
// app/Http/Resources/PostResource.php
class PostResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'title' => $this->title,
            'author' => new UserResource($this->whenLoaded('author')),
            'comments_count' => $this->whenCounted('comments'),
            'internal_notes' => $this->when($request->user()?->is_admin, $this->notes),
        ];
    }
}

// routes/api.php
Route::middleware(['auth:sanctum', 'throttle:60,1'])->prefix('v1')->group(function () {
    Route::get('/posts', fn () => PostResource::collection(
        Post::with('author')->withCount('comments')->latest()->paginate(20)
    ));
    Route::post('/posts', [PostApiController::class, 'store']);
});
// GET /api/v1/posts -> {"data": [...], "links": {...}, "meta": {"current_page": 1, ...}}
// Issue a token: $user->createToken('mobile', ['posts:write'])->plainTextToken

Remember

  • API resources decouple the JSON shape from database columns
  • whenLoaded and whenCounted avoid hidden N+1 queries
  • Sanctum issues bearer tokens; protect routes with auth:sanctum

Watch out for

  • Returning models straight from API routes, leaking new columns (or hidden data) as the schema evolves.

Check yourself

  1. What does whenLoaded('author') do if the author relation wasn't eager-loaded?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: laravel.com/docs/12.x/eloquent-resources