Toidicode.com

Toidicode.com

BASIC TO ADVANCE

Bài 24: API Resource và JSON:API trong Laravel 13

API ở bài trước trả về nguyên model, kể cả user_id, project_id... Tiếp tục với series, bài này mình sẽ giới thiệu với mọi người về API Resource và JSON:API trong Laravel 13 để tự quyết định API trả về những gì.

1. API Resource là gì?

API Resource là class đứng giữa model và JSON trả về: chọn trường nào được trả, đặt tên, định dạng ngày... mà không phải sửa #[Hidden] của model như Bài 19. Mình tạo hai resource, file nằm trong app/Http/Resources:

php artisan make:resource TaskResource
php artisan make:resource ProjectResource

2. Viết JsonResource cho Task và Project.

Resource kế thừa JsonResource, mảng mà toArray() trả về sẽ thành JSON:

- app/Http/Resources/TaskResource.php

<?php

namespace App\Http\Resources;

use App\Enums\TaskStatus;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class TaskResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'title' => $this->title,
            'status' => $this->status,
            'status_label' => $this->status->label(),
            'due_date' => $this->due_date?->toDateString(),
            'is_overdue' => $this->is_overdue,
            // chỉ có khi task đã hoàn thành
            'completed_at' => $this->when($this->status === TaskStatus::Done, fn () => $this->completed_at?->toDateTimeString()),
            // chỉ có khi controller đã eager load tags
            'tags' => $this->whenLoaded('tags', fn () => $this->tags->pluck('name')),
        ];
    }
}

Trong đó:

  • $this->title: đọc thẳng từ model bên trong ($this->resource), cast vẫn có tác dụng.
  • when($condition, $value): điều kiện sai thì bỏ hẳn key (không phải null), closure chỉ chạy khi điều kiện đúng.
  • whenLoaded('tags'): chỉ có key khi relationship đã load, không tự query nên không dính N+1 hay preventLazyLoading() (Bài 18).

- app/Http/Resources/ProjectResource.php

<?php

namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class ProjectResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'description' => $this->description,
            'tasks_count' => $this->whenCounted('tasks'),
            'tasks' => TaskResource::collection($this->whenLoaded('tasks')),
            // hai trường này chỉ có ở trang chi tiết
            $this->mergeWhen($request->routeIs('api.projects.show'), [
                'created_at' => $this->created_at->toDateTimeString(),
                'updated_at' => $this->updated_at->toDateTimeString(),
            ]),
        ];
    }
}

whenCounted('tasks') cần query có withCount('tasks'), collection() áp resource cho cả danh sách, mergeWhen() như when() cho nhiều key.

3. Dùng resource trong controller.

- app/Http/Controllers/Api/ProjectController.php, TaskController.php (trích các dòng return; kiểu trả về đổi thành AnonymousResourceCollection, ProjectResource, TaskResource, sửa use theo)

// ProjectController::index()
return ProjectResource::collection($request->user()->projects()->withCount('tasks')->get());

// ProjectController::show()
return new ProjectResource($project->load('tasks'));

// TaskController::index(): eager load tags để có trường tags, 3 task mỗi trang
return TaskResource::collection($project->tasks()->with('tags')->paginate(3));

// TaskController::store(): task vừa tạo nên response vẫn có status 201
return new TaskResource($project->tasks()->create($data + ['status' => TaskStatus::Todo]));

Viết tắt: $project->toResource(), $projects->toResourceCollection() tự tìm resource theo tên model.

Chạy php artisan migrate:fresh --seed, php artisan serve, lấy $TOKEN như Bài 23 rồi gọi lại (output đã đổi \u1ebf... về tiếng Việt, rút gọn bằng ...):

curl -H "Accept: application/json" -H "Authorization: Bearer $TOKEN" http://127.0.0.1:8000/api/projects/1
# {"data":{"id":1,...,"tasks":[{"id":1,"title":"Viết bài Seeder và Factory","status":"doing","status_label":"Đang làm",
#   "due_date":"2026-11-17","is_overdue":false},{"id":2,...,"completed_at":"2026-11-15 01:24:22"},...],"created_at":...}}

Chỉ task đã xong mới có completed_at. /api/projects thì có tasks_count, không có tasks.

Resource ngoài cùng được bọc trong key data: bỏ bằng JsonResource::withoutWrapping(); trong boot() của AppServiceProvider, đổi tên bằng public static $wrap = 'project';. TaskFlow giữ mặc định.

Truyền paginator vào collection() thì có thêm links, meta (vẫn bọc data dù đã withoutWrapping()):

curl -H "Accept: application/json" -H "Authorization: Bearer $TOKEN" http://127.0.0.1:8000/api/projects/1/tasks
# {"data":[{"id":1,...,"tags":["viết bài","gấp"]},{"id":2,...},{"id":3,...,"tags":["frontend"]}],
#  "links":{...,"next":"http://127.0.0.1:8000/api/projects/1/tasks?page=2"},"meta":{"current_page":1,...,"per_page":3,"to":3,"total":4}}

POST tạo task vẫn trả 201, dạng {"data":{"id":11,...}}. Muốn thêm dữ liệu riêng cho danh sách thì tạo class ResourceCollection bằng make:resource TaskCollection, TaskFlow chưa cần.

4. JSON:API resource mới trong Laravel 13.

JSON:API (jsonapi.org) là chuẩn hình dạng JSON: mỗi bản ghi có id (chuỗi), type, attributes, relationships. Laravel 13 có sẵn class JsonApiResource, tạo bằng cờ --json-api, VD php artisan make:resource JsonApi/ProjectResource --json-api (thư mục JsonApi cho khỏi trùng tên).

- app/Http/Resources/JsonApi/ProjectResource.php

<?php

namespace App\Http\Resources\JsonApi;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\JsonApi\JsonApiResource;

class ProjectResource extends JsonApiResource
{
    public $attributes = [
        'name',
        'description',
    ];

    // relationship client được include
    public $relationships = [
        'tasks' => TaskResource::class,
    ];

    public function toLinks(Request $request): array
    {
        return [
            'self' => route('api.jsonapi.projects.show', $this->resource),
        ];
    }
}

- app/Http/Resources/JsonApi/TaskResource.php

<?php

namespace App\Http\Resources\JsonApi;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\JsonApi\JsonApiResource;

class TaskResource extends JsonApiResource
{
    public function toAttributes(Request $request): array
    {
        return [
            'title' => $this->title,
            'status' => $this->status,
            'due_date' => $this->due_date?->toDateString(),
            'is_overdue' => fn () => $this->is_overdue, // chỉ tính khi cần
        ];
    }
}

Trong đó:

  • $attributes: cột lấy từ model, tự tính thì ghi đè toAttributes().
  • $relationships: relationship cho phép include và resource của nó (không ghi thì Laravel đoán ra TaskResource thường).
  • id là khóa chính, type suy từ tên class (projects), đổi bằng toId(), toType(); toLinks(), toMeta() thêm links, meta.

- routes/api.php (trích, trong group auth:sanctum, thêm use App\Http\Resources\JsonApi\ProjectResource;, use App\Models\Project;)

// cùng dữ liệu project nhưng theo chuẩn JSON:API
Route::get('/jsonapi/projects/{project}', fn (Project $project) => new ProjectResource($project))
    ->can('view', 'project')
    ->name('jsonapi.projects.show');

5. Include và sparse fieldset.

Relationship chỉ có khi client xin bằng include:

curl -i -H "Accept: application/json" -H "Authorization: Bearer $TOKEN" "http://127.0.0.1:8000/api/jsonapi/projects/2?include=tasks"
# HTTP/1.1 200 OK
# Content-Type: application/vnd.api+json
# {"data":{"id":"2","type":"projects","attributes":{"name":"TaskFlow API","description":null},
#   "relationships":{"tasks":{"data":[{"id":"5","type":"tasks"},{"id":"6","type":"tasks"},{"id":"7","type":"tasks"}]}},"links":{...}},
#  "included":[{"id":"5","type":"tasks","attributes":{"title":"Thiết kế cấu trúc API","status":"done","due_date":null,"is_overdue":false}},...]}

application/vnd.api+json là media type của JSON:API. relationships chỉ có id, type, dữ liệu nằm trong included (Laravel tự eager load). Tên ngoài $relationships (VD owner) bị bỏ qua.

Sparse fieldset là tham số fields[type] để chỉ lấy attribute cần (-g: curl không xử lý []):

curl -g -H "Accept: application/json" -H "Authorization: Bearer $TOKEN" \
    "http://127.0.0.1:8000/api/jsonapi/projects/2?include=tasks&fields[projects]=name&fields[tasks]=title,status"
# {"data":{"id":"2","type":"projects","attributes":{"name":"TaskFlow API"},...},
#  "included":[{"id":"5","type":"tasks","attributes":{"title":"Thiết kế cấu trúc API","status":"done"}},...]}

Closure is_overdue không chạy vì không được xin. So với JsonResource, JSON:API theo chuẩn, client tự chọn include, field; đổi lại JSON dài hơn, không withoutWrapping() được. TaskFlow giữ JsonResource cho các API cũ.

6. Lời kết.

Như vậy chúng ta đã tìm hiểu xong API Resource và JSON:API trong Laravel 13: JsonResource, collection, phân trang và JsonApiResource với include, sparse fieldset. Các bạn xem thêm tại tài liệu chính thức. Bài sau mình sẽ giới thiệu về service container và facade trong Laravel 13.

Đăng ký nhận tin.

Chúng tôi chỉ gửi tối đa 2 lần trên 1 tháng. Tuyên bố không spam mail!

Vũ Thanh Tài

About author
The best way to learn is to share
Xem tất cả bài đăng

0 Comments

Bài viết chưa có ai bình luận, hãy là người đầu tiên đi bạn!

Bình luận

Captcha