Yii2 RESTful API Development

Phase 4 và 5 đã trang bị task manager với authentication, RBAC, và complete form system. Nhưng forms phục vụ browsers. Mobile apps, SPAs, third-party integrations — tất cả nói chuyện qua REST. Và build REST APIs từ đầu rất nhàm chán: routing, serialization, pagination, error formatting, authentication, rate limiting, CORS. Yii2 ship sẵn tất cả.
Với ActiveController, bạn có CRUD API hoàn chỉnh trong chưa đến 20 dòng code. Với yii\rest\Controller, bạn có toàn quyền kiểm soát khi defaults không đủ. Cả hai đều thừa hưởng authentication filters, rate limiting, và content negotiation tự động.
Kết thúc bài này, task manager sẽ có complete REST API với token authentication, controlled field exposure, pagination, rate limiting, versioning, và proper CORS headers.
Bạn Sẽ Build Gì
✅ ActiveController cho instant CRUD APIs không cần boilerplate
✅ Custom REST controllers cho non-standard endpoints
✅ Field serialization với fields() và extraFields()
✅ Pagination, sorting, và filtering
✅ Bearer token authentication với HttpBearerAuth
✅ Rate limiting với RateLimiterInterface
✅ API versioning (URL prefix strategy)
✅ CORS configuration cho cross-origin requests
1. REST API Đầu Tiên Trong 5 Phút
Cấu Hình URL Manager Cho Pretty URLs
REST APIs cần clean URLs kiểu /api/tasks/42 thay vì ?r=task/view&id=42. Thêm REST-specific URL rules:
// config/web.php
'components' => [
'urlManager' => [
'enablePrettyUrl' => true,
'showScriptName' => false,
'enableStrictParsing' => false,
'rules' => [
// REST rules cho tasks
['class' => 'yii\rest\UrlRule', 'controller' => 'api/task'],
],
],
'request' => [
'parsers' => [
'application/json' => 'yii\web\JsonParser',
],
],
],JsonParser rất quan trọng — không có nó, Yii2 không đọc được JSON request bodies (Content-Type: application/json).
yii\rest\UrlRule Generate Những Gì
Một dòng duy nhất tạo tất cả standard REST routes:
| HTTP Method | URL | Action | Mô tả |
|---|---|---|---|
GET | /api/tasks | index | Liệt kê tất cả tasks |
GET | /api/tasks/42 | view | Lấy task #42 |
POST | /api/tasks | create | Tạo task |
PUT | /api/tasks/42 | update | Cập nhật task #42 |
PATCH | /api/tasks/42 | update | Cập nhật một phần task #42 |
DELETE | /api/tasks/42 | delete | Xóa task #42 |
OPTIONS | /api/tasks | options | CORS preflight |
HEAD | /api/tasks | index | Chỉ headers |
Chú ý nó tự động chuyển task → tasks trong URL.
Tạo Controller
// controllers/api/TaskController.php
namespace app\controllers\api;
use yii\rest\ActiveController;
class TaskController extends ActiveController
{
public $modelClass = 'app\models\Task';
}Chỉ vậy thôi. Hai dòng code có ý nghĩa và bạn đã có CRUD API hoàn chỉnh. ActiveController cung cấp index, view, create, update, và delete actions — tất cả kết nối tới Task Active Record model.
Test Thử
# Liệt kê tất cả tasks
curl -s http://localhost:8080/api/tasks | jq
# Tạo task
curl -s -X POST http://localhost:8080/api/tasks \
-H "Content-Type: application/json" \
-d '{"title": "Learn Yii2 REST", "status": 1}' | jq
# Lấy một task
curl -s http://localhost:8080/api/tasks/1 | jq
# Cập nhật task
curl -s -X PUT http://localhost:8080/api/tasks/1 \
-H "Content-Type: application/json" \
-d '{"status": 3}' | jq
# Xóa task
curl -s -X DELETE http://localhost:8080/api/tasks/1
# Trả về 204 No ContentDefault Response Format
ActiveController tự động trả về JSON:
// GET /api/tasks
[
{
"id": 1,
"title": "Learn Yii2 REST",
"description": null,
"status": 1,
"priority": 2,
"user_id": 1,
"created_at": "2026-03-18 10:30:00",
"updated_at": "2026-03-18 10:30:00"
}
]Vấn đề: nó expose mọi column, bao gồm user_id, created_at, và updated_at. Chúng ta sẽ fix bằng serialization ở phần 3.
2. Custom REST Controllers
ActiveController tuyệt vời cho standard CRUD, nhưng API thực tế cần custom actions — search, bulk operations, statistics, actions span nhiều models. Cho những trường hợp này, extend yii\rest\Controller trực tiếp.
Tắt Default Actions
Đầu tiên, customize ActiveController bằng cách tắt hoặc override actions:
// controllers/api/TaskController.php
namespace app\controllers\api;
use yii\rest\ActiveController;
class TaskController extends ActiveController
{
public $modelClass = 'app\models\Task';
public function actions()
{
$actions = parent::actions();
// Bỏ delete action — ta dùng soft-delete thay thế
unset($actions['delete']);
// Customize index action's query
$actions['index']['prepareDataProvider'] = [$this, 'prepareDataProvider'];
return $actions;
}
public function prepareDataProvider()
{
$query = \app\models\Task::find()
->where(['user_id' => \Yii::$app->user->id])
->andWhere(['!=', 'status', \app\models\Task::STATUS_DELETED]);
return new \yii\data\ActiveDataProvider([
'query' => $query,
'sort' => [
'defaultOrder' => ['created_at' => SORT_DESC],
],
'pagination' => [
'pageSize' => 20,
],
]);
}
}Thêm Custom Actions
class TaskController extends ActiveController
{
public $modelClass = 'app\models\Task';
public function actions()
{
$actions = parent::actions();
unset($actions['delete']);
return $actions;
}
/**
* POST /api/tasks/42/complete
*/
public function actionComplete($id)
{
$task = $this->findModel($id);
$task->status = Task::STATUS_DONE;
$task->completed_at = date('Y-m-d H:i:s');
if ($task->save()) {
return $task;
}
return $task; // validation errors tự động serialize
}
/**
* GET /api/tasks/stats
*/
public function actionStats()
{
$userId = \Yii::$app->user->id;
return [
'total' => Task::find()->where(['user_id' => $userId])->count(),
'todo' => Task::find()->where([
'user_id' => $userId,
'status' => Task::STATUS_TODO,
])->count(),
'in_progress' => Task::find()->where([
'user_id' => $userId,
'status' => Task::STATUS_IN_PROGRESS,
])->count(),
'done' => Task::find()->where([
'user_id' => $userId,
'status' => Task::STATUS_DONE,
])->count(),
];
}
/**
* POST /api/tasks/bulk-update
*/
public function actionBulkUpdate()
{
$request = \Yii::$app->request;
$ids = $request->post('ids', []);
$status = $request->post('status');
if (empty($ids) || $status === null) {
throw new \yii\web\BadRequestHttpException(
'ids and status are required.'
);
}
$count = Task::updateAll(
['status' => $status],
['id' => $ids, 'user_id' => \Yii::$app->user->id]
);
return ['updated' => $count];
}
protected function findModel($id)
{
$model = Task::findOne([
'id' => $id,
'user_id' => \Yii::$app->user->id,
]);
if ($model === null) {
throw new \yii\web\NotFoundHttpException("Task #$id not found.");
}
return $model;
}
}Custom URL Rules Cho Non-Standard Actions
Custom actions cần explicit URL rules:
// config/web.php
'rules' => [
// Custom actions trước (thứ tự quan trọng — match đầu tiên thắng)
'POST api/tasks/<id:\d+>/complete' => 'api/task/complete',
'GET api/tasks/stats' => 'api/task/stats',
'POST api/tasks/bulk-update' => 'api/task/bulk-update',
// Standard REST rules sau
['class' => 'yii\rest\UrlRule', 'controller' => 'api/task'],
],Build Từ yii\rest\Controller
Khi bạn không muốn default CRUD actions nào:
// controllers/api/AuthController.php
namespace app\controllers\api;
use yii\rest\Controller;
class AuthController extends Controller
{
/**
* Không cần authentication cho login/register.
*/
public function behaviors()
{
$behaviors = parent::behaviors();
unset($behaviors['authenticator']);
return $behaviors;
}
/**
* POST /api/auth/login
*/
public function actionLogin()
{
$request = \Yii::$app->request;
$email = $request->post('email');
$password = $request->post('password');
$user = \app\models\User::findOne(['email' => $email]);
if (!$user || !$user->validatePassword($password)) {
throw new \yii\web\UnauthorizedHttpException(
'Invalid email or password.'
);
}
// Generate access token mới
$user->access_token = \Yii::$app->security->generateRandomString(32);
$user->save(false);
return [
'token' => $user->access_token,
'user' => [
'id' => $user->id,
'username' => $user->username,
'email' => $user->email,
],
];
}
/**
* POST /api/auth/register
*/
public function actionRegister()
{
$model = new \app\models\User();
$model->scenario = \app\models\User::SCENARIO_REGISTER;
if ($model->load(\Yii::$app->request->post(), '') && $model->validate()) {
$model->setPassword($model->password);
$model->access_token = \Yii::$app->security->generateRandomString(32);
$model->save(false);
\Yii::$app->response->statusCode = 201;
return [
'token' => $model->access_token,
'user' => [
'id' => $model->id,
'username' => $model->username,
'email' => $model->email,
],
];
}
return $model;
}
}3. Serialization — Kiểm Soát API Output
Mặc định, Yii2 serialize mọi model attribute. Trong production, bạn muốn kiểm soát chính xác fields nào được expose và cho phép clients request related data theo yêu cầu.
fields() — Default Fields
Override fields() trên model để định nghĩa set fields mặc định trả về:
// models/Task.php
class Task extends ActiveRecord
{
public function fields()
{
return [
'id',
'title',
'description',
'status',
'status_label' => function ($model) {
return $model->getStatusLabel();
},
'priority',
'due_date',
];
}
}Giờ GET /api/tasks/1 trả về:
{
"id": 1,
"title": "Learn Yii2 REST",
"description": "Build a complete REST API",
"status": 1,
"status_label": "To Do",
"priority": 2,
"due_date": "2026-04-01"
}Chú ý: user_id, created_at, và updated_at bị ẩn. status_label là computed field.
extraFields() — Expandable Relations
extraFields() định nghĩa fields mà clients có thể opt-in qua query parameter expand:
// models/Task.php
class Task extends ActiveRecord
{
public function fields()
{
return ['id', 'title', 'description', 'status', 'priority', 'due_date'];
}
public function extraFields()
{
return ['user', 'comments', 'attachments'];
}
public function getUser()
{
return $this->hasOne(User::class, ['id' => 'user_id']);
}
public function getComments()
{
return $this->hasMany(Comment::class, ['task_id' => 'id']);
}
public function getAttachments()
{
return $this->hasMany(Attachment::class, ['task_id' => 'id']);
}
}# Mặc định — không relations
GET /api/tasks/1
# Expand user info
GET /api/tasks/1?expand=user
# Expand nhiều relations
GET /api/tasks/1?expand=user,comments
# Chọn specific fields + expand
GET /api/tasks/1?fields=id,title,status&expand=userResponse với ?expand=user:
{
"id": 1,
"title": "Learn Yii2 REST",
"description": "Build a complete REST API",
"status": 1,
"priority": 2,
"due_date": "2026-04-01",
"user": {
"id": 1,
"username": "chanh",
"email": "[email protected]"
}
}Kiểm Soát User Serialization Luôn
Model User cũng nên kiểm soát output:
// models/User.php
class User extends ActiveRecord
{
public function fields()
{
return [
'id',
'username',
'email',
];
// password_hash, access_token, v.v. KHÔNG BAO GIỜ expose
}
}Component Serializer
Serializer REST của Yii2 xử lý chuyển đổi từ models/data providers sang arrays. Bạn có thể customize:
// controllers/api/TaskController.php
class TaskController extends ActiveController
{
public $modelClass = 'app\models\Task';
// Dùng Serializer với envelope cho collections
public $serializer = [
'class' => 'yii\rest\Serializer',
'collectionEnvelope' => 'items',
'metaEnvelope' => 'meta',
];
}Response mặc định của serializer cho collections:
// Không envelope (mặc định)
[
{"id": 1, "title": "Task 1"},
{"id": 2, "title": "Task 2"}
]
// Pagination info nằm trong HTTP headers (X-Pagination-*)
// Với collectionEnvelope
{
"items": [
{"id": 1, "title": "Task 1"},
{"id": 2, "title": "Task 2"}
],
"meta": {
"totalCount": 42,
"pageCount": 3,
"currentPage": 1,
"perPage": 20
}
}4. Pagination, Sorting & Filtering
Automatic Pagination
ActiveController::actionIndex() trả về ActiveDataProvider, tự động xử lý pagination. Clients điều khiển qua query parameters:
# Trang 2, 10 items mỗi trang
GET /api/tasks?page=2&per-page=10Pagination info xuất hiện trong response headers:
X-Pagination-Total-Count: 42
X-Pagination-Page-Count: 5
X-Pagination-Current-Page: 2
X-Pagination-Per-Page: 10
Link: <http://localhost/api/tasks?page=1&per-page=10>; rel="first",
<http://localhost/api/tasks?page=5&per-page=10>; rel="last",
<http://localhost/api/tasks?page=1&per-page=10>; rel="prev",
<http://localhost/api/tasks?page=3&per-page=10>; rel="next"Sorting
Bật sorting trong data provider:
public function prepareDataProvider()
{
return new \yii\data\ActiveDataProvider([
'query' => Task::find()->where(['user_id' => \Yii::$app->user->id]),
'sort' => [
'attributes' => ['id', 'title', 'status', 'priority', 'created_at'],
'defaultOrder' => ['created_at' => SORT_DESC],
],
'pagination' => [
'pageSize' => 20,
'pageSizeLimit' => [1, 100], // min/max per-page
],
]);
}# Sort theo priority giảm dần
GET /api/tasks?sort=-priority
# Sort theo status tăng dần, rồi created_at giảm dần
GET /api/tasks?sort=status,-created_atPrefix - nghĩa là descending order.
Filtering Với Query Parameters
Implement filtering trong prepareDataProvider:
public function prepareDataProvider()
{
$request = \Yii::$app->request;
$query = Task::find()->where(['user_id' => \Yii::$app->user->id]);
// Filter theo status
$status = $request->get('status');
if ($status !== null) {
$query->andWhere(['status' => $status]);
}
// Filter theo priority
$priority = $request->get('priority');
if ($priority !== null) {
$query->andWhere(['priority' => $priority]);
}
// Tìm kiếm trong title
$search = $request->get('q');
if ($search) {
$query->andWhere(['like', 'title', $search]);
}
// Date range filter
$from = $request->get('from');
$to = $request->get('to');
if ($from) {
$query->andWhere(['>=', 'due_date', $from]);
}
if ($to) {
$query->andWhere(['<=', 'due_date', $to]);
}
return new \yii\data\ActiveDataProvider([
'query' => $query,
'sort' => [
'attributes' => ['id', 'title', 'status', 'priority', 'created_at', 'due_date'],
'defaultOrder' => ['created_at' => SORT_DESC],
],
]);
}# Tasks đang "in progress" với high priority
GET /api/tasks?status=2&priority=3
# Tìm tasks chứa "deploy"
GET /api/tasks?q=deploy
# Tasks đến hạn tuần này
GET /api/tasks?from=2026-03-18&to=2026-03-24
# Kết hợp: search + filter + sort + paginate
GET /api/tasks?q=deploy&status=1&sort=-priority&page=1&per-page=5Dùng DataFilter Cho Complex Filtering
Cho filtering nâng cao hơn, Yii2 cung cấp DataFilter:
use yii\data\ActiveDataFilter;
public function prepareDataProvider()
{
$filter = new ActiveDataFilter([
'searchModel' => 'app\models\TaskSearch',
]);
$filterCondition = null;
if ($filter->load(\Yii::$app->request->get())) {
$filterCondition = $filter->build();
if ($filterCondition === false) {
return $filter;
}
}
$query = Task::find()->where(['user_id' => \Yii::$app->user->id]);
if ($filterCondition !== null) {
$query->andWhere($filterCondition);
}
return new \yii\data\ActiveDataProvider([
'query' => $query,
]);
}# Filter với operators
GET /api/tasks?filter[status]=1
GET /api/tasks?filter[priority][gte]=2
GET /api/tasks?filter[due_date][lt]=2026-04-015. Authentication — Bearer Tokens
REST APIs là stateless — không sessions, không cookies. Cách tiếp cận standard là Bearer token authentication: client gửi token trong Authorization header mỗi request.
Cấu Hình User Model
Model User cần findIdentityByAccessToken() từ IdentityInterface:
// models/User.php
class User extends ActiveRecord implements \yii\web\IdentityInterface
{
// ... existing IdentityInterface methods ...
public static function findIdentityByAccessToken($token, $type = null)
{
return static::findOne(['access_token' => $token]);
}
/**
* Generate access token mới.
*/
public function generateAccessToken()
{
$this->access_token = \Yii::$app->security->generateRandomString(32);
}
}Thêm Auth Behavior Vào Controller
// controllers/api/TaskController.php
namespace app\controllers\api;
use yii\rest\ActiveController;
use yii\filters\auth\HttpBearerAuth;
class TaskController extends ActiveController
{
public $modelClass = 'app\models\Task';
public function behaviors()
{
$behaviors = parent::behaviors();
// Thêm Bearer token authentication
$behaviors['authenticator'] = [
'class' => HttpBearerAuth::class,
];
return $behaviors;
}
}Request Với Authentication
# Login để lấy token
curl -s -X POST http://localhost:8080/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email": "[email protected]", "password": "secret123"}' | jq
# Response:
# {
# "token": "abc123def456...",
# "user": { "id": 1, "username": "chanh", "email": "[email protected]" }
# }
# Dùng token cho authenticated requests
curl -s http://localhost:8080/api/tasks \
-H "Authorization: Bearer abc123def456..." | jq
# Không có token → 401 Unauthorized
curl -s http://localhost:8080/api/tasks | jq
# {
# "name": "Unauthorized",
# "message": "Your request was made with invalid credentials.",
# "code": 0,
# "status": 401
# }Nhiều Auth Methods
Hỗ trợ cả Bearer tokens (API clients) và session auth (same-domain JS):
use yii\filters\auth\HttpBearerAuth;
use yii\filters\auth\QueryParamAuth;
use yii\filters\auth\CompositeAuth;
public function behaviors()
{
$behaviors = parent::behaviors();
$behaviors['authenticator'] = [
'class' => CompositeAuth::class,
'authMethods' => [
HttpBearerAuth::class, // Authorization: Bearer <token>
QueryParamAuth::class, // ?access-token=<token>
],
];
return $behaviors;
}Optional Auth — Public + Private Endpoints
Một số actions nên hoạt động cả authenticated lẫn unauthenticated (ví dụ: public task list vs private tasks):
public function behaviors()
{
$behaviors = parent::behaviors();
$behaviors['authenticator'] = [
'class' => HttpBearerAuth::class,
'optional' => ['index', 'view'], // auth optional cho những actions này
];
return $behaviors;
}
public function prepareDataProvider()
{
$query = Task::find()->where(['is_public' => true]);
// Nếu đã authenticated, bao gồm cả private tasks của họ
if (!\Yii::$app->user->isGuest) {
$query->orWhere(['user_id' => \Yii::$app->user->id]);
}
return new \yii\data\ActiveDataProvider(['query' => $query]);
}Auth Flow Sequence
6. Rate Limiting
Rate limiting ngăn chặn API abuse. Yii2 có built-in support — User model chỉ cần implement RateLimiterInterface.
Implement RateLimiterInterface
// models/User.php
use yii\filters\RateLimitInterface;
class User extends ActiveRecord implements
\yii\web\IdentityInterface,
RateLimitInterface
{
/**
* Số requests tối đa cho phép.
*/
public function getRateLimit($request, $action)
{
// 100 requests mỗi 60 giây
return [100, 60];
}
/**
* Trả về remaining allowance hiện tại + last check time.
*/
public function loadAllowance($request, $action)
{
return [
$this->rate_limit_allowance ?? 100,
$this->rate_limit_allowance_updated_at ?? time(),
];
}
/**
* Lưu allowance + timestamp hiện tại.
*/
public function saveAllowance($request, $action, $allowance, $timestamp)
{
$this->rate_limit_allowance = $allowance;
$this->rate_limit_allowance_updated_at = $timestamp;
$this->save(false);
}
}Bạn cần hai columns trên bảng users:
// migrations/m260318_000001_add_rate_limit_to_users.php
$this->addColumn('{{%users}}', 'rate_limit_allowance', $this->integer());
$this->addColumn('{{%users}}', 'rate_limit_allowance_updated_at', $this->integer());Bật Rate Limiting Trong Controller
public function behaviors()
{
$behaviors = parent::behaviors();
$behaviors['authenticator'] = [
'class' => HttpBearerAuth::class,
];
$behaviors['rateLimiter'] = [
'class' => \yii\filters\RateLimiter::class,
'enableRateLimitHeaders' => true,
];
return $behaviors;
}Rate Limit Headers
Mỗi response bao gồm rate limit information:
X-Rate-Limit-Limit: 100
X-Rate-Limit-Remaining: 97
X-Rate-Limit-Reset: 42Khi vượt limit:
{
"name": "Too Many Requests",
"message": "Rate limit exceeded.",
"code": 0,
"status": 429
}Limits Khác Nhau Cho Mỗi Action
Cho kiểm soát chi tiết hơn, thay đổi limits theo action:
public function getRateLimit($request, $action)
{
// Write operations có limit thấp hơn
if (in_array($action->id, ['create', 'update', 'delete'])) {
return [20, 60]; // 20 writes mỗi phút
}
return [100, 60]; // 100 reads mỗi phút
}7. API Versioning
Khi API phát triển, breaking changes là không thể tránh khỏi. Versioning cho phép bạn phát triển API mà không phá hỏng existing clients. Strategy phổ biến và rõ ràng nhất là URL prefix versioning.
Cấu Trúc Thư Mục
controllers/
├── api/
│ └── v1/
│ ├── TaskController.php
│ └── UserController.php
│ └── v2/
│ ├── TaskController.php
│ └── UserController.phpVersion 1 Controller
// controllers/api/v1/TaskController.php
namespace app\controllers\api\v1;
use yii\rest\ActiveController;
use yii\filters\auth\HttpBearerAuth;
class TaskController extends ActiveController
{
public $modelClass = 'app\models\Task';
public function behaviors()
{
$behaviors = parent::behaviors();
$behaviors['authenticator'] = [
'class' => HttpBearerAuth::class,
];
return $behaviors;
}
}Version 2 Controller
V2 có thể dùng serialization khác, validation chặt hơn, hoặc response structure mới:
// controllers/api/v2/TaskController.php
namespace app\controllers\api\v2;
use yii\rest\ActiveController;
use yii\filters\auth\HttpBearerAuth;
class TaskController extends ActiveController
{
public $modelClass = 'app\models\Task';
// V2 wrap collections trong envelope
public $serializer = [
'class' => 'yii\rest\Serializer',
'collectionEnvelope' => 'data',
'metaEnvelope' => 'meta',
];
public function behaviors()
{
$behaviors = parent::behaviors();
$behaviors['authenticator'] = [
'class' => HttpBearerAuth::class,
];
return $behaviors;
}
public function actions()
{
$actions = parent::actions();
// V2 không hỗ trợ DELETE — dùng soft-delete qua PATCH
unset($actions['delete']);
return $actions;
}
}URL Rules Cho Versioned API
// config/web.php
'rules' => [
// V1 routes
['class' => 'yii\rest\UrlRule', 'controller' => 'api/v1/task',
'prefix' => 'api/v1'],
['class' => 'yii\rest\UrlRule', 'controller' => 'api/v1/user',
'prefix' => 'api/v1'],
// V2 routes
['class' => 'yii\rest\UrlRule', 'controller' => 'api/v2/task',
'prefix' => 'api/v2'],
// Custom actions cho mỗi version
'POST api/v1/tasks/<id:\d+>/complete' => 'api/v1/task/complete',
'POST api/v2/tasks/<id:\d+>/complete' => 'api/v2/task/complete',
],# V1 — flat array
GET /api/v1/tasks
# V2 — envelope format
GET /api/v2/tasks
# { "data": [...], "meta": { "totalCount": 42, ... } }Module-Based Versioning (Sạch Hơn)
Cho APIs lớn hơn, dùng Yii2 modules:
// config/web.php
'modules' => [
'v1' => [
'class' => 'app\modules\v1\Module',
],
'v2' => [
'class' => 'app\modules\v2\Module',
],
],Mỗi version có namespace, controllers, và configuration riêng.
8. CORS Configuration
Cross-Origin Resource Sharing (CORS) là bắt buộc khi API ở domain khác với frontend. Không có CORS headers, browsers sẽ block request.
Bật CORS Filter
// controllers/api/TaskController.php
use yii\filters\Cors;
use yii\filters\auth\HttpBearerAuth;
public function behaviors()
{
$behaviors = parent::behaviors();
// CORS phải đặt TRƯỚC authenticator
$behaviors['cors'] = [
'class' => Cors::class,
'cors' => [
'Origin' => [
'https://myapp.com',
'https://staging.myapp.com',
'http://localhost:3000', // dev
],
'Access-Control-Request-Method' => [
'GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS',
],
'Access-Control-Request-Headers' => ['*'],
'Access-Control-Allow-Credentials' => true,
'Access-Control-Max-Age' => 86400, // cache preflight 24h
'Access-Control-Expose-Headers' => [
'X-Pagination-Total-Count',
'X-Pagination-Page-Count',
'X-Pagination-Current-Page',
'X-Pagination-Per-Page',
'X-Rate-Limit-Limit',
'X-Rate-Limit-Remaining',
'X-Rate-Limit-Reset',
],
],
];
// Auth SAU cors
$behaviors['authenticator'] = [
'class' => HttpBearerAuth::class,
];
return $behaviors;
}Tại Sao Thứ Tự Quan Trọng
Nếu authenticator chạy trước CORS, preflight OPTIONS request (không có auth header) trả về 401, và browser không bao giờ gửi actual request.
Expose Pagination Headers
Mặc định, browsers chỉ đọc được vài "simple" response headers. Custom headers như X-Pagination-* và X-Rate-Limit-* phải được expose rõ ràng qua Access-Control-Expose-Headers, nếu không JavaScript frontend không đọc được.
9. Error Handling
REST framework của Yii2 tự động chuyển exceptions thành JSON error responses.
Standard Error Responses
// 400 Bad Request
{
"name": "Bad Request",
"message": "ids and status are required.",
"code": 0,
"status": 400
}
// 401 Unauthorized
{
"name": "Unauthorized",
"message": "Your request was made with invalid credentials.",
"code": 0,
"status": 401
}
// 404 Not Found
{
"name": "Not Found",
"message": "Task #999 not found.",
"code": 0,
"status": 404
}
// 422 Validation Error (model validation fails)
[
{
"field": "title",
"message": "Title cannot be blank."
},
{
"field": "email",
"message": "This email is already taken."
}
]
// 429 Too Many Requests
{
"name": "Too Many Requests",
"message": "Rate limit exceeded.",
"code": 0,
"status": 429
}Throwing Exceptions
Dùng HTTP exception classes của Yii2:
use yii\web\BadRequestHttpException;
use yii\web\NotFoundHttpException;
use yii\web\ForbiddenHttpException;
use yii\web\UnauthorizedHttpException;
use yii\web\ServerErrorHttpException;
// 400
throw new BadRequestHttpException('Invalid date format.');
// 403
throw new ForbiddenHttpException('You cannot edit this task.');
// 404
throw new NotFoundHttpException('Task not found.');
// 500
throw new ServerErrorHttpException('Failed to save task.');Validation Errors
Khi model fail validation, ActiveController tự động trả về 422 với error details. Bạn không cần làm gì đặc biệt — chỉ cần return model:
public function actionCreate()
{
$model = new Task();
$model->load(\Yii::$app->request->post(), '');
if (!$model->save()) {
// Return model với errors triggers 422 response
return $model;
}
\Yii::$app->response->statusCode = 201;
return $model;
}Custom Error Handler Cho APIs
Cho error format nhất quán trên toàn bộ API:
// config/web.php
'components' => [
'response' => [
'class' => 'yii\web\Response',
'on beforeSend' => function ($event) {
$response = $event->sender;
if ($response->data !== null && \Yii::$app->request->isAjax) {
// Đã được format đúng bởi REST controllers
return;
}
},
],
],10. Ghép Tất Cả Lại — Complete API Setup
Đây là full configuration cho REST API production-ready:
Configuration
// config/web.php
return [
'id' => 'task-manager',
'components' => [
'urlManager' => [
'enablePrettyUrl' => true,
'showScriptName' => false,
'rules' => [
// Auth routes (không cần authentication)
'POST api/v1/auth/login' => 'api/v1/auth/login',
'POST api/v1/auth/register' => 'api/v1/auth/register',
// Custom task actions
'POST api/v1/tasks/<id:\d+>/complete' => 'api/v1/task/complete',
'GET api/v1/tasks/stats' => 'api/v1/task/stats',
'POST api/v1/tasks/bulk-update' => 'api/v1/task/bulk-update',
// Standard REST routes
['class' => 'yii\rest\UrlRule',
'controller' => 'api/v1/task',
'prefix' => 'api/v1'],
],
],
'request' => [
'parsers' => [
'application/json' => 'yii\web\JsonParser',
],
],
'user' => [
'identityClass' => 'app\models\User',
'enableSession' => false, // Stateless
'loginUrl' => null, // Không redirect — trả về 401
],
],
];Base API Controller
// controllers/api/v1/BaseController.php
namespace app\controllers\api\v1;
use yii\rest\Controller;
use yii\filters\Cors;
use yii\filters\auth\HttpBearerAuth;
use yii\filters\RateLimiter;
class BaseController extends Controller
{
public function behaviors()
{
$behaviors = parent::behaviors();
// 1. CORS (đầu tiên)
$behaviors['cors'] = [
'class' => Cors::class,
'cors' => [
'Origin' => ['https://myapp.com', 'http://localhost:3000'],
'Access-Control-Request-Method' => [
'GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS',
],
'Access-Control-Request-Headers' => ['*'],
'Access-Control-Allow-Credentials' => true,
'Access-Control-Max-Age' => 86400,
'Access-Control-Expose-Headers' => [
'X-Pagination-Total-Count',
'X-Pagination-Page-Count',
'X-Pagination-Current-Page',
'X-Pagination-Per-Page',
'X-Rate-Limit-Limit',
'X-Rate-Limit-Remaining',
'X-Rate-Limit-Reset',
],
],
];
// 2. Authentication
$behaviors['authenticator'] = [
'class' => HttpBearerAuth::class,
];
// 3. Rate limiting
$behaviors['rateLimiter'] = [
'class' => RateLimiter::class,
'enableRateLimitHeaders' => true,
];
return $behaviors;
}
}Task Controller (Final)
// controllers/api/v1/TaskController.php
namespace app\controllers\api\v1;
use app\models\Task;
use yii\rest\ActiveController;
use yii\filters\Cors;
use yii\filters\auth\HttpBearerAuth;
use yii\filters\RateLimiter;
use yii\data\ActiveDataProvider;
class TaskController extends ActiveController
{
public $modelClass = 'app\models\Task';
public function behaviors()
{
$behaviors = parent::behaviors();
$behaviors['cors'] = [
'class' => Cors::class,
'cors' => [
'Origin' => ['https://myapp.com', 'http://localhost:3000'],
'Access-Control-Request-Method' => [
'GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS',
],
'Access-Control-Request-Headers' => ['*'],
'Access-Control-Allow-Credentials' => true,
'Access-Control-Max-Age' => 86400,
'Access-Control-Expose-Headers' => [
'X-Pagination-Total-Count',
'X-Pagination-Page-Count',
'X-Pagination-Current-Page',
'X-Pagination-Per-Page',
'X-Rate-Limit-Limit',
'X-Rate-Limit-Remaining',
'X-Rate-Limit-Reset',
],
],
];
$behaviors['authenticator'] = [
'class' => HttpBearerAuth::class,
];
$behaviors['rateLimiter'] = [
'class' => RateLimiter::class,
'enableRateLimitHeaders' => true,
];
return $behaviors;
}
public function actions()
{
$actions = parent::actions();
unset($actions['delete']);
$actions['index']['prepareDataProvider'] = [$this, 'prepareDataProvider'];
return $actions;
}
public function prepareDataProvider()
{
$request = \Yii::$app->request;
$query = Task::find()
->where(['user_id' => \Yii::$app->user->id]);
// Filtering
if (($status = $request->get('status')) !== null) {
$query->andWhere(['status' => $status]);
}
if (($priority = $request->get('priority')) !== null) {
$query->andWhere(['priority' => $priority]);
}
if ($search = $request->get('q')) {
$query->andWhere(['like', 'title', $search]);
}
return new ActiveDataProvider([
'query' => $query,
'sort' => [
'attributes' => [
'id', 'title', 'status', 'priority',
'due_date', 'created_at',
],
'defaultOrder' => ['created_at' => SORT_DESC],
],
'pagination' => [
'pageSize' => 20,
'pageSizeLimit' => [1, 100],
],
]);
}
public function actionComplete($id)
{
$task = $this->findModel($id);
$task->status = Task::STATUS_DONE;
$task->completed_at = date('Y-m-d H:i:s');
if (!$task->save()) {
return $task;
}
return $task;
}
public function actionStats()
{
$userId = \Yii::$app->user->id;
$base = Task::find()->where(['user_id' => $userId]);
return [
'total' => (clone $base)->count(),
'todo' => (clone $base)->andWhere(['status' => Task::STATUS_TODO])->count(),
'in_progress' => (clone $base)->andWhere(['status' => Task::STATUS_IN_PROGRESS])->count(),
'done' => (clone $base)->andWhere(['status' => Task::STATUS_DONE])->count(),
];
}
protected function findModel($id)
{
$model = Task::findOne([
'id' => $id,
'user_id' => \Yii::$app->user->id,
]);
if ($model === null) {
throw new \yii\web\NotFoundHttpException("Task #$id not found.");
}
return $model;
}
}Complete API Request Flow
Bảng Tham Khảo API Endpoints
| Method | URL | Auth | Mô tả |
|---|---|---|---|
POST | /api/v1/auth/login | Không | Login, lấy token |
POST | /api/v1/auth/register | Không | Register, lấy token |
GET | /api/v1/tasks | Có | Liệt kê tasks (paginated, filterable, sortable) |
GET | /api/v1/tasks/:id | Có | Lấy single task |
POST | /api/v1/tasks | Có | Tạo task |
PUT/PATCH | /api/v1/tasks/:id | Có | Cập nhật task |
POST | /api/v1/tasks/:id/complete | Có | Đánh dấu task hoàn thành |
GET | /api/v1/tasks/stats | Có | Đếm task theo status |
Query Parameters
| Parameter | Ví dụ | Mô tả |
|---|---|---|
page | ?page=2 | Số trang |
per-page | ?per-page=10 | Items mỗi trang (1-100) |
sort | ?sort=-priority,status | Sort fields (- = giảm dần) |
q | ?q=deploy | Tìm kiếm trong title |
status | ?status=1 | Filter theo status |
priority | ?priority=3 | Filter theo priority |
fields | ?fields=id,title,status | Chọn specific fields |
expand | ?expand=user,comments | Bao gồm relations |
Tiếp Theo
Task manager của bạn giờ đã có complete REST API:
ActiveControllercung cấp CRUD không cần boilerplate- Custom actions xử lý business logic (complete, stats, bulk-update)
fields()vàextraFields()kiểm soát chính xác client thấy gì- Pagination, sorting, và filtering hoạt động qua query parameters
- Bearer token authentication bảo vệ mọi endpoint
- Rate limiting ngăn chặn abuse (100 req/min mỗi user)
- API versioning hỗ trợ phát triển dần dần
- CORS cho phép frontends ở domains khác gọi API
Deep Dive → Widgets, Asset Bundles & Frontend Integration — build custom widgets, quản lý CSS/JS assets, dùng GridView, ListView, Pjax, và tích hợp npm packages.
📬 Đăng ký nhận bản tin
Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.
Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.
💬 Comments
Đăng nhập để bình luận
Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.