API routes give your JSON endpoints a home, and API Resources decide exactly which fields each model sends to the outside world.
๐ชAn API Resource is like a shop window. The storeroom (your database row) holds everything, including things customers must never see, but the window shows only what you carefully arrange.
Your JSON is a contract
Don't leak secrets
Returning a raw model can expose columns you forgot about. A Resource lists fields explicitly.
Stable shape
Rename a database column and the JSON can stay the same, so mobile apps don't break.
Five routes in one line
Route::apiResource registers the full set of CRUD endpoints for a controller.
From model to JSON
app/ โโโ Http/ โ โโโ Controllers/Api/ โ โ โโโ PostController.php โ --api controller โ โโโ Resources/ โ โโโ PostResource.php โ one post โ โโโ PostCollection.php โ optional custom list routes/ โโโ api.php โ created by install:api
Build a posts API
We'll expose the blog's Post model as a clean JSON API.
-
Install the API scaffolding
New Laravel 11/12 apps don't include
routes/api.phpby default. One command creates it, installs Sanctum (for tokens, next lesson) and registers the file inbootstrap/app.php.php artisan install:api->withRouting( web: __DIR__.'/../routes/web.php', api: __DIR__.'/../routes/api.php', // added for you commands: __DIR__.'/../routes/console.php', health: '/up', )Every route in
api.phpautomatically starts with/apiand is stateless (no sessions or CSRF). -
Register all CRUD routes at once
Create a controller with the
--apiflag. It skipscreateandedit, because APIs don't serve HTML forms. Then wire it up withRoute::apiResource.php artisan make:controller Api/PostController --api --model=Postuse App\Http\Controllers\Api\PostController; Route::apiResource('posts', PostController::class);GET|HEAD api/posts ........... posts.index POST api/posts ........... posts.store GET|HEAD api/posts/{post} .... posts.show PUT|PATCH api/posts/{post} .... posts.update DELETE api/posts/{post} .... posts.destroy -
Shape the output with an API Resource
An API Resource is a class that converts one model into an array. Only the keys you return end up in the JSON.
php artisan make:resource PostResource<?php namespace App\Http\Resources; use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\JsonResource; class PostResource extends JsonResource { public function toArray(Request $request): array { return [ 'id' => $this->id, 'title' => $this->title, 'excerpt' => str($this->body)->limit(80), 'author' => $this->whenLoaded('user', fn () => $this->user->name), 'comments' => $this->whenCounted('comments'), 'created_at' => $this->created_at->toIso8601String(), ]; } }whenLoadedonly includes the relation if you eager-loaded it, which helps avoid the N+1 problem. -
Return resources with the right status codes
Use
PostResource::collection()for lists andnew PostResource()for one item. Set201on create and204on delete.public function index() { $posts = Post::with('user')->withCount('comments')->latest()->paginate(10); return PostResource::collection($posts); } public function store(Request $request) { $data = $request->validate([ 'title' => ['required', 'string', 'max:255'], 'body' => ['required', 'string'], ]); $post = $request->user()->posts()->create($data); return (new PostResource($post))->response()->setStatusCode(201); } public function show(Post $post) { return new PostResource($post->load('user')); // 404 if not found } public function destroy(Post $post) { $post->delete(); return response()->noContent(); // 204 }โน๏ธ Validation is automaticIf
validate()fails on a JSON request, Laravel returns422with the errors. You'll see the exact shape in Versioning & Error Handling. -
Read the JSON, including pagination meta
Every resource response wraps data in a
datakey. When you pass a paginator, Laravel addslinksandmetaso clients can page through results.curl "http://127.0.0.1:8000/api/posts?page=2" -H "Accept: application/json"{ "data": [ { "id": 11, "title": "Queues 101", "excerpt": "Send emails later...", "author": "Ada", "comments": 4, "created_at": "2026-10-01T08:00:00+00:00" } ], "links": { "first": "http://127.0.0.1:8000/api/posts?page=1", "last": "http://127.0.0.1:8000/api/posts?page=3", "prev": "http://127.0.0.1:8000/api/posts?page=1", "next": "http://127.0.0.1:8000/api/posts?page=3" }, "meta": { "current_page": 2, "last_page": 3, "per_page": 10, "total": 27 } } -
Customise the list with a ResourceCollection
Need extra top-level data on a list? Create a ResourceCollection. It wraps many
PostResourceitems and lets you add your own keys.php artisan make:resource PostCollectionclass PostCollection extends ResourceCollection { public function toArray(Request $request): array { return [ 'data' => $this->collection, // each item uses PostResource 'summary' => ['count_on_page' => $this->collection->count()], ]; } }return new PostCollection(Post::latest()->paginate(10));
Avoid this, do that
public function show(User $user)
{
return $user; // every column!
}
Raw models expose whatever is in the table today, and anything you add tomorrow.
public function show(User $user)
{
return new UserResource($user);
}
The Resource is an allow-list: new columns stay private until you add them on purpose.
return PostResource::collection(Post::all());
Loading every row will get slower and heavier as the blog grows.
return PostResource::collection(Post::paginate(10));
Paginate lists. You get links and meta for free.
Tools like Postman, Insomnia, or the HTTP client built into many editors let you save and replay API requests. Remember to set the Accept: application/json header there too.
- Run
php artisan install:apiin your blog project and confirmroutes/api.phpexists. - Create
Api/CategoryControllerwith--apiand register it withRoute::apiResource. - Make a
CategoryResourcethat returns onlyid,nameandslug. - Paginate the index at 5 per page and check the
meta.totalvalue with curl. - Make
storereturn201anddestroyreturn204. Verify withcurl -i.
Key takeaways
php artisan install:apicreatesroutes/api.php(auto/apiprefix) and installs Sanctum.Route::apiResourceregisters index, store, show, update and destroy in one line.- API Resources are an allow-list that turns a model into exactly the JSON you intend.
- Use
::collection()for lists; pass a paginator to getlinksandmeta. - Return
201after creating and204(response()->noContent()) after deleting.