API Resource là lớp trung gian giúp chúng ta kiểm soát dữ liệu từ Eloquent Model trước khi Laravel trả dữ liệu về cho API client dưới dạng JSON.
Ở Bài 41 — REST API, chúng ta đã tạo được:
GET /api/posts
GET /api/posts/{post}
POST /api/posts
PUT /api/posts/{post}
DELETE /api/posts/{post}
Nhưng chúng ta gặp một vấn đề:
return $post;
hoặc:
return Post::all();
Laravel có thể tự chuyển Model thành JSON.
Điều này rất tiện.
Nhưng đối với một API thực tế, chúng ta thường không muốn trả toàn bộ dữ liệu của Model.
Đó chính là lúc:
API Resource
xuất hiện.
1. API Resource là gì?
Có thể hình dung:
Database
↓
Eloquent Model
↓
API Resource
↓
JSON
↓
Frontend / Mobile App
Ví dụ Database có:
posts
id
user_id
category_id
title
slug
excerpt
content
image
status
published_at
view_count
created_at
updated_at
Nhưng API public có thể chỉ cần:
{
"id": 1,
"title": "Laravel 13 là gì?",
"slug": "laravel-13-la-gi",
"excerpt": "Tìm hiểu Laravel 13",
"image": "...",
"published_at": "...",
"view_count": 125
}
Chúng ta không nhất thiết phải đưa toàn bộ database record ra ngoài.
API Resource tạo ra một lớp biến đổi dữ liệu nằm giữa Model và JSON response. Đây cũng là mục đích chính của Resource trong Laravel.
2. Tại sao không trả thẳng Model?
Ở bài trước chúng ta có thể viết:
public function show(Post $post)
{
return $post;
}
Cách này rất nhanh.
Nhưng hãy tưởng tượng Model Post có thêm:
internal_flag
admin_note
some_internal_data
...
Nếu trả thẳng Model:
return $post;
chúng ta có thể vô tình đưa những dữ liệu không cần thiết ra API.
Ngoài ra frontend có thể cần cấu trúc:
{
"id": 1,
"title": "...",
"author": {
"id": 1,
"name": "Admin"
},
"category": {
"id": 2,
"name": "Laravel"
}
}
Trong khi Model lại có:
user_id
category_id
Resource cho phép chúng ta quyết định chính xác:
API sẽ trả về dữ liệu gì và dữ liệu được tổ chức như thế nào.
3. API Resource nằm ở đâu?
Laravel đặt Resource mặc định trong:
app/Http/Resources
Ví dụ:
app/
└── Http/
└── Resources/
└── PostResource.php
Resource thông thường kế thừa:
Illuminate\Http\Resources\Json\JsonResource
4. Tạo PostResource
Chạy:
php artisan make:resource PostResource
Laravel tạo:
app/Http/Resources/PostResource.php
File ban đầu có dạng:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class PostResource extends JsonResource
{
/**
* Transform the resource into an array.
*/
public function toArray(Request $request): array
{
return [
//
];
}
}
Đây chính là nơi chúng ta định nghĩa dữ liệu API.
5. toArray() là gì?
Phương thức quan trọng nhất của Resource là:
toArray()
Ví dụ:
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'title' => $this->title,
'slug' => $this->slug,
];
}
Nếu Post có:
id = 1
title = Laravel 13
slug = laravel-13
API sẽ trả:
{
"data": {
"id": 1,
"title": "Laravel 13",
"slug": "laravel-13"
}
}
Resource đã biến Model thành một cấu trúc JSON mà chúng ta kiểm soát được.
6. $this trong Resource là gì?
Đây là điểm người mới học rất dễ nhầm.
Trong:
class PostResource extends JsonResource
$this đại diện cho Resource đang bao bọc Model.
Ví dụ:
return [
'id' => $this->id,
'title' => $this->title,
];
thì:
$this->id
chính là:
$post->id
và:
$this->title
chính là:
$post->title
Có thể hình dung:
Post Model
↓
PostResource
↓
$this
↓
Dữ liệu của Post
7. Resource đầu tiên cho Blog CMS
Với cấu trúc posts hiện tại, chúng ta có thể viết:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class PostResource extends JsonResource
{
/**
* Transform the resource into an array.
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'title' => $this->title,
'slug' => $this->slug,
'excerpt' => $this->excerpt,
'image' => $this->image,
'status' => $this->status,
'published_at' => $this->published_at,
'view_count' => $this->view_count,
];
}
}
Bây giờ API không còn phụ thuộc vào việc Laravel tự động serialize toàn bộ Model.
Chúng ta đã định nghĩa rõ:
API trả:
id
title
slug
excerpt
image
status
published_at
view_count
8. Sử dụng Resource trong Controller
Trước đây:
public function show(Post $post)
{
return $post;
}
Bây giờ:
use App\Http\Resources\PostResource;
public function show(Post $post)
{
return new PostResource($post);
}
Kết quả:
{
"data": {
"id": 1,
"title": "Laravel 13",
"slug": "laravel-13",
"excerpt": "..."
}
}
Resource mặc định có lớp data ở ngoài khi trả một resource. Cách wrapping này là một phần của hệ thống API Resource của Laravel.
9. Resource Collection
Một Post:
new PostResource($post)
Nhưng danh sách nhiều Post thì sao?
Chúng ta có thể sử dụng:
PostResource::collection($posts)
Ví dụ:
public function index()
{
$posts = Post::published()
->latest('published_at')
->get();
return PostResource::collection($posts);
}
Kết quả:
{
"data": [
{
"id": 1,
"title": "Laravel 13"
},
{
"id": 2,
"title": "REST API"
}
]
}
Đây là cách rất phổ biến khi trả danh sách Model thông qua Resource.
10. PostResource::collection()
Không cần phải tạo một Controller riêng cho collection.
Chỉ cần:
PostResource::collection($posts)
Laravel sẽ áp dụng:
PostResource
cho từng Post.
Ví dụ:
posts
├── Post #1
├── Post #2
└── Post #3
↓
PostResource
↓
PostResource
↓
PostResource
↓
JSON
11. API Resource với Pagination
Đây là trường hợp rất quan trọng đối với Blog CMS.
Ở Bài 41 chúng ta đã có:
$posts = Post::published()
->latest('published_at')
->paginate(10);
Bây giờ có thể:
return PostResource::collection($posts);
Laravel sẽ xử lý collection phân trang và response vẫn có thông tin pagination.
Ví dụ cấu trúc có thể gồm:
{
"data": [
{
"id": 1,
"title": "Laravel 13"
}
],
"links": {
"first": "...",
"last": "...",
"prev": null,
"next": "..."
},
"meta": {
"current_page": 1,
"from": 1,
"last_page": 5,
"per_page": 10,
"to": 10,
"total": 50
}
}
Laravel paginator vốn đã hỗ trợ chuyển kết quả phân trang thành JSON và cung cấp thông tin meta, links và data.
12. Resource + Relationship
Blog CMS của chúng ta có:
User
│
└── Posts
Category
│
└── Posts
Post
├── User
└── Category
Trong Post Model:
public function user()
{
return $this->belongsTo(User::class);
}
public function category()
{
return $this->belongsTo(Category::class);
}
API có thể muốn trả:
{
"id": 1,
"title": "Laravel 13",
"author": {
"id": 1,
"name": "Admin"
},
"category": {
"id": 2,
"name": "Laravel"
}
}
Resource rất phù hợp để xử lý trường hợp này.
13. Tạo UserResource
Chạy:
php artisan make:resource UserResource
File:
app/Http/Resources/UserResource.php
Viết:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class UserResource extends JsonResource
{
/**
* Transform the resource into an array.
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
];
}
}
14. Tạo CategoryResource
Chạy:
php artisan make:resource CategoryResource
Viết:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class CategoryResource extends JsonResource
{
/**
* Transform the resource into an array.
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'slug' => $this->slug,
];
}
}
Bây giờ chúng ta có:
app/Http/Resources/
PostResource.php
UserResource.php
CategoryResource.php
15. Đưa Relationship vào PostResource
PostResource.php:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class PostResource extends JsonResource
{
/**
* Transform the resource into an array.
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'title' => $this->title,
'slug' => $this->slug,
'excerpt' => $this->excerpt,
'image' => $this->image,
'published_at' => $this->published_at,
'view_count' => $this->view_count,
'author' => new UserResource($this->whenLoaded('user')),
'category' => new CategoryResource(
$this->whenLoaded('category')
),
];
}
}
Đây là một kỹ thuật rất quan trọng:
$this->whenLoaded('user')
và:
$this->whenLoaded('category')
16. whenLoaded() là gì?
Giả sử Controller:
$posts = Post::with(['user', 'category'])->get();
thì:
user
category
đã được load.
Resource có thể đưa chúng vào JSON.
Nhưng nếu Controller không load relationship:
$posts = Post::all();
thì Resource không nhất thiết phải truy vấn relationship một cách vô điều kiện.
Đó là lý do:
$this->whenLoaded('user')
rất hữu ích.
Có thể hiểu:
Relationship đã load?
│
┌───┴───┐
│ │
YES NO
│ │
trả về bỏ qua
17. Eager Loading + Resource
Đây là cách chúng ta nên viết:
public function index()
{
$posts = Post::published()
->with(['user', 'category'])
->latest('published_at')
->paginate(10);
return PostResource::collection($posts);
}
Luồng:
Database
↓
Post
↓
with(user, category)
↓
PostResource
↓
JSON
Resource chịu trách nhiệm định dạng.
Eloquent chịu trách nhiệm lấy dữ liệu.
Controller chịu trách nhiệm điều phối.
18. Tách nhiệm vụ giữa các tầng
Đây là một nguyên tắc rất quan trọng.
Model
Quan hệ
Scope
Database logic
Controller
Nhận request
Gọi Model
Gọi Resource
Resource
Định dạng dữ liệu API
Database
Lưu trữ dữ liệu
Có thể hình dung:
Request
↓
Controller
↓
Model
↓
Database
↑
Model
↓
Resource
↓
JSON Response
19. API Resource không phải Model
Đừng nhầm:
Post
với:
PostResource
Post:
use App\Models\Post;
là Eloquent Model.
Nó đại diện cho dữ liệu và logic liên quan đến bảng:
posts
Trong khi:
use App\Http\Resources\PostResource;
là lớp dùng để biến Post thành API response.
Post
↓
PostResource
↓
JSON
20. Resource cho API Detail
Ví dụ Controller:
public function show(Post $post)
{
$post->load(['user', 'category']);
return new PostResource($post);
}
API:
GET /api/posts/1
Có thể trả:
{
"data": {
"id": 1,
"title": "Laravel 13",
"slug": "laravel-13",
"excerpt": "Tìm hiểu Laravel 13",
"image": "posts/laravel-13.jpg",
"published_at": "2026-09-10T10:00:00Z",
"view_count": 120,
"author": {
"id": 1,
"name": "Admin"
},
"category": {
"id": 2,
"name": "Laravel",
"slug": "laravel"
}
}
}
Đây đã gần với một API thực tế hơn rất nhiều.
21. Resource cho danh sách Posts
Controller:
public function index()
{
$posts = Post::published()
->with(['user', 'category'])
->latest('published_at')
->paginate(10);
return PostResource::collection($posts);
}
API:
GET /api/posts
Response:
{
"data": [
{
"id": 1,
"title": "Laravel 13",
"slug": "laravel-13",
"author": {
"id": 1,
"name": "Admin"
},
"category": {
"id": 2,
"name": "Laravel",
"slug": "laravel"
}
}
]
}
22. Không trả password
Đây là một nguyên tắc bảo mật rất quan trọng.
Ví dụ User Model có:
name
email
password
remember_token
API public không nên trả:
{
"name": "Admin",
"email": "admin@example.com",
"password": "..."
}
UserResource chỉ nên định nghĩa những trường cần thiết:
return [
'id' => $this->id,
'name' => $this->name,
];
Resource vì vậy cũng là một lớp giúp kiểm soát dữ liệu được public.
23. Resource có thể đổi tên field
Database:
view_count
Frontend có thể muốn:
views
Resource:
return [
'id' => $this->id,
'title' => $this->title,
'views' => $this->view_count,
];
JSON:
{
"id": 1,
"title": "Laravel 13",
"views": 120
}
Database không cần đổi.
API cũng không cần expose tên column giống database.
24. Resource có thể tạo field mới
Ví dụ muốn API trả:
is_published
Trong Resource:
'is_published' => $this->status === 'published',
Kết quả:
{
"id": 1,
"title": "Laravel 13",
"status": "published",
"is_published": true
}
is_published không nhất thiết phải là một column trong database.
Resource có thể tạo ra field phục vụ API.
25. Resource có thể tạo URL
Ví dụ Post có:
image
Database lưu:
posts/abc.jpg
Nhưng frontend cần URL hoàn chỉnh.
Có thể xử lý:
'image_url' => $this->image
? asset('storage/' . $this->image)
: null,
Response:
{
"id": 1,
"title": "Laravel 13",
"image_url": "http://127.0.0.1:8000/storage/posts/abc.jpg"
}
Trong project thực tế, cách tạo URL nên thống nhất với hệ thống Storage mà chúng ta đã học ở Bài 39 — Storage.
26. Resource và Conditional Fields
Đôi khi một field chỉ nên xuất hiện trong một số trường hợp.
Ví dụ:
'admin_note' => $this->when(
$request->user()?->is_admin,
$this->admin_note
),
Ý tưởng:
User bình thường
↓
không có admin_note
Admin
↓
có admin_note
Đây là một ví dụ về conditional attributes trong Resource.
Trong những API lớn, kỹ thuật này giúp response linh hoạt hơn.
27. Resource và whenLoaded()
Tương tự relationship:
'author' => new UserResource(
$this->whenLoaded('user')
),
Nếu user được eager load:
Post::with('user')
thì API có author.
Nếu không:
Post::query()
thì field relationship có thể không được đưa vào response.
Điều này giúp tránh việc Resource tự động truy cập relationship trong mọi trường hợp.
28. API Resource + Scope
Blog CMS của chúng ta đã có:
Post::published()
Do đó Controller:
public function index()
{
$posts = Post::published()
->with(['user', 'category'])
->latest('published_at')
->paginate(10);
return PostResource::collection($posts);
}
Đây là một kiến trúc rất đẹp:
Scope
↓
lọc dữ liệu
Eager Loading
↓
lấy relationship
Resource
↓
định dạng JSON
Mỗi tầng có một nhiệm vụ.
29. Hoàn chỉnh PostResource cho Blog CMS
Đây là phiên bản chúng ta có thể sử dụng trong project:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class PostResource extends JsonResource
{
/**
* Transform the resource into an array.
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'title' => $this->title,
'slug' => $this->slug,
'excerpt' => $this->excerpt,
'image' => $this->image,
'status' => $this->status,
'published_at' => $this->published_at,
'view_count' => $this->view_count,
'author' => new UserResource(
$this->whenLoaded('user')
),
'category' => new CategoryResource(
$this->whenLoaded('category')
),
];
}
}
30. Hoàn chỉnh API Controller
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Resources\PostResource;
use App\Models\Post;
use Illuminate\Http\Request;
class PostController extends Controller
{
public function index()
{
$posts = Post::published()
->with(['user', 'category'])
->latest('published_at')
->paginate(10);
return PostResource::collection($posts);
}
public function show(Post $post)
{
$post->load(['user', 'category']);
return new PostResource($post);
}
public function store(Request $request)
{
$validated = $request->validate([
'title' => ['required', 'string', 'max:255'],
'slug' => ['required', 'string', 'max:255'],
'content' => ['required', 'string'],
]);
$post = Post::create([
...$validated,
'user_id' => $request->user()->id,
]);
return (new PostResource($post))
->response()
->setStatusCode(201);
}
public function update(Request $request, Post $post)
{
$validated = $request->validate([
'title' => ['required', 'string', 'max:255'],
'slug' => ['required', 'string', 'max:255'],
'content' => ['required', 'string'],
]);
$post->update($validated);
return new PostResource($post);
}
public function destroy(Post $post)
{
$post->delete();
return response()->json([
'message' => 'Post deleted successfully'
]);
}
}
31. Route
routes/api.php:
<?php
use App\Http\Controllers\Api\PostController;
use Illuminate\Support\Facades\Route;
Route::apiResource('posts', PostController::class);
Bây giờ:
GET /api/posts
GET /api/posts/{post}
POST /api/posts
PUT /api/posts/{post}
PATCH /api/posts/{post}
DELETE /api/posts/{post}
Vẫn là REST API như Bài 41.
Nhưng response bây giờ đi qua:
PostResource
Laravel hỗ trợ Route::apiResource() để tạo resource routes cho API và loại bỏ các route create / edit vốn phục vụ giao diện HTML.
32. Resource Collection riêng có cần thiết không?
Trong nhiều trường hợp:
PostResource::collection($posts)
là đủ.
Không cần tạo:
PostCollection.php
Tuy nhiên Laravel cũng cho phép tạo Resource Collection riêng:
php artisan make:resource Posts --collection
Khi đó có thể có:
app/Http/Resources/Posts.php
hoặc tên collection mà chúng ta chỉ định.
Resource Collection phù hợp khi muốn thêm logic hoặc metadata ở cấp toàn bộ collection, thay vì từng item.
Người mới học API chưa cần lạm dụng phần này.
33. Resource và API Resource khác nhau như thế nào?
Có thể gặp hai cách gọi:
API Resource
và:
JsonResource
Trong bài học cơ bản:
API Resource
↓
JsonResource
↓
PostResource
Ví dụ:
class PostResource extends JsonResource
Đây là loại Resource chúng ta đang sử dụng để biến Model thành JSON.
34. Laravel 13 có điểm mới liên quan Resource
Laravel 13 hiện còn có thêm:
JSON:API Resources
Đây là khả năng hỗ trợ response theo chuẩn JSON:API, bao gồm resource object serialization, relationships, sparse fieldsets, links và các header phù hợp với JSON:API.
Điều này rất đáng chú ý vì Laravel 13 tiếp tục mở rộng khả năng xây dựng API.
Tuy nhiên:
Bài 42
↓
JsonResource
↓
Hiểu Resource trước
là hướng học phù hợp hơn cho người mới.
Sau khi đã hiểu:
Model
↓
Resource
↓
JSON
thì việc học các chuẩn API nâng cao sẽ dễ dàng hơn.
35. API Resource và Frontend
Giả sử frontend React cần:
{
"id": 1,
"title": "Laravel 13",
"author": {
"id": 1,
"name": "Admin"
}
}
Laravel Resource đảm bảo cấu trúc này ổn định.
Frontend không cần biết:
posts.user_id
posts.category_id
được lưu như thế nào trong database.
Frontend chỉ cần biết API contract:
GET /api/posts/1
trả về:
data.id
data.title
data.author
data.category
Đây là một lợi ích lớn của việc tách:
Database structure
khỏi:
API response structure
36. API Resource giống một "bộ lọc"
Có thể tưởng tượng:
DATABASE
│
▼
Post Model
│
▼
┌──────────────┐
│ PostResource │
└──────────────┘
│
┌────────┴────────┐
│ │
Giữ lại Bỏ đi
│ │
▼ ▼
title internal data
slug password
image dữ liệu không cần
author
category
│
▼
JSON
Đây là cách rất dễ nhớ:
Model lấy dữ liệu — Resource quyết định dữ liệu nào được đưa ra API.
37. Một lỗi thường gặp
Nhiều người viết:
public function index()
{
return Post::all();
}
và nghĩ:
API chạy rồi là xong.
Đúng ở mức demo.
Nhưng API thực tế còn phải quan tâm:
Response structure
Security
Relationships
Pagination
Validation
Authentication
Authorization
Versioning
Performance
Resource giải quyết một phần rất quan trọng trong số đó:
Response structure
38. Một lỗi khác: Load quá nhiều relationship
Ví dụ:
Post::with([
'user',
'category',
'tags',
'comments',
'comments.user',
'comments.replies'
])->get();
Resource có thể trả được rất nhiều dữ liệu.
Nhưng không có nghĩa là chúng ta nên trả tất cả.
API nên trả đúng dữ liệu client cần.
Ví dụ:
Post List
chỉ cần:
id
title
slug
image
category
Không nhất thiết phải tải:
comments
replies
author details
tags
...
Đây là vấn đề thiết kế API và hiệu năng.
39. API Resource trong Blog CMS
Kiến trúc hiện tại:
BLOG CMS
│
┌──────────────┴──────────────┐
│ │
Website API
│ │
web.php api.php
│ │
BlogController PostController
│ │
Blade PostResource
│ │
HTML JSON
│ │
└──────────────┬──────────────┘
│
Eloquent
│
MySQL
Chúng ta đã có một kiến trúc backend khá hoàn chỉnh.
40. Bài tập thực hành
Bài tập 1 — Tạo PostResource
Chạy:
php artisan make:resource PostResource
Tạo:
app/Http/Resources/PostResource.php
Bài tập 2 — Chỉ trả dữ liệu cần thiết
Resource chỉ trả:
id
title
slug
excerpt
image
published_at
view_count
Không trả toàn bộ Model.
Bài tập 3 — Tạo UserResource
php artisan make:resource UserResource
Chỉ trả:
id
name
Bài tập 4 — Tạo CategoryResource
php artisan make:resource CategoryResource
Trả:
id
name
slug
Bài tập 5 — Relationship
Trong PostResource:
'author' => new UserResource(
$this->whenLoaded('user')
),
'category' => new CategoryResource(
$this->whenLoaded('category')
),
Bài tập 6 — Pagination
API:
GET /api/posts
phải sử dụng:
paginate(10)
và:
PostResource::collection($posts)
Quan sát:
data
links
meta
41. Ghi nhớ
API Resource là gì?
Một lớp trung gian dùng để biến Eloquent Model thành dữ liệu JSON theo cấu trúc mà API mong muốn.
Tạo Resource:
php artisan make:resource PostResource
Resource nằm tại:
app/Http/Resources/
Resource kế thừa:
JsonResource
Định nghĩa dữ liệu:
public function toArray(Request $request): array
Một Model:
return new PostResource($post);
Một collection:
return PostResource::collection($posts);
Relationship:
$this->whenLoaded('user')
42. REST API và Resource
Hãy nhớ sự khác nhau:
BÀI 41
REST API
↓
Endpoint
HTTP Method
JSON
Controller
CRUD
và:
BÀI 42
API Resource
↓
Model
↓
Resource
↓
JSON
Hay đơn giản hơn:
Bài 41:
"Làm API như thế nào?"
Bài 42:
"API nên trả dữ liệu như thế nào?"
🎯 Tiếp theo — Bài 43
Chúng ta đã có:
REST API
↓
API Resource
↓
JSON
Nhưng hiện tại API vẫn chưa giải quyết một câu hỏi quan trọng:
Làm sao xác định ai đang gọi API?
Ví dụ:
GET /api/posts
có thể là API public.
Nhưng:
POST /api/posts
PUT /api/posts/1
DELETE /api/posts/1
thì cần xác thực người dùng.
Đây chính là lúc chúng ta bước sang:
🔐 BÀI 43 — SANCTUM
Chúng ta sẽ tìm hiểu:
Laravel Sanctum
↓
API Token
↓
Authentication
↓
Protected API
và từ đó hoàn thiện phần API Authentication ở Bài 44.




Không có nhận xét nào:
Đăng nhận xét