API cho phép ứng dụng di động (Flutter), trang JavaScript hay hệ thống khác dùng dữ liệu của bạn. Bài này học nguyên tắc REST, thiết kế URL và phương thức, viết API CRUD sinh viên bằng PHP thuần, xác thực bằng token, CORS và kiểm thử bằng curl/Postman.
- Hiểu REST: tài nguyên, phương thức, mã trạng thái, stateless
- Thiết kế endpoint CRUD cho một tài nguyên
- Viết API trả JSON, đọc body JSON, báo lỗi chuẩn
- Bảo vệ API bằng token, bật CORS, kiểm thử bằng curl/Postman
- Resource
- Tài nguyên, định danh bằng URL danh từ số nhiều: /api/sinhvien.
- Stateless
- Mỗi yêu cầu mang đủ thông tin (token), máy chủ không nhớ phiên.
- Bearer token
- Chuỗi bí mật gửi trong header Authorization.
- CORS
- Cho phép trang ở tên miền khác gọi API.
Khái niệm và lý thuyết
| Phương thức + URL | Thao tác | Mã trả về |
|---|---|---|
| GET /api/sinhvien | Danh sách (hỗ trợ ?q=&page=) | 200 |
| GET /api/sinhvien/SV001 | Chi tiết | 200 / 404 |
| POST /api/sinhvien | Tạo mới (body JSON) | 201 / 422 |
| PUT /api/sinhvien/SV001 | Cập nhật toàn bộ | 200 / 404 / 422 |
| PATCH /api/sinhvien/SV001 | Cập nhật một phần | 200 |
| DELETE /api/sinhvien/SV001 | Xóa | 204 / 404 |
Quy ước: URL là danh từ, hành động thể hiện bằng phương thức HTTP; trả JSON thống nhất; dùng đúng mã trạng thái (200, 201, 204, 400, 401, 404, 422, 500); phiên bản hóa khi cần (/api/v1/).
api/index.php: khung API
<?php
declare(strict_types=1);
header("Content-Type: application/json; charset=utf-8");
header("Access-Control-Allow-Origin: *"); // CORS (thu hẹp khi lên thật)
header("Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS");
header("Access-Control-Allow-Headers: Content-Type, Authorization");
if ($_SERVER["REQUEST_METHOD"] === "OPTIONS") exit; // preflight
function tra(mixed $du_lieu, int $ma = 200): never {
http_response_code($ma);
if ($ma !== 204) echo json_encode($du_lieu, JSON_UNESCAPED_UNICODE);
exit;
}
function loi(string $tb, int $ma = 400, array $chi_tiet = []): never {
tra(["loi" => $tb] + ($chi_tiet ? ["chi_tiet" => $chi_tiet] : []), $ma);
}
function body(): array { return json_decode(file_get_contents("php://input"), true) ?? []; }
// Xác thực bằng token (trừ GET)
$method = $_SERVER["REQUEST_METHOD"];
if ($method !== "GET") {
$auth = $_SERVER["HTTP_AUTHORIZATION"] ?? "";
if (!hash_equals("Bearer " . TOKEN_BI_MAT, $auth)) loi("Chưa xác thực", 401);
}
require_once __DIR__ . "/../models/SinhVien.php";
$path = trim(str_replace("/qlsv/api", "", parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH)), "/");
$phan = explode("/", $path); // ["sinhvien", "SV001"]
if (($phan[0] ?? "") !== "sinhvien") loi("Không tìm thấy", 404);
$ma = $phan[1] ?? null;Xử lý từng phương thức
<?php
match (true) {
$method === "GET" && !$ma => tra(["du_lieu" => SinhVien::search($_GET["q"] ?? "", (int)($_GET["page"] ?? 1))]),
$method === "GET" => tra(SinhVien::find($ma) ?? loi("Không tìm thấy sinh viên", 404)),
$method === "POST" => (function () {
$d = body(); $l = SinhVien::validate($d);
if ($l) loi("Dữ liệu không hợp lệ", 422, $l);
if (SinhVien::find($d["ma_sv"])) loi("Mã sinh viên đã tồn tại", 409);
SinhVien::create($d);
tra(SinhVien::find($d["ma_sv"]), 201);
})(),
in_array($method, ["PUT", "PATCH"]) => (function () use ($ma, $method) {
if (!SinhVien::find($ma)) loi("Không tìm thấy sinh viên", 404);
$d = body(); $l = SinhVien::validate($d, $method === "PATCH");
if ($l) loi("Dữ liệu không hợp lệ", 422, $l);
SinhVien::update($ma, $d);
tra(SinhVien::find($ma));
})(),
$method === "DELETE" => SinhVien::find($ma) ? (SinhVien::delete($ma) ? tra(null, 204) : loi("Không xóa được", 500)) : loi("Không tìm thấy sinh viên", 404),
default => loi("Phương thức không hỗ trợ", 405),
};Mỗi nhánh gọi Model (loạt PHP và MySQL) với prepared statement. validate() trả mảng lỗi theo trường, ví dụ {"email": "không hợp lệ"}.
Kiểm thử bằng curl và Postman
curl http://localhost/qlsv/api/sinhvien
curl http://localhost/qlsv/api/sinhvien/SV001
curl -X POST http://localhost/qlsv/api/sinhvien \
-H "Authorization: Bearer abc123" -H "Content-Type: application/json" \
-d '{"ma_sv":"SV010","ho_ten":"Lò Văn Mới","lop":"K66 Tin","email":"Địa chỉ email này đang được bảo vệ từ spam bots, bạn cần kích hoạt Javascript để xem nó. Bạn cần kích hoạt Javascript để xem nó. "}'
curl -X PATCH http://localhost/qlsv/api/sinhvien/SV010 -H "Authorization: Bearer abc123" \
-H "Content-Type: application/json" -d '{"lop":"K65 Tin"}'
curl -i -X DELETE http://localhost/qlsv/api/sinhvien/SV010 -H "Authorization: Bearer abc123"Postman hoặc tiện ích REST Client của VS Code giúp lưu bộ request để thử lại. Phản hồi lỗi luôn có cùng dạng {"loi": "...", "chi_tiet": {...}} để client dễ xử lý.
Gọi API từ client
// JavaScript (loạt JavaScript, Bài 7)
const res = await fetch("http://localhost/qlsv/api/sinhvien?q=An");
const { du_lieu } = await res.json();
// Flutter/Dart (loạt Flutter, Bài 9)
final res = await http.get(Uri.parse("http://10.0.2.2/qlsv/api/sinhvien"));
final ds = jsonDecode(res.body)["du_lieu"] as List;Trong thực tế, token được cấp qua endpoint đăng nhập (POST /api/login trả JWT hoặc token lưu CSDL) và có hạn dùng; thư viện firebase/php-jwt cài bằng Composer hỗ trợ JWT.
Access-Control-Allow-Origin: * chỉ dùng khi phát triển. Khi lên thật, chỉ cho phép tên miền của ứng dụng, giới hạn tốc độ (rate limit) và bắt buộc HTTPS.
- Hoàn thiện API sinh viên với 6 endpoint và kiểm thử bằng curl.
- Thêm phân trang ?page=&limit= và trả về tổng số bản ghi.
- Thêm endpoint POST /api/login trả token; các request khác dùng token đó.
- Viết trang HTML + JavaScript (hoặc ứng dụng Flutter) hiển thị và thêm sinh viên qua API.
- REST: URL danh từ, phương thức HTTP là hành động, mã trạng thái đúng.
- JSON vào/ra; hàm tra()/loi() thống nhất phản hồi.
- Token trong header Authorization; CORS cho client khác tên miền.
- Kiểm thử bằng curl/Postman; client là JS hoặc Flutter.