Series: Every Laravel Project Should Have These Building Blocks
Part: 33 of 35 Level: Beginner–Intermediate Prerequisites: Thin Controllers
What You’ll Learn
- Why returning raw Eloquent models from APIs is a mistake
- The standard response envelope:
{"success": bool, "message": string, "data": {}} - Building a
BaseApiResourcefor consistent structure - Resource Collections and pagination
- Nested resources and relationships
- A
ApiResponsehelper for non-resource responses
The Problem with Raw Eloquent Models
This is in every beginner’s codebase:
// ❌ Never do this public function show(Order $order): JsonResponse { return response()->json($order); } What’s wrong with it?
- Exposes your database schema – column names become part of your API contract. Rename a column, break all clients.
- Leaks sensitive data –
password,remember_token,stripe_customer_idare all included unless you explicitly hide them. - No version control – you can’t add a new field to the model without it appearing in the API.
- Inconsistent shape – different endpoints return different structures with no standard envelope.
API Resources solve all four problems.
The Standard Envelope
Every API response in these projects follows this shape:
{ "success": true, "message": "Order retrieved successfully.", "data": { "id": 123, "status": "pending", "total": "$250.00" } } For lists:
{ "success": true, "message": "Orders retrieved successfully.", "data": [...], "meta": { "current_page": 1, "last_page": 5, "per_page": 15, "total": 74 } } For errors:
{ "success": false, "message": "Order not found.", "data": null } The BaseApiResource
A base class that enforces the envelope on every resource:
// app/Http/Resources/BaseApiResource.php <?php declare(strict_types=1); namespace App\Http\Resources; use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\JsonResource; abstract class BaseApiResource extends JsonResource { /** * The success message for this resource. */ protected string $message = 'Data retrieved successfully.'; /** * Subclasses define the actual resource fields here. * * @return array<string, mixed> */ abstract protected function fields(Request $request): array; /** * The bare fields, without the envelope (used by collections). * * @return array<string, mixed> */ final public function fieldsFor(Request $request): array { return $this->fields($request); } /** * @return array<string, mixed> */ final public function toArray(Request $request): array { return [ 'success' => true, 'message' => $this->message, 'data' => $this->fields($request), ]; } } A concrete resource:
// app/Http/Resources/OrderResource.php final class OrderResource extends BaseApiResource { protected string $message = 'Order retrieved successfully.'; protected function fields(Request $request): array { return [ 'id' => $this->id, 'status' => $this->status->value, 'status_label' => $this->status->label(), 'total' => $this->total_formatted, 'item_count' => $this->items_count ?? $this->items->count(), 'created_at' => $this->created_at->toISOString(), 'customer' => [ 'name' => $this->customer_name, 'email' => $this->email, ], ]; } } Notice what’s NOT in fields(): password, remember_token, raw integer totals — only exactly what the API consumer needs.
Using Resources in Controllers
public function show(Order $order): OrderResource { Gate::authorize('view', $order); // $this->authorize() needs AuthorizesRequests on Laravel 11+ $order->loadMissing(['items', 'user']); return new OrderResource($order); } public function store(StoreOrderRequest $request): JsonResponse { $order = $this->createOrder->execute( $request->user(), OrderCustomerData::fromArray($request->validated()) ); return (new OrderResource($order)) ->response() ->setStatusCode(201); } Resource Collections with Pagination
For list endpoints:
// app/Http/Resources/OrderCollection.php final class OrderCollection extends ResourceCollection { // Untyped on purpose: ResourceCollection declares `public $collects;`, // and redeclaring it as `public string` is a fatal error. public $collects = OrderResource::class; public function toArray(Request $request): array { return [ 'success' => true, 'message' => 'Orders retrieved successfully.', // Bare fields per item: OrderResource::toArray() would wrap // every row in its own success/message envelope 'data' => $this->collection->map( fn (OrderResource $order): array => $order->fieldsFor($request) ), // `meta` and `links` are added automatically for paginated results ]; } } In the controller:
public function index(Request $request): OrderCollection { $orders = Order::with(['items']) ->forUser($request->user()) ->latest() ->paginate(15); return new OrderCollection($orders); } Nested Resources – Relationships Inside Resources
When a resource includes a related model, use the related resource:
// app/Http/Resources/OrderResource.php protected function fields(Request $request): array { return [ 'id' => $this->id, 'status' => $this->status->value, 'total' => $this->total_formatted, // Only include when the relationship is loaded 'items' => OrderItemResource::collection( $this->whenLoaded('items') ), 'customer' => new UserResource( $this->whenLoaded('user') ), ]; } $this->whenLoaded('items') only includes the relationship if it was eager loaded. This prevents accidental N+1 from API consumers who don’t load relationships.
The ApiResponse Helper for Non-Resource Responses
For responses that don’t involve a model (confirmation messages, action results):
// app/Http/Responses/ApiResponse.php final class ApiResponse { public static function success(string $message, mixed $data = null, int $status = 200): JsonResponse { return response()->json([ 'success' => true, 'message' => $message, 'data' => $data, ], $status); } public static function error(string $message, mixed $data = null, int $status = 422): JsonResponse { return response()->json([ 'success' => false, 'message' => $message, 'data' => $data, ], $status); } public static function notFound(string $message = 'Resource not found.'): JsonResponse { return self::error($message, status: 404); } public static function unauthorized(string $message = 'Unauthenticated.'): JsonResponse { return self::error($message, status: 401); } public static function forbidden(string $message = 'Forbidden.'): JsonResponse { return self::error($message, status: 403); } } Usage:
public function destroy(Order $order): JsonResponse { Gate::authorize('delete', $order); $order->delete(); return ApiResponse::success('Order deleted successfully.'); } FAQs
Why use API Resources instead of returning models?
Raw models leak columns you add later and break your contract; a Resource is an explicit whitelist and a stable envelope.
How do I add pagination meta to Laravel API responses?
Return a ResourceCollection – pagination meta (links, total, per_page) is added automatically.
Key Takeaways
- Never return raw Eloquent models from APIs – use Resources to control exactly what’s exposed.
- The standard envelope:
{"success": bool, "message": string, "data": {}}. Every endpoint, every response. -
BaseApiResourceenforces the envelope automatically. Subclasses only definefields(). -
$this->whenLoaded('relation')prevents N+1 in nested resources. -
OrderCollection extends ResourceCollectionaddsmetapagination data to list responses. -
ApiResponsehelper for confirmation messages and non-model responses.
Tips and Gotchas
⚠️ Warning:
$this->whenLoaded('items')returnsMissingValuewhen the relationship isn’t loaded – notnull, not an empty array. This is intentional: the key is omitted from the response entirely. If your API clients expect the key to always be present, use$this->items ?? []instead, with eager loading enforced at the controller.💡 Tip: Use Resource return types on controller methods (
public function show(Order $order): OrderResource) instead ofJsonResponse. The return type documents what the endpoint returns, and static analysis tools can verify the shape matches the Resource definition.🔥 Expert Note: Never add conditional logic that changes the response structure based on the authenticated user inside a Resource. A Resource with “if admin, show extra fields” logic is untestable and produces inconsistent API contracts. Use separate Resources (
AdminOrderResource extends OrderResource) for different permission levels.
Further Reading
- Laravel Docs: Eloquent Resources
- Laravel Docs: Resource Collections
- Laravel Docs:
whenLoaded() - JSON:API Specification – a standard envelope format worth knowing even if you don’t adopt it fully