Tạo REST API đầu tiên với Spring Boot
1. Tổng quan
Bài này hướng dẫn bạn tạo một REST API hoàn chỉnh với Spring Boot, dùng ví dụ quản lý Order (đơn hàng). Đọc xong, bạn sẽ dựng được project, viết controller xử lý CRUD, trả lỗi đúng chuẩn HTTP, và viết test cho API bằng MockMvc.
2. Khởi tạo project
Tạo project qua Spring Initializr với các dependency sau:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
spring-boot-starter-web cung cấp Tomcat nhúng và Spring MVC. spring-boot-starter-validation dùng để validate request body (mục 5). Phần spring-boot-starter-test đi kèm JUnit 5, AssertJ, MockMvc.
Kiểm tra lại application chạy được:
@SpringBootApplication
public class OrderApiApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApiApplication.class, args);
}
}
Chạy mvn spring-boot:run, bạn sẽ thấy Tomcat khởi động ở port 8080.
3. Định nghĩa model
public class Order {
private Long id;
private String customerName;
private BigDecimal amount;
public Order(Long id, String customerName, BigDecimal amount) {
this.id = id;
this.customerName = customerName;
this.amount = amount;
}
// getters, setters
}
Đây là model đơn giản, chưa dính tới JPA. Mục tiêu bài này là làm chủ layer REST trước, phần persistence để dành cho bài khác.
4. Viết Service lưu dữ liệu tạm
@Service
public class OrderService {
private final Map<Long, Order> store = new ConcurrentHashMap<>();
private final AtomicLong idSequence = new AtomicLong(0);
public Order create(Order order) {
long id = idSequence.incrementAndGet();
Order saved = new Order(id, order.getCustomerName(), order.getAmount());
store.put(id, saved);
return saved;
}
public List<Order> findAll() {
return new ArrayList<>(store.values());
}
public Optional<Order> findById(Long id) {
return Optional.ofNullable(store.get(id));
}
public void deleteById(Long id) {
store.remove(id);
}
}
ConcurrentHashMap đóng vai trò “database” tạm thời, giúp bạn chạy demo mà không cần cấu hình DB. AtomicLong sinh id tăng dần, an toàn khi nhiều thread cùng gọi create.
Lưu ý: Cách lưu dữ liệu này chỉ dùng để học, dữ liệu mất khi restart app. Production cần Spring Data JPA hoặc tương đương.
5. Viết REST Controller
5.1. Endpoint tạo và lấy danh sách
@RestController
@RequestMapping("/api/orders")
public class OrderController {
private final OrderService orderService;
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
@PostMapping
public ResponseEntity<Order> create(@Valid @RequestBody OrderRequest request) {
Order order = new Order(null, request.customerName(), request.amount());
Order saved = orderService.create(order);
return ResponseEntity.status(HttpStatus.CREATED).body(saved);
}
@GetMapping
public List<Order> findAll() {
return orderService.findAll();
}
}
@RestController kết hợp @Controller và @ResponseBody, trả JSON thay vì view. @RequestMapping("/api/orders") gắn prefix chung cho mọi endpoint trong lớp. @PostMapping nhận body qua @RequestBody, Spring tự convert JSON sang OrderRequest nhờ Jackson có sẵn trong spring-boot-starter-web.
OrderRequest là record dùng cho validate input, tách biệt với Order:
public record OrderRequest(
@NotBlank String customerName,
@NotNull @Positive BigDecimal amount
) {}
5.2. Endpoint lấy theo id và xóa
@GetMapping("/{id}")
public ResponseEntity<Order> findById(@PathVariable Long id) {
return orderService.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable Long id) {
orderService.deleteById(id);
return ResponseEntity.noContent().build();
}
@PathVariable lấy giá trị {id} từ URL. Khi không tìm thấy order, API trả về 404 Not Found thay vì null — đây là điểm dễ bị bỏ sót khi mới làm REST API.
Gọi thử bằng curl:
curl -X POST http://localhost:8080/api/orders \
-H "Content-Type: application/json" \
-d '{"customerName":"Nguyen Van A","amount":150000}'
HTTP/1.1 201 Created
Content-Type: application/json
{"id":1,"customerName":"Nguyen Van A","amount":150000}
6. Xử lý lỗi tập trung
Nếu không xử lý, validation lỗi sẽ trả về response JSON mặc định khó đọc. Dùng @RestControllerAdvice để chuẩn hóa:
@RestControllerAdvice
public class ApiExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidation(
MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors()
.forEach(error -> errors.put(error.getField(), error.getDefaultMessage()));
return ResponseEntity.badRequest().body(errors);
}
}
Lớp này bắt mọi MethodArgumentNotValidException ném ra từ @Valid, gom lỗi theo field rồi trả 400 Bad Request với body dễ đọc:
{
"amount": "must be greater than 0",
"customerName": "must not be blank"
}
Lưu ý: Đừng để exception mặc định của Spring lộ ra ngoài production — nó có thể expose stack trace, ảnh hưởng bảo mật.
7. Viết test cho Controller
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
@WebMvcTest(OrderController.class)
class OrderControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private OrderService orderService;
@Test
void shouldReturn201WhenCreateOrderSuccessfully() throws Exception {
Order saved = new Order(1L, "Tran Thi B", new BigDecimal("200000"));
given(orderService.create(any(Order.class))).willReturn(saved);
mockMvc.perform(post("/api/orders")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"customerName":"Tran Thi B","amount":200000}
"""))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.customerName").value("Tran Thi B"));
}
@Test
void shouldReturn404WhenOrderNotFound() throws Exception {
given(orderService.findById(99L)).willReturn(Optional.empty());
mockMvc.perform(get("/api/orders/99"))
.andExpect(status().isNotFound());
}
}
@WebMvcTest chỉ load layer web (controller, filter, validation), không khởi tạo toàn bộ context nên test chạy nhanh. @MockBean thay OrderService thật bằng mock, giúp test controller độc lập với logic service. Hai test case kiểm tra cả đường thành công lẫn đường lỗi — thói quen nên có với mọi endpoint.
Kiểm chứng thêm bằng AssertJ ở tầng service:
@Test
void shouldGenerateIncrementalIdWhenCreatingMultipleOrders() {
OrderService service = new OrderService();
Order first = service.create(new Order(null, "A", BigDecimal.TEN));
Order second = service.create(new Order(null, "B", BigDecimal.ONE));
assertThat(first.getId()).isEqualTo(1L);
assertThat(second.getId()).isEqualTo(2L);
}
8. Lỗi thường gặp
- Quên
@RequestBody: Spring sẽ không bind JSON vào object, field luônnullmà không báo lỗi rõ ràng. - Trả
nullthay vìResponseEntity.notFound(): client nhận200 OKvới bodynull, dễ gây bug phía frontend. - Không validate input: API nhận amount âm, tên rỗng… gây lỗi logic ở tầng dưới mà khó truy vết.
- Dùng
@Controllerthay vì@RestController: phải thêm@ResponseBodythủ công ở từng method, dễ quên.
Lưu ý: Nếu API của bạn phục vụ nhiều client (mobile, web, partner), hãy cân nhắc versioning ngay từ đầu (ví dụ
/api/v1/orders) để tránh breaking change về sau.
9. Kết luận
Bạn đã dựng được một REST API cơ bản với Spring Boot: từ model, service, controller, xử lý lỗi tập trung đến test bằng MockMvc. Bước tiếp theo, bạn nên thay ConcurrentHashMap bằng Spring Data JPA để dữ liệu persist thật sự, và thêm @RestControllerAdvice xử lý thêm các exception khác như EntityNotFoundException.