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'])->plainTextTokenRemember
- 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.