Backend

99 Ngày Spring — Ngày 12: Request mapping & tham số — @PathVariable, @RequestParam

SSite Admin
8 tháng 08, 2026 5 phút đọc 22 lượt xem
99 Ngày Spring — Ngày 12: Request mapping & tham số — @PathVariable, @RequestParam

Ngày 11, API của ta trả đúng một danh sách cứng — gọi kiểu gì cũng ra bấy nhiêu. API thật phải nghe được ý người gọi: "cho tôi bài viết số 42", "tìm bài chứa spring, trang 2". Hôm nay ta học hai kênh nhận ý đó — @PathVariable trong đường dẫn và @RequestParam sau dấu hỏi — cùng chuẩn mực thiết kế URL mà người phỏng vấn rất hay soi.

Sketchnote Ngày 12: @PathVariable, @RequestParam, quy tắc mapping và thiết kế URL chuẩn REST

@PathVariable — biến trong đường dẫn

Muốn /api/posts/42 thì đường dẫn phải có chỗ trống: khai {id} trong mapping rồi hứng bằng @PathVariable. Tiện thể, gom prefix chung của cả controller lên @RequestMapping cấp class:

@RestController@RequestMapping("/api/posts")            // prefix CHUNG cho cả classpublic class PostController {

    @GetMapping                          // GET /api/posts
    public List<Post> list() { ... }

    @GetMapping("/{id}")                 // GET /api/posts/42
    public Post detail(@PathVariable Long id) {
        return postService.find(id);     // "42" → Long 42 — Spring tự đổi kiểu
    }

    @GetMapping("/{id}/comments")        // GET /api/posts/42/comments
    public List<Comment> comments(@PathVariable Long id) { ... }
}

// Đường dẫn KHÔNG khớp kiểu? GET /api/posts/abc → 400 Bad Request luôn// (Spring không đổi được "abc" thành Long — chặn từ cửa)
  • Tên {id} khớp tên tham số là đủ — khác tên thì chỉ định @PathVariable("id").

  • Khai Long id thay vì String: Spring tự đổi kiểu, và /abc bị chặn 400 trước khi vào method — validation miễn phí tầng đầu.

  • Prefix cấp class là thói quen sạch: đổi /api/posts thành /api/v2/posts chỉ sửa một dòng.

@RequestParam — tham số sau dấu hỏi

Kênh thứ hai: query string — phần ?keyword=java&page=2 — dành cho lọc, phân trang, sắp xếp:

// GET /api/posts?keyword=java&page=2&size=10@GetMappingpublic List<Post> search(
        @RequestParam String keyword,                       // BẮT BUỘC — thiếu là 400
        @RequestParam(defaultValue = "0") int page,         // tùy chọn + mặc định
        @RequestParam(defaultValue = "10") int size,
        @RequestParam(required = false) String author) {    // tùy chọn, có thể null
    return postService.search(keyword, author, page, size);
}

// ?tag=java&tag=spring → gom vào danh sách:@GetMapping("/by-tags")public List<Post> byTags(@RequestParam List<String> tag) { ... }

// Mẹo: khai kiểu đúng (int, boolean, LocalDate...) — Spring convert giúp,// sai định dạng thì 400 ngay, khỏi tự parse chuỗi trong controller
  • Ba mức linh hoạt: bắt buộc (mặc định — thiếu là 400), defaultValue (vắng thì điền sẵn), required = false (vắng thì null — nhớ Ngày 09 mà phòng thân).

  • Bộ ba keyword/page/size chính là hình hài phân trang chuẩn — Ngày 18 nâng cấp lên Pageable của Spring Data.

  • Đừng nhận String rồi tự parseInt — khai đúng kiểu để Spring convert và trả lỗi chuẩn giúp bạn.

Chọn kênh nào? Định danh vs bộ lọc

Câu hỏi kinh điển: khi nào đường dẫn, khi nào query? Quy tắc một câu: định danh tài nguyên vào path, tùy chọn vào query:

// PATH VARIABLE — định danh tài nguyên: "CON NÀO?"
GET /api/posts/42            // bài viết số 42 — thiếu id thì URL vô nghĩa
GET /api/users/nam/posts     // bài của user nam

// REQUEST PARAM — bộ lọc/tùy chọn: "LỌC/SẮP XẾP THẾ NÀO?"
GET /api/posts?page=2&size=10&sort=views     // bỏ hết vẫn có nghĩa
GET /api/posts?keyword=spring&author=nam

// Đặt cạnh nhau cho quen mắt:
GET /api/posts/42/comments?page=0&size=20
//            └─ định danh ─┘└──── tùy chọn ────┘

// Quy ước URL tử tế:
//  ✓ danh từ SỐ NHIỀU: /posts, /users — KHÔNG động từ: /getPosts ✗
//  ✓ phân cấp theo quan hệ: /posts/42/comments
//  ✓ kebab-case: /api/reading-lists — không camelCase trong URL
  • Tự kiểm: bỏ hết query mà URL vẫn trỏ đúng "thứ đó" → thiết kế ổn; còn /api/posts/page/2 là mùi thiết kế — trang 2 không phải một tài nguyên.

  • URL là hợp đồng công khai của API — đổi sau này là phá client; đầu tư đặt tên từ hôm nay rẻ hơn nhiều so với đổi về sau.

Quy tắc mapping cần nhớ

// Một method — một cặp (động từ + đường dẫn). Trùng nhau là Boot từ chối chạy:// Ambiguous mapping. Cannot map 'postController' method ... — lỗi lúc KHỞI ĐỘNG, may!

@GetMapping("/{id}")         // GET  /api/posts/42  → đọc@PostMapping                 // POST /api/posts     → tạo (Ngày 13!)@PutMapping("/{id}")         // PUT  /api/posts/42  → sửa@DeleteMapping("/{id}")      // DELETE /api/posts/42 → xóa

// Cùng đường dẫn, KHÁC động từ → hợp lệ, và chính là bộ mặt REST chuẩn// Path cụ thể thắng path có biến: /posts/latest được thử TRƯỚC /posts/{id}
  • Trùng (động từ + path) là sập lúc khởi động — lỗi ồn ào là lỗi rẻ; đỡ hơn nhiều so với lặng lẽ gọi nhầm method.

  • Cùng path khác động từ là chuẩn REST: GET /posts/42 đọc, DELETE /posts/42 xóa — Ngày 13–15 lấp nốt POST/PUT với body và status code.

  • Boot còn tự xử lý tinh tế: /posts/latest cụ thể hơn nên được ưu tiên trước /posts/{id} — cứ khai, không cần lo thứ tự method.

Bài tập nhỏ

  • Thêm GET /api/books/{id} vào BookController hôm qua — thử /api/books/abc và quan sát 400.

  • Viết GET /api/books?minPrice=&maxPrice= lọc theo khoảng giá — dùng defaultValue cho hai đầu.

  • Thiết kế URL cho: "các bình luận của bài 42, trang 2, sắp theo mới nhất" — viết mapping đầy đủ.

  • Cố ý khai hai @GetMapping("/api/books") — đọc kỹ lỗi Ambiguous mapping để nhận diện sau này.

Kết luận

API đã biết lắng nghe: @PathVariable hứng định danh trong đường dẫn, @RequestParam hứng bộ lọc sau dấu hỏi, Spring convert kiểu và chặn dữ liệu xấu từ cửa, còn URL theo chuẩn danh-từ-số-nhiều phân cấp là bộ mặt chuyên nghiệp của service. Nhưng GET chỉ đọc — muốn tạo mới tài nguyên, dữ liệu phải đi trong request body. Ngày 13: @RequestBody, DTO, và vì sao đừng bao giờ phơi entity ra ngoài. Hẹn gặp lại!

S

Site Admin

Engineer and writer. Building things with TypeScript and distributed systems.

Bình luận (0)

Bạn cần đăng nhập bằng Google để bình luận.

Hãy là người bình luận đầu tiên.

Bài viết liên quan

99 Ngày Spring — Ngày 14: Bean Validation — @Valid & bộ constraint chuẩn

Ngày 14 của 99 Ngày Spring: Bean Validation khai báo luật ngay trên DTO — @Valid kích hoạt, bộ constraint chuẩn (@NotBlank vs @NotEmpty vs @NotNull, @Size, @Email, @Min/@Max), bẫy int vs Integer, validate object lồng nhau không tự lan, và lỗi 400 gom một lượt.

10 thg 8, 20265 phút0
99 Ngày Java — Ngày 14: Kế thừa (Inheritance)

Ngày 14 của 99 Ngày Java: extends trao gia tài từ lớp cha, override với @Override và super., thứ tự khai sinh cha trước con sau qua super(...), protected trả nợ Ngày 13, và phép thử is-a vs has-a — khi nào nên composition thay vì kế thừa.

10 thg 8, 20266 phút0
99 Ngày Spring — Ngày 13: @RequestBody & DTO — đừng công khai entity ra API

Ngày 13 của 99 Ngày Spring: @RequestBody + Jackson deserialize JSON thành object, DTO là hợp đồng API — chặn lỗ hổng mass assignment chiều vào và rò rỉ dữ liệu chiều ra, request/response DTO khác nhau là bình thường, và Jackson annotations tinh chỉnh hợp đồng JSON.

9 thg 8, 20266 phút9