REST là gì? Nguyên tắc và Ràng buộc
Trong Bài 1, chúng ta đã đề cập ngắn gọn về REST API. Bây giờ hãy đi sâu hơn vào REST thực sự là gì và tại sao nó là kiến trúc phổ biến nhất cho web API hiện nay.
REST — Representational State Transfer
REST được định nghĩa bởi Roy Fielding trong luận án tiến sĩ năm 2000. Nó không phải là giao thức, không phải tiêu chuẩn, và không phải thư viện. Nó là một phong cách kiến trúc — một tập hợp các nguyên tắc cho thiết kế ứng dụng mạng.
Khi chúng ta nói “REST API” hoặc “RESTful API,” chúng ta muốn nói một API tuân theo các nguyên tắc này.
Sáu ràng buộc của REST
REST định nghĩa sáu ràng buộc. Một API tuân theo tất cả chúng được coi là “RESTful.”
1. Phân tách Client-Server
Client và server là độc lập. Client không biết server lưu trữ dữ liệu như thế nào. Server không biết client hiển thị dữ liệu ra sao. Chúng chỉ giao tiếp qua HTTP request và response.
Đây chính xác là những gì chúng ta đã thảo luận ở Bài 1. Server Spring Boot của bạn trả về dữ liệu JSON. Ứng dụng React, ứng dụng Android, hoặc lệnh curl đều có thể sử dụng dữ liệu đó theo cách khác nhau.
2. Không trạng thái (Statelessness)
Mỗi request từ client phải chứa tất cả thông tin server cần để xử lý nó. Server không lưu trạng thái client giữa các request.
Điều này có nghĩa: không có session phía server (theo nghĩa truyền thống). Nếu client cần được xác thực, nó phải gửi thông tin đăng nhập hoặc token với mọi request. Chúng ta sẽ triển khai điều này với JWT token ở Bài 12.
# Mỗi request là độc lập — server không nhớ request trước
GET /api/users/1 → Server phản hồi với dữ liệu người dùng
GET /api/users/1 → Server phản hồi theo cách tương tự (không "nhớ" request đầu)
Tại sao điều này quan trọng? Statelessness làm API của bạn có khả năng mở rộng. Nếu bạn có 10 server phía sau load balancer, bất kỳ server nào cũng có thể xử lý bất kỳ request nào vì không server nào giữ trạng thái đặc thù client.
3. Khả năng cache (Cacheability)
Response nên chỉ ra liệu chúng có thể được cache hay không. Nếu response có thể cache, client có thể tái sử dụng nó cho các request tương tự trong tương lai mà không cần truy cập server.
4. Giao diện thống nhất (Uniform Interface)
Đây là ràng buộc quan trọng nhất. API nên có cấu trúc nhất quán, có thể dự đoán. REST đạt được điều này thông qua:
- Nhận dạng tài nguyên qua URI: Mỗi thực thể là một “tài nguyên” với URL duy nhất.
- Thao tác qua biểu diễn: Tài nguyên được thao tác bằng cách gửi biểu diễn (thường là JSON) trong HTTP request.
- Thông điệp tự mô tả: Mỗi request/response chứa đủ thông tin để hiểu chính nó.
- HATEOAS: Response bao gồm liên kết đến tài nguyên liên quan. Trong thực tế, nhiều API bỏ qua ràng buộc này.
5. Hệ thống phân tầng (Layered System)
Client không biết liệu nó đang giao tiếp trực tiếp với server hay qua trung gian (load balancer, CDN, proxy).
6. Code on Demand (Tùy chọn)
Server có thể mở rộng chức năng client bằng cách gửi mã thực thi (như JavaScript). Ràng buộc này là tùy chọn và hiếm khi được sử dụng trong REST API.
Tài nguyên: Cốt lõi của REST
Trong REST, mọi thứ là một tài nguyên (resource). Tài nguyên là bất kỳ thực thể nào có thể được đặt tên và truy cập:
- Một người dùng →
/api/users/42 - Một bài viết blog →
/api/posts/15 - Một bình luận trên bài viết →
/api/posts/15/comments/3 - Tất cả thẻ tag →
/api/tags
Tài nguyên được nhận dạng bởi URI (Uniform Resource Identifier). URI cho biết tài nguyên bạn muốn. Phương thức HTTP cho biết bạn muốn làm gì với nó.
REST không phải lựa chọn duy nhất
Cần biết rằng REST không phải cách tiếp cận duy nhất để xây dựng API. Các lựa chọn thay thế bao gồm:
- GraphQL — Client chỉ định chính xác dữ liệu nó cần. Phổ biến với frontend phức tạp.
- gRPC — Giao thức nhị phân hiệu suất cao. Phổ biến cho giao tiếp server-to-server.
- WebSocket — Giao tiếp hai chiều đầy đủ cho ứng dụng thời gian thực (chat, thông báo trực tiếp).
REST vẫn là lựa chọn phổ biến nhất cho web API nhờ tính đơn giản và sự phổ biến của HTTP. Trong series này, chúng ta hoàn toàn tập trung vào REST.
Phương thức HTTP ánh xạ đến thao tác CRUD
REST sử dụng phương thức HTTP để diễn đạt hành động bạn muốn thực hiện trên tài nguyên. Bốn phương thức chính ánh xạ trực tiếp đến thao tác CRUD (Create, Read, Update, Delete):
| Phương thức HTTP | Thao tác CRUD | Mô tả | Ví dụ |
|---|---|---|---|
POST |
Create (Tạo) | Tạo tài nguyên mới | POST /api/posts — Tạo bài viết mới |
GET |
Read (Đọc) | Lấy tài nguyên | GET /api/posts/5 — Lấy bài viết ID 5 |
PUT |
Update (Cập nhật) | Thay thế toàn bộ tài nguyên | PUT /api/posts/5 — Thay thế bài viết 5 |
PATCH |
Update (Cập nhật một phần) | Cập nhật một phần tài nguyên | PATCH /api/posts/5 — Chỉ cập nhật tiêu đề |
DELETE |
Delete (Xóa) | Xóa tài nguyên | DELETE /api/posts/5 — Xóa bài viết 5 |
Phương thức An toàn và Idempotent
Hai thuộc tính quan trọng phân biệt các phương thức HTTP:
Phương thức an toàn (safe) không sửa đổi tài nguyên. GET và HEAD là an toàn — gọi chúng không bao giờ thay đổi dữ liệu trên server.
Phương thức idempotent tạo ra cùng kết quả bất kể bạn gọi bao nhiêu lần:
| Phương thức | An toàn? | Idempotent? | Giải thích |
|---|---|---|---|
GET |
Có | Có | Đọc dữ liệu không bao giờ thay đổi gì |
POST |
Không | Không | Mỗi lần gọi tạo tài nguyên mới (gọi 2 lần = 2 tài nguyên) |
PUT |
Không | Có | Thay thế cùng tài nguyên với cùng dữ liệu cho cùng kết quả |
PATCH |
Không | Không* | Phụ thuộc vào implementation |
DELETE |
Không | Có | Xóa tài nguyên đã xóa không có tác dụng thêm |
Hiểu idempotency quan trọng cho xử lý lỗi. Nếu request PUT bị timeout, client có thể an toàn thử lại — kết quả sẽ giống nhau. Nếu request POST bị timeout, thử lại có thể tạo tài nguyên trùng lặp.
Quy ước thiết kế URL
URL REST nên tuân theo các quy ước sau:
# Dùng danh từ, không dùng động từ — phương thức HTTP ĐÃ LÀ động từ
✅ GET /api/posts (lấy tất cả bài viết)
❌ GET /api/getPosts (thừa — GET đã có nghĩa "lấy")
❌ GET /api/posts/getAll (thừa)
# Dùng danh từ số nhiều cho collection
✅ /api/posts
✅ /api/users
❌ /api/post (số ít cảm giác không nhất quán)
# Dùng path variable cho tài nguyên cụ thể
✅ GET /api/posts/42 (lấy bài viết ID 42)
# Dùng query parameter cho lọc, sắp xếp, phân trang
✅ GET /api/posts?status=published&sort=date&page=2
# Dùng nesting cho tài nguyên con (quan hệ)
✅ GET /api/posts/42/comments (tất cả bình luận của bài viết 42)
✅ GET /api/posts/42/comments/7 (bình luận 7 của bài viết 42)
# Giữ URL chữ thường, dùng dấu gạch nối cho nhiều từ
✅ /api/blog-posts
❌ /api/BlogPosts
❌ /api/blog_posts
@RestController vs @Controller
Trong Bài 2, chúng ta đã đề cập rằng @RestController kết hợp @Controller và @ResponseBody. Hãy hiểu sự khác biệt sâu hơn.
@Controller — Cho trang web render phía Server
@Controller được sử dụng khi server tạo trang HTML. Giá trị trả về của phương thức được coi là tên view — Spring tìm template HTML với tên đó và render nó.
@Controller
public class PageController {
// Spring tìm template có tên "home" (ví dụ home.html trong thư mục templates)
// và render nó thành trang HTML.
@GetMapping("/home")
public String homePage(Model model) {
model.addAttribute("title", "Chào mừng đến Blog");
model.addAttribute("posts", postService.getRecentPosts());
return "home"; // Đây là TÊN VIEW, không phải dữ liệu
}
}
Cách tiếp cận này gọi là server-side rendering (SSR). Server tạo HTML hoàn chỉnh và gửi đến trình duyệt.
@RestController — Cho REST API
@RestController được sử dụng khi server trả về dữ liệu (thường là JSON). Giá trị trả về được ghi trực tiếp vào phần thân HTTP response.
@RestController
public class PostApiController {
// Giá trị trả về (List<Post>) được tự động
// serialize thành JSON và ghi vào phần thân response.
@GetMapping("/api/posts")
public List<Post> getAllPosts() {
return postService.getAllPosts(); // Đây là DỮ LIỆU, không phải tên view
}
}
Sự tương đương
@RestController đúng nghĩa là phím tắt. Hai ví dụ này hoàn toàn giống nhau:
// Dùng @RestController (phím tắt)
@RestController
public class PostApiController {
@GetMapping("/api/posts")
public List<Post> getAllPosts() {
return postService.getAllPosts();
}
}
// Dùng @Controller + @ResponseBody (cách dài)
@Controller
@ResponseBody
public class PostApiController {
@GetMapping("/api/posts")
public List<Post> getAllPosts() {
return postService.getAllPosts();
}
}
Chọn cái nào cho Series này?
Chúng ta đang xây dựng REST API, nên sẽ sử dụng @RestController hoàn toàn. Server trả về dữ liệu JSON, và chúng ta để phần giao diện cho ứng dụng frontend riêng biệt (React, Angular, Vue, hoặc ứng dụng di động).
Request Mapping — @GetMapping, @PostMapping, @PutMapping, @DeleteMapping
Cơ bản
Spring MVC cung cấp annotation phím tắt để ánh xạ phương thức HTTP đến phương thức xử lý:
@RestController
@RequestMapping("/api/posts") // Đường dẫn gốc cho tất cả phương thức trong controller
public class PostController {
@GetMapping // GET /api/posts
public List<Post> getAll() { ... }
@GetMapping("/{id}") // GET /api/posts/42
public Post getById(@PathVariable Long id) { ... }
@PostMapping // POST /api/posts
public Post create(@RequestBody Post post) { ... }
@PutMapping("/{id}") // PUT /api/posts/42
public Post update(@PathVariable Long id, @RequestBody Post post) { ... }
@PatchMapping("/{id}") // PATCH /api/posts/42
public Post partialUpdate(@PathVariable Long id, @RequestBody Map<String, Object> updates) { ... }
@DeleteMapping("/{id}") // DELETE /api/posts/42
public void delete(@PathVariable Long id) { ... }
}
@RequestMapping — Annotation cha
Tất cả annotation phím tắt là chuyên biệt hóa của @RequestMapping:
// Hai cái này tương đương:
@GetMapping("/api/posts")
@RequestMapping(value = "/api/posts", method = RequestMethod.GET)
// Hai cái này tương đương:
@PostMapping("/api/posts")
@RequestMapping(value = "/api/posts", method = RequestMethod.POST)
Annotation phím tắt dễ đọc hơn, nên luôn ưu tiên chúng. Sử dụng @RequestMapping ở cấp class để đặt đường dẫn gốc cho tất cả phương thức trong controller.
Path Variable và Query Parameter
Có hai cách chính để truyền dữ liệu trong URL: path variable và query parameter. Hiểu khi nào dùng cái nào là thiết yếu cho thiết kế API tốt.
Path Variable — Xác định tài nguyên cụ thể
Path variable là một phần của đường dẫn URL. Chúng được sử dụng để xác định tài nguyên cụ thể.
GET /api/users/42 → 42 là path variable (ID người dùng)
GET /api/posts/15/comments → 15 là path variable (ID bài viết)
Trong Spring, sử dụng @PathVariable để trích xuất chúng:
@RestController
@RequestMapping("/api/users")
public class UserController {
// Một path variable
// URL: GET /api/users/42
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
// id = 42
return userService.getUserById(id);
}
// Nhiều path variable
// URL: GET /api/users/42/posts/15
@GetMapping("/{userId}/posts/{postId}")
public Post getUserPost(
@PathVariable Long userId,
@PathVariable Long postId) {
// userId = 42, postId = 15
return postService.getPostByUserAndId(userId, postId);
}
}
Query Parameter — Lọc, Sắp xếp, Phân trang
Query parameter đến sau dấu ? trong URL. Chúng dùng cho bộ lọc tùy chọn như lọc, sắp xếp, và phân trang.
GET /api/posts?status=published → lọc theo trạng thái
GET /api/posts?sort=date&order=desc → sắp xếp theo ngày giảm dần
GET /api/posts?page=2&size=10 → phân trang
Trong Spring, sử dụng @RequestParam để trích xuất:
@RestController
@RequestMapping("/api/posts")
public class PostController {
// Query parameter bắt buộc
// URL: GET /api/posts/search?keyword=spring
@GetMapping("/search")
public List<Post> search(@RequestParam String keyword) {
return postService.searchByKeyword(keyword);
}
// Query parameter tùy chọn với giá trị mặc định
// URL: GET /api/posts?page=2&size=20
// URL: GET /api/posts (dùng mặc định: page=0, size=10)
@GetMapping
public List<Post> getAllPosts(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) {
return postService.getPosts(page, size);
}
// Tham số tùy chọn tường minh (có thể null)
@GetMapping("/filter")
public List<Post> filterPosts(
@RequestParam(required = false) String status,
@RequestParam(required = false) String category) {
return postService.filterPosts(status, category);
}
}
Khi nào dùng cái nào?
Quy tắc đơn giản:
| Trường hợp | Cách tiếp cận | Ví dụ |
|---|---|---|
| Xác định tài nguyên cụ thể | Path variable | GET /api/users/42 |
| Mã định danh bắt buộc | Path variable | GET /api/posts/15/comments/3 |
| Lọc / tìm kiếm | Query parameter | GET /api/posts?status=draft |
| Sắp xếp | Query parameter | GET /api/posts?sort=date |
| Phân trang | Query parameter | GET /api/posts?page=2&size=10 |
| Bộ điều chỉnh tùy chọn | Query parameter | GET /api/users?role=admin |
Nghĩ theo cách này: path variable xác định tài nguyên nào, query parameter chỉ định cách bạn muốn.
Request Body & @RequestBody
Với request POST và PUT, dữ liệu được gửi trong phần thân request (request body), không phải trong URL. Phần thân request thường chứa biểu diễn JSON của tài nguyên bạn muốn tạo hoặc cập nhật.
@RequestBody hoạt động như thế nào
@RequestBody yêu cầu Spring đọc phần thân HTTP request và chuyển đổi nó thành đối tượng Java. Việc chuyển đổi này do Jackson (thư viện JSON đi kèm spring-boot-starter-web) xử lý.
@RestController
@RequestMapping("/api/posts")
public class PostController {
// Client gửi JSON trong phần thân request:
// {
// "title": "Học Spring Boot",
// "content": "Spring Boot giúp lập trình web Java dễ dàng...",
// "category": "TUTORIAL"
// }
//
// Spring đọc JSON này, tạo đối tượng Post, và gán giá trị cho các trường.
// Quá trình này gọi là DESERIALIZATION (JSON → đối tượng Java).
@PostMapping
public Post createPost(@RequestBody Post post) {
// Tại thời điểm này, post.getTitle() = "Học Spring Boot"
// post.getContent() = "Spring Boot giúp lập trình web Java dễ dàng..."
// post.getCategory() = "TUTORIAL"
return postService.createPost(post);
}
}
Kết hợp @RequestBody với @PathVariable
Thường xuyên kết hợp path variable với request body, đặc biệt cho thao tác cập nhật:
// PUT /api/posts/42
// Request body: { "title": "Tiêu đề cập nhật", "content": "Nội dung cập nhật" }
@PutMapping("/{id}")
public Post updatePost(
@PathVariable Long id, // Từ đường dẫn URL
@RequestBody Post updatedPost) { // Từ phần thân request
return postService.updatePost(id, updatedPost);
}
ResponseEntity và Mã trạng thái HTTP
Tại sao mã trạng thái quan trọng
Mã trạng thái HTTP cho client biết điều gì đã xảy ra với request. Sử dụng mã trạng thái đúng là phần cơ bản của việc xây dựng REST API đúng cách.
Mã trạng thái phổ biến
2xx — Thành công:
| Mã | Tên | Khi nào dùng |
|---|---|---|
| 200 | OK | Thành công chung. Dùng cho GET, PUT, PATCH |
| 201 | Created | Tài nguyên mới đã được tạo. Dùng cho POST |
| 204 | No Content | Thành công, không có gì để trả về. Dùng cho DELETE |
4xx — Lỗi Client:
| Mã | Tên | Khi nào dùng |
|---|---|---|
| 400 | Bad Request | Dữ liệu request không hợp lệ (lỗi validation) |
| 401 | Unauthorized | Cần xác thực (chưa đăng nhập) |
| 403 | Forbidden | Đã xác thực nhưng không có quyền |
| 404 | Not Found | Tài nguyên yêu cầu không tồn tại |
| 409 | Conflict | Xung đột tài nguyên (ví dụ email trùng) |
5xx — Lỗi Server:
| Mã | Tên | Khi nào dùng |
|---|---|---|
| 500 | Internal Server Error | Lỗi bất ngờ trên server |
| 503 | Service Unavailable | Server tạm thời quá tải hoặc bảo trì |
Trả về mã trạng thái với ResponseEntity
Mặc định, Spring trả về 200 OK cho tất cả response thành công. Để trả về mã trạng thái khác, sử dụng ResponseEntity:
@RestController
@RequestMapping("/api/posts")
public class PostController {
// POST /api/posts — Trả về 201 Created
@PostMapping
public ResponseEntity<Post> createPost(@RequestBody Post post) {
Post created = postService.createPost(post);
return ResponseEntity.status(HttpStatus.CREATED).body(created);
}
// GET /api/posts/42 — Trả về 200 OK hoặc 404 Not Found
@GetMapping("/{id}")
public ResponseEntity<Post> getPostById(@PathVariable Long id) {
return postService.findById(id)
.map(post -> ResponseEntity.ok(post))
.orElse(ResponseEntity.notFound().build());
}
// DELETE /api/posts/42 — Trả về 204 No Content
@DeleteMapping("/{id}")
public ResponseEntity<Void> deletePost(@PathVariable Long id) {
try {
postService.deletePost(id);
return ResponseEntity.noContent().build();
} catch (RuntimeException e) {
return ResponseEntity.notFound().build();
}
}
}
Các phương thức builder của ResponseEntity
// 200 OK với body
ResponseEntity.ok(body)
// 201 Created với body
ResponseEntity.status(HttpStatus.CREATED).body(createdObject)
// 204 No Content
ResponseEntity.noContent().build()
// 400 Bad Request với body
ResponseEntity.badRequest().body(errorObject)
// 404 Not Found
ResponseEntity.notFound().build()
// Bất kỳ mã trạng thái nào
ResponseEntity.status(HttpStatus.CONFLICT).body(errorObject)
Content Negotiation (JSON mặc định với Jackson)
Content Negotiation là gì?
Content negotiation là quá trình client và server thỏa thuận về định dạng dữ liệu trao đổi. Client sử dụng header Accept để nói định dạng nó muốn, và header Content-Type để nói định dạng nó đang gửi.
Jackson — Thư viện JSON mặc định
Spring Boot tự động bao gồm Jackson với spring-boot-starter-web. Jackson xử lý:
- Serialization: Chuyển đổi đối tượng Java → JSON (cho response)
- Deserialization: Chuyển đổi JSON → đối tượng Java (cho request body)
Tùy chỉnh đầu ra JSON với Annotation
Jackson cung cấp annotation để kiểm soát serialization và deserialization:
public class Post {
private Long id;
private String title;
private String content;
private String internalNotes;
private LocalDateTime createdAt;
// @JsonProperty — thay đổi tên trường JSON
@JsonProperty("created_at")
public LocalDateTime getCreatedAt() { return createdAt; }
// @JsonIgnore — loại trừ trường này khỏi JSON hoàn toàn
@JsonIgnore
public String getInternalNotes() { return internalNotes; }
}
Kết quả JSON:
{
"id": 1,
"title": "Bài viết của tôi",
"content": "Nội dung...",
"created_at": "2024-01-15T10:30:00"
}
// "internalNotes" KHÔNG được bao gồm (vì @JsonIgnore)
// "createdAt" được serialize thành "created_at" (vì @JsonProperty)
Chiến lược phiên bản API
Khi API phát triển, bạn sẽ cần thực hiện thay đổi phá vỡ — đổi tên trường, thay đổi cấu trúc response, loại bỏ endpoint. Phiên bản API cho phép bạn giới thiệu thay đổi mà không phá vỡ client hiện tại.
Chiến lược 1: Phiên bản qua đường dẫn URL (Phổ biến nhất)
Bao gồm số phiên bản trong đường dẫn URL:
// Phiên bản 1 — trả về thông tin người dùng cơ bản
@RestController
@RequestMapping("/api/v1/users")
public class UserControllerV1 {
@GetMapping("/{id}")
public Map<String, Object> getUser(@PathVariable Long id) {
return Map.of("id", id, "name", "Alice", "email", "alice@example.com");
}
}
// Phiên bản 2 — trả về thông tin mở rộng với cấu trúc khác
@RestController
@RequestMapping("/api/v2/users")
public class UserControllerV2 {
@GetMapping("/{id}")
public Map<String, Object> getUser(@PathVariable Long id) {
return Map.of(
"id", id,
"profile", Map.of("firstName", "Alice", "lastName", "Smith", "email", "alice@example.com"),
"metadata", Map.of("createdAt", "2024-01-15", "lastLogin", "2024-06-20")
);
}
}
Client chọn phiên bản để sử dụng:
GET /api/v1/users/42 → định dạng cũ
GET /api/v2/users/42 → định dạng mới
Đây là cách tiếp cận phổ biến nhất vì đơn giản và dễ thấy.
Lời khuyên thực tế
Trong dự án thực, hãy cố giảm thiểu nhu cầu phiên bản:
- Thêm trường thay vì xóa — thêm trường mới không phải thay đổi phá vỡ.
- Dùng trường tùy chọn — làm trường mới tùy chọn để client cũ không bị ảnh hưởng.
- Deprecate trước khi xóa — cho client thời gian để di chuyển.
- Phiên bản khi buộc phải — chỉ tạo phiên bản mới cho thay đổi thực sự phá vỡ.
Thực hành: Xây dựng CRUD REST API hoàn chỉnh (In-Memory, chưa có Database)
Hãy áp dụng mọi thứ từ bài giảng này để xây dựng API bài viết blog đầy đủ tính năng. Chúng ta sẽ sử dụng kiến trúc phân tầng từ Bài 2 (Controller → Service → Repository) và triển khai tất cả thao tác CRUD với phương thức HTTP và mã trạng thái đúng.
Bước 1: Model Post
File: src/main/java/com/example/blogapi/model/Post.java
package com.example.blogapi.model;
import com.fasterxml.jackson.annotation.JsonInclude;
import java.time.LocalDateTime;
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Post {
private Long id;
private String title;
private String content;
private String category;
private String status; // "DRAFT" hoặc "PUBLISHED"
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
public Post() { }
public Post(String title, String content, String category) {
this.title = title;
this.content = content;
this.category = category;
this.status = "DRAFT";
this.createdAt = LocalDateTime.now();
}
// --- Getter và Setter ---
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getContent() { return content; }
public void setContent(String content) { this.content = content; }
public String getCategory() { return category; }
public void setCategory(String category) { this.category = category; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
}
Bước 2: Kiểm thử CRUD hoàn chỉnh
Sau khi tạo đầy đủ Repository, Service, và Controller (theo pattern tương tự Bài 2), kiểm thử với curl:
# Tạo bài viết
curl -X POST http://localhost:8080/api/posts \
-H "Content-Type: application/json" \
-d '{
"title": "Bắt đầu với Spring Boot",
"content": "Spring Boot là framework giúp đơn giản hóa lập trình web Java...",
"category": "TUTORIAL"
}'
# Kết quả: 201 Created
# Lấy tất cả bài viết
curl http://localhost:8080/api/posts
# Cập nhật toàn bộ (PUT)
curl -X PUT http://localhost:8080/api/posts/1 \
-H "Content-Type: application/json" \
-d '{
"title": "Bắt đầu với Spring Boot 3",
"content": "Spring Boot 3 yêu cầu Java 17...",
"category": "TUTORIAL",
"status": "PUBLISHED"
}'
# Cập nhật một phần (PATCH) — chỉ cập nhật trạng thái
curl -X PATCH http://localhost:8080/api/posts/1 \
-H "Content-Type: application/json" \
-d '{"status": "PUBLISHED"}'
# Xóa bài viết
curl -X DELETE http://localhost:8080/api/posts/1 -v
# Kết quả: 204 No Content
Hiểu PUT vs PATCH
Giả sử chúng ta có bài viết:
{"id": 1, "title": "Tiêu đề gốc", "content": "Nội dung gốc", "category": "TUTORIAL", "status": "DRAFT"}
PUT /api/posts/1 — Thay thế toàn bộ. Bạn phải gửi TẤT CẢ trường. Trường không gửi sẽ thành null.
PATCH /api/posts/1 — Cập nhật một phần. Chỉ gửi trường bạn muốn thay đổi. Các trường khác giữ nguyên.
Sự khác biệt quan trọng: PUT có nghĩa “thay thế toàn bộ tài nguyên này,” trong khi PATCH có nghĩa “chỉ cập nhật các trường cụ thể này.”
Bài tập
- Thêm endpoint publish: Tạo
PATCH /api/posts/{id}/publishthay đổi trạng thái bài viết thành “PUBLISHED” và đặt timestampupdatedAt. Không cần request body. - Thêm phân trang: Sửa
GET /api/postsđể nhận query parameterpagevàsize. Chỉ trả về bài viết cho trang yêu cầu. - Thêm endpoint tóm tắt: Tạo
GET /api/posts/summarytrả về danh sách bài viết chỉ cóid,title,status, vàcreatedAt(không có content). - Nhất quán lỗi: Tạo class
ErrorResponsethống nhất cho tất cả response lỗi trong controller.
Tổng kết
Trong bài giảng này, bạn đã xây dựng CRUD REST API hoàn chỉnh và học các khái niệm thiết yếu:
- Nguyên tắc REST: Tài nguyên được xác định bởi URI, thao tác qua phương thức HTTP, và biểu diễn dưới dạng JSON. Statelessness cho phép mở rộng.
- Phương thức HTTP ánh xạ CRUD: POST=Tạo, GET=Đọc, PUT=Cập nhật (toàn bộ), PATCH=Cập nhật (một phần), DELETE=Xóa.
- @RestController vs @Controller: Dùng
@RestControllercho REST API (trả về JSON),@Controllercho trang HTML render phía server. - Path variable và query parameter: Path variable xác định tài nguyên cụ thể. Query parameter lọc, sắp xếp, và phân trang.
- @RequestBody: Yêu cầu Spring deserialize phần thân HTTP request (JSON) thành đối tượng Java sử dụng Jackson.
- ResponseEntity: Cho bạn quyền kiểm soát mã trạng thái HTTP. Dùng
201 Createdcho POST,204 No Contentcho DELETE,404 Not Foundcho tài nguyên không tìm thấy. - Jackson: Thư viện JSON xử lý serialization (Java → JSON) và deserialization (JSON → Java).
- Phiên bản API: Phiên bản qua đường dẫn URL (
/v1/,/v2/) là chiến lược phổ biến nhất.
Bài tiếp theo
Trong Bài 4, chúng ta sẽ thiết lập MariaDB — cài đặt, thiết kế schema database cho blog, và làm mới kỹ năng SQL.
Tham chiếu nhanh
| Khái niệm | Mô tả |
|---|---|
| REST | Phong cách kiến trúc cho web API dựa trên tài nguyên và phương thức HTTP |
| Tài nguyên (Resource) | Thực thể được xác định bởi URI (/api/posts/42) |
| CRUD | Create (POST), Read (GET), Update (PUT/PATCH), Delete (DELETE) |
| Idempotent | Cùng kết quả bất kể gọi bao nhiêu lần (GET, PUT, DELETE) |
@RestController |
Controller trả về dữ liệu (JSON) thay vì tên view |
@RequestMapping |
Ánh xạ đường dẫn URL đến controller (cấp class = đường dẫn gốc) |
@PathVariable |
Trích xuất giá trị từ đường dẫn URL (/users/{id}) |
@RequestParam |
Trích xuất query parameter (?name=value) |
@RequestBody |
Deserialize JSON phần thân request thành đối tượng Java |
ResponseEntity |
Bọc response với mã trạng thái và header |
| Jackson | Thư viện serialization/deserialization JSON (đi kèm mặc định) |
| Content Negotiation | Client và server thỏa thuận định dạng dữ liệu qua header Accept/Content-Type |
