404 Not Found

404 Not Found


nginx

Laravel API Resources and RESTful API Development

API Resources is Laravel's "data wrapper"—it controls which fields are exposed to clients, how they are formatted, and how relationships are nested, ensuring that API output follows a consistent standard.

1. What You'll Learn


2. A True Story of a Front-End Developer

(1) Pain Point: The JSON format returned by the API causes the front end to crash

Alice ran into a nightmare while integrating with the ShopMetrics API—/shops returned {shops: [...]}, but /orders returned {data: [...]}, and /products simply returned an array. The field names weren't consistent either: some used created_at, some used createdAt, and others used createdDate. Passwords and internal IDs were also exposed in the JSON. There was more front-end adaptation code than business logic.

(2) Solution for API Resources

API Resources uniformly control the JSON output format—ensuring consistent field names, hiding sensitive fields, nesting association conditions, and adhering to standard pagination formats.

PHP
// app/Http/Resources/ShopResource.php
class ShopResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'slug' => $this->slug,
            'status' => $this->status,
            'products' => ProductResource::collection($this->whenLoaded('products')),
            'created_at' => $this->created_at->toISOString(),
        ];
    }
}

(3) Revenue

After Alice started using API Resources, all endpoints were standardized, front-end adaptation code was reduced by 80%, and sensitive fields such as passwords were automatically hidden.


3. The Resource Class and ResourceCollection

(1) Create a Resource

BASH
php artisan make:resource ShopResource
php artisan make:resource ProductResource
php artisan make:resource OrderResource
# Creates: app/Http/Resources/ShopResource.php

(2) Resource Class (Single Record)

PHP
// app/Http/Resources/ShopResource.php
class ShopResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'slug' => $this->slug,
            'description' => $this->description,
            'status' => $this->status,
            'revenue' => (float) $this->revenue,
            'created_at' => $this->created_at->toISOString(),
            'updated_at' => $this->updated_at->toISOString(),
        ];
    }
}

// Usage — single resource
return new ShopResource($shop);
// {"data": {"id": 1, "name": "Alice Store", ...}}

(3) ResourceCollection (Multiple Records)

PHP
// Using resource collection
return ShopResource::collection($shops);
// {"data": [...], "links": {...}, "meta": {...}}

// Custom collection
php artisan make:resource ShopCollection
class ShopCollection extends ResourceCollection
{
    public function toArray(Request $request): array
    {
        return [
            'data' => $this->collection,
            'meta' => [
                'total_shops' => $this->collection->count(),
            ],
        ];
    }
}
Type Return Format Suitable For
new Resource($model) {data: {...}} Single Record
Resource::collection($models) {data: [...], links, meta} List + Pagination
CustomCollection Custom Format Requires additional meta

(1) ▶ Example: ShopMetrics ShopResource

PHP
// app/Http/Resources/ShopResource.php
class ShopResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'slug' => $this->slug,
            'description' => $this->whenNotNull($this->description),
            'status' => $this->status,
            'revenue' => [
                'raw' => (float) $this->revenue,
                'formatted' => $this->revenue_formatted,
            ],
            'products_count' => $this->whenCounted('products'),
            'orders_count' => $this->whenCounted('orders'),
            'products' => ProductResource::collection($this->whenLoaded('products')),
            'latest_order' => new OrderResource($this->whenLoaded('latestOrder')),
            'links' => [
                'self' => route('api.v1.shops.show', $this->id),
                'products' => route('api.v1.products.index', ['shop_id' => $this->id]),
            ],
            'created_at' => $this->created_at->toISOString(),
        ];
    }
}

Output:

TEXT
// Execution Successful

(1) whenLoaded() Conditional Loading

PHP
// Only include relation if it was eager-loaded
'products' => ProductResource::collection($this->whenLoaded('products')),

// If products were not loaded with(), this returns null and is omitted
// Shop::find(1) → no products key in JSON
// Shop::with('products')->find(1) → products included

// Single relation
'user' => new UserResource($this->whenLoaded('user')),

// Count only (no data)
'products_count' => $this->whenCounted('products'),

(2) Defining Nested Resources

PHP
// app/Http/Resources/OrderResource.php
class OrderResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'order_number' => $this->order_number,
            'status' => $this->status,
            'total' => (float) $this->total,
            'items' => OrderItemResource::collection($this->whenLoaded('items')),
            'shop' => new ShopBriefResource($this->whenLoaded('shop')),
            'user' => new UserBriefResource($this->whenLoaded('user')),
            'created_at' => $this->created_at->toISOString(),
        ];
    }
}

// Brief resource — minimal data for nesting
class ShopBriefResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'slug' => $this->slug,
        ];
    }
}
Method Description How to Avoid Problems
whenLoaded() Output only after joins have been loaded Avoid N+1 with lazy loading
whenCounted() Output only after counting Avoid extra queries
whenNotNull() Output only if not null Clean up empty fields
when() Conditional Output Flexible Control
Brief Resource Use the Lite Version for Nesting Avoid Nested Loops

(1) ▶ Example: ShopMetrics Nested Resources

PHP
// app/Http/Resources/OrderItemResource.php
class OrderItemResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'product' => new ProductBriefResource($this->whenLoaded('product')),
            'quantity' => $this->quantity,
            'unit_price' => (float) $this->price,
            'subtotal' => (float) ($this->price * $this->quantity),
        ];
    }
}

// app/Http/Resources/ProductBriefResource.php
class ProductBriefResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'sku' => $this->sku,
        ];
    }
}

Output:

TEXT
// Execution Successful

5. RESTful API Design Specifications

(1) URIs and HTTP Verbs

Operation HTTP URI Description
List GET /api/v1/shops Returns a collection
Create POST /api/v1/shops Create resource
Details GET /api/v1/shops/{id} Return a single record
Full Update PUT /api/v1/shops/{id} Replace Resource
Partial Update PATCH /api/v1/shops/{id} Modify Fields
Delete DELETE /api/v1/shops/{id} Delete resource

(2) HTTP Status Codes

Status Code Meaning Use Case
200 OK Successful response
201 Created Resource created successfully
204 No Content Deleted successfully
400 Bad Request Invalid request format
401 Unauthorized Not Authenticated
403 Forbidden No Permission
404 Not Found Resource does not exist
422 Unprocessable Entity Validation Failed
429 Too Many Requests Rate Limiting
500 Internal Server Error Server Error

(3) Incorrect Response Format

JSON
{
    "success": false,
    "message": "Validation failed.",
    "errors": {
        "name": ["The name field is required."],
        "email": ["The email must be a valid email address."]
    }
}

(1) ▶ Example: ShopMetrics RESTful API Endpoint Design

PHP
// routes/api.php
Route::prefix('v1')->middleware('auth:sanctum')->group(function () {
    // Shops
    Route::apiResource('shops', Api\V1\ShopController::class);
    Route::get('shops/{shop}/analytics', [Api\V1\ShopAnalyticsController::class, 'show']);

    // Products (nested then shallow)
    Route::apiResource('shops.products', Api\V1\ProductController::class)->shallow();

    // Orders
    Route::apiResource('orders', Api\V1\OrderController::class)->only(['index', 'show', 'update']);
    Route::post('orders/{order}/cancel', [Api\V1\OrderController::class, 'cancel']);

    // Categories
    Route::apiResource('categories', Api\V1\CategoryController::class)->only(['index', 'show']);

    // Analytics
    Route::get('analytics/overview', [Api\V1\AnalyticsController::class, 'overview']);
});

Output:

TEXT
// Execution Successful

6. Filtering, Sorting, and Pagination Encapsulation

(1) Query Filter Base Class

PHP
// app/Filters/QueryFilter.php
abstract class QueryFilter
{
    public function __construct(protected Request $request) {}

    public function apply(Builder $query): Builder
    {
        foreach ($this->filters() as $filter => $value) {
            if (method_exists($this, $filter) && $value !== null) {
                $this->$filter($query, $value);
            }
        }
        return $query;
    }

    protected function filters(): array
    {
        return $this->request->all();
    }
}

(2) Specific Filter

PHP
// app/Filters/ShopFilter.php
class ShopFilter extends QueryFilter
{
    public function search(Builder $query, string $value): Builder
    {
        return $query->where('name', 'like', "%{$value}%");
    }

    public function status(Builder $query, string $value): Builder
    {
        return $query->where('status', $value);
    }

    public function min_revenue(Builder $query, float $value): Builder
    {
        return $query->where('revenue', '>=', $value);
    }

    public function sort(Builder $query, string $value): Builder
    {
        $direction = str_starts_with($value, '-') ? 'desc' : 'asc';
        $field = ltrim($value, '-');
        return $query->orderBy($field, $direction);
    }
}

(1) ▶ Example: ShopMetrics API Endpoints with Filters

PHP
// app/Http/Controllers/Api/V1/ShopController.php
class ShopController extends Controller
{
    public function index(ShopFilter $filter): JsonResponse
    {
        $shops = Shop::where('tenant_id', tenant()->id)
            ->filter($filter)
            ->withCount(['products', 'orders'])
            ->paginate(request()->integer('per_page', 15));

        return ShopResource::collection($shops);
    }

    public function store(StoreShopRequest $request): JsonResponse
    {
        $shop = Shop::create(array_merge($request->validated(), ['tenant_id' => tenant()->id]));
        return response()->json([
            'message' => 'Shop created.',
            'data' => new ShopResource($shop),
        ], 201);
    }

    public function show(Shop $shop): JsonResponse
    {
        $shop->load(['products' => fn ($q) => $q->active()->latest()->take(10)]);
        return new ShopResource($shop);
    }

    public function update(UpdateShopRequest $request, Shop $shop): JsonResponse
    {
        $shop->update($request->validated());
        return new ShopResource($shop);
    }

    public function destroy(Shop $shop): Response
    {
        $shop->delete();
        return response()->noContent();
    }
}

Output:

TEXT
// Execution Successful

7. API Version Control

(1) Routing Group Version

PHP
// routes/api.php
Route::prefix('v1')->group(function () {
    Route::apiResource('shops', Api\V1\ShopController::class);
});

Route::prefix('v2')->group(function () {
    Route::apiResource('shops', Api\V2\ShopController::class);
});

(2) Resource Inheritance

PHP
// V1 ShopResource
namespace App\Http\Resources\Api\V1;
class ShopResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'status' => $this->status,
        ];
    }
}

// V2 ShopResource — extends and adds fields
namespace App\Http\Resources\Api\V2;
class ShopResource extends \App\Http\Resources\Api\V1\ShopResource
{
    public function toArray(Request $request): array
    {
        return array_merge(parent::toArray($request), [
            'revenue' => (float) $this->revenue,
            'products_count' => $this->whenCounted('products'),
            'links' => [
                'self' => route('api.v2.shops.show', $this->id),
            ],
        ]);
    }
}
Versioning Strategy Approach Pros and Cons
URL Prefix /api/v1/, /api/v2/ ✅ Simple and Clear
Header Accept: application/vnd.api.v2+json More RESTful but More Complex
Query ?version=2 Not recommended, not RESTful

(1) ▶ Example: ShopMetrics API Version Routing

PHP
// routes/api.php
Route::prefix('v1')->middleware('auth:sanctum')->group(function () {
    Route::apiResource('shops', Api\V1\ShopController::class);
    Route::apiResource('products', Api\V1\ProductController::class);
    Route::apiResource('orders', Api\V1\OrderController::class)->only(['index', 'show']);
});

Route::prefix('v2')->middleware('auth:sanctum')->group(function () {
    Route::apiResource('shops', Api\V2\ShopController::class);
    Route::apiResource('products', Api\V2\ProductController::class);
    Route::apiResource('orders', Api\V2\OrderController::class);
    // V2 adds full CRUD for orders + analytics
    Route::get('analytics', [Api\V2\AnalyticsController::class, 'overview']);
});

Output:

TEXT
// Execution Successful

8. Comprehensive Example: The Complete Process for ShopMetrics API Resources

PHP
// ============================================
// Comprehensive: ShopMetrics API Resources
// Covers: resources, relations, filtering, pagination, versioning
// ============================================

// app/Http/Resources/Api/V1/OrderResource.php
class OrderResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'order_number' => $this->order_number,
            'status' => $this->status,
            'subtotal' => (float) $this->subtotal,
            'discount' => (float) $this->discount,
            'total' => (float) $this->total,
            'items_count' => $this->whenCounted('items'),
            'items' => OrderItemResource::collection($this->whenLoaded('items')),
            'shop' => new ShopBriefResource($this->whenLoaded('shop')),
            'user' => [
                'id' => $this->whenLoaded('user')?->id,
                'name' => $this->whenLoaded('user')?->name,
            ],
            'created_at' => $this->created_at->toISOString(),
            'updated_at' => $this->updated_at->toISOString(),
        ];
    }
}

// Controller with full pipeline
class OrderController extends Controller
{
    public function index(OrderFilter $filter): JsonResponse
    {
        $orders = Order::where('tenant_id', tenant()->id)
            ->filter($filter)
            ->with(['shop', 'user'])
            ->withCount('items')
            ->latest()
            ->paginate(request()->integer('per_page', 15));

        return OrderResource::collection($orders);
    }

    public function show(Order $order): JsonResponse
    {
        $order->load(['items.product', 'shop', 'user']);
        return new OrderResource($order);
    }

    public function update(UpdateOrderRequest $request, Order $order): JsonResponse
    {
        $order->update($request->validated());
        return new OrderResource($order->fresh()->load('items.product'));
    }
}

❓ FAQ

Q What is the difference between using Resource and directly returning the model JSON?
A Directly returning the model exposes all fields (including sensitive data such as passwords), and the format cannot be controlled; Resource allows for precise control over output fields, formatted values, and nested relationships. APIs must use Resource.
Q What is the difference between whenLoaded and direct access to a relationship?
A Direct access to a relationship triggers lazy loading (the N+1 problem); whenLoaded only outputs the relationship if it has been preloaded; if it hasn't been loaded, it returns null and is automatically removed from the JSON.
Q How many versions of the API are needed?
A Typically, only two versions are maintained (the current one and the previous one). When a new version is released, users are given a 6-month migration period; after that period expires, the old version returns a 410 Gone status. Avoid maintaining too many versions at the same time.
Q Can the pagination format for ResourceCollection be customized?
A Yes. Create a custom Collection class and override the toArray() method, or use JsonResource::collection()'s paginationResponse() method to customize the pagination format.
Q How do I access authenticated users in a Resource?
A Use $request->user() or auth()->user(). The Resource's toArray() method accepts a Request parameter, allowing you to determine which fields to output based on the user's permissions.
Q What should I do if there is duplicate code between Brief Resource and Full Resource?
A Have Brief Resource inherit from Full Resource and only override toArray() to output fewer fields; or use $this->when() in Full Resource to dynamically output fields based on context.

📖 Summary


📝 Exercises

  1. Basic Exercise (⭐): Create three resource classes—ShopResource, ProductResource, and OrderResource—for ShopMetrics. In the controller, replace the direct return of the model to ensure consistent JSON formatting.

  2. Advanced Exercise (⭐⭐): Implement the ShopFilter query filter to support the search, status, min_revenue, and sort parameters. Use it in ShopController::index and test the filtering functionality using Postman.

  3. Challenge (⭐⭐⭐): Design two API versions, V1 and V2—V1 returns only the basic fields, while V2 additionally returns revenue, products_count, and links. Implement this using resource inheritance, ensuring that the V1 endpoint does not break existing clients.

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏