A minimal but real Laravel API needs three pieces working together: routes under routes/api.php, a resource controller, and an API Resource class to control exactly what JSON shape gets returned — skipping that last piece and returning Eloquent models directly is the most common shortcut that causes problems later.
1. Routes
// routes/api.php
Route::apiResource('posts', PostController::class);
apiResource() registers the standard index/store/show/update/destroy routes, skipping the create/edit form routes a normal web resource controller would also register — those don't make sense for a JSON API.
2. The controller
php artisan make:controller Api/PostController --api
public function index()
{
return PostResource::collection(Post::latest()->paginate(20));
}
public function show(Post $post)
{
return new PostResource($post);
}
public function store(StorePostRequest $request)
{
$post = Post::create($request->validated());
return new PostResource($post);
}
3. Why an API Resource, not the model directly
Returning Post::all() directly leaks every column — including ones you never meant to expose (internal flags, foreign keys with no meaning to a client). An API Resource makes the response shape explicit and independent of the database schema:
php artisan make:resource PostResource
public function toArray($request): array
{
return [
'id' => $this->id,
'title' => $this->title,
'excerpt' => $this->excerpt,
'published_at' => $this->published_at?->toIso8601String(),
'author' => $this->whenLoaded('author', fn () => [
'name' => $this->author->name,
]),
];
}
whenLoaded() only includes the relationship if it was actually eager-loaded — protecting against an accidental N+1 query triggered by the resource itself when a relationship wasn't loaded.
Validation belongs in a Form Request
Keep validation out of the controller with a dedicated request class (php artisan make:request StorePostRequest) — it keeps the controller focused on orchestration and makes the validation rules independently testable.