Back to blog

Best Practices Phát Triển REST API với Spring Boot

javaspring-bootrest-apibackendbest-practices
Best Practices Phát Triển REST API với Spring Boot

Giới Thiệu

Xây dựng một REST API hoạt động là một chuyện—xây dựng một API dễ maintain, scalable, và tuân theo các tiêu chuẩn ngành là chuyện khác. Trong bài viết này, chúng ta sẽ khám phá các best practices production-ready giúp phân biệt hobby projects với enterprise-grade APIs.

Những Gì Bạn Sẽ Học

✅ RESTful design principles và resource naming conventions
✅ HTTP status codes và semantic method usage
✅ Consistent response structures và error handling patterns
✅ Performance optimization với caching, compression, và rate limiting
✅ Security best practices cho production APIs
✅ Health checks, logging, và monitoring strategies
✅ Real-world patterns: filtering, sorting, pagination, soft deletes
✅ Xây dựng E-Commerce Product API với tất cả best practices

Yêu Cầu Tiên Quyết

Lưu ý: Bài viết này tập trung vào conventions và standards. Để biết chi tiết implementation về exception handling, validation, và testing, xem bài viết REST API Advanced Patterns.


1. RESTful Design Principles

Resource-Oriented Architecture

REST APIs nên model resources, không phải actions. Dùng nouns, không dùng verbs.

✅ Tốt:

GET    /products              # List products
GET    /products/{id}         # Get single product
POST   /products              # Create product
PUT    /products/{id}         # Update product
DELETE /products/{id}         # Delete product

❌ Không tốt:

GET    /getProducts
POST   /createProduct
POST   /updateProduct
DELETE /deleteProduct

Resource Naming Conventions

Quy tắc:

  1. Dùng plural nouns cho collections: /users, /orders, /products
  2. Dùng lowercase và hyphens cho readability: /order-items, /user-profiles
  3. Tránh file extensions: /products không phải /products.json
  4. Giữ URLs shallow (max 2-3 levels): /users/{id}/orders/{orderId}
  5. Dùng query parameters cho filtering: /products?category=electronics&sort=price

HTTP Methods Semantics

Mỗi HTTP method có ý nghĩa cụ thể và expected behavior:

MethodMục đíchIdempotentSafeRequest BodyResponse Body
GETRetrieve resource(s)✅✅❌✅
POSTCreate resource❌❌✅✅
PUTReplace entire resource✅❌✅✅
PATCHUpdate partial resource❌❌✅✅
DELETERemove resource✅❌❌Optional

Idempotent: Multiple identical requests có cùng effect như một single request. Safe: Không modify server state.


2. Response Standards

HTTP Status Codes Best Practices

Dùng status codes phù hợp để communicate intent rõ ràng.

Success Codes (2xx):

  • 200 OK: Successful GET, PUT, PATCH, DELETE với response body
  • 201 Created: Successful POST (resource created)
  • 204 No Content: Successful DELETE hoặc update không có response body

Client Error Codes (4xx):

  • 400 Bad Request: Invalid request body hoặc parameters
  • 401 Unauthorized: Authentication required hoặc invalid
  • 403 Forbidden: Authenticated nhưng không authorized
  • 404 Not Found: Resource không tồn tại
  • 409 Conflict: Request conflicts với current state
  • 422 Unprocessable Entity: Validation errors
  • 429 Too Many Requests: Rate limit exceeded

Server Error Codes (5xx):

  • 500 Internal Server Error: Unexpected server error
  • 503 Service Unavailable: Temporary outage

Consistent Response Structure

Option 1: Direct Response (Simple APIs)

{
  "id": 123,
  "name": "Laptop",
  "price": 999.99,
  "category": "electronics"
}

Option 2: Envelope Pattern (Complex APIs)

{
  "success": true,
  "data": {
    "content": [...],
    "pagination": {
      "page": 0,
      "size": 20,
      "totalElements": 150
    }
  },
  "timestamp": "2026-02-04T10:30:00Z"
}

3. API Design Patterns

Filtering, Sorting, và Searching

@RestController
@RequestMapping("/api/v1/products")
public class ProductController {
    
    @GetMapping
    public ResponseEntity<Page<Product>> getProducts(
            @RequestParam(required = false) String category,
            @RequestParam(required = false) BigDecimal minPrice,
            @RequestParam(required = false) BigDecimal maxPrice,
            @RequestParam(required = false) String search,
            @RequestParam(defaultValue = "name") String sortBy,
            @RequestParam(defaultValue = "asc") String sortDirection,
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "20") int size) {
        
        // Implementation
    }
}
 
// Example requests:
// GET /api/v1/products?category=electronics&minPrice=100&maxPrice=1000
// GET /api/v1/products?search=laptop&sortBy=price&sortDirection=desc

PATCH vs PUT

PUT: Replace entire resource (tất cả fields required)

@PutMapping("/{id}")
public ResponseEntity<Product> updateProduct(
        @PathVariable Long id,
        @RequestBody @Valid ProductRequest request) {
    // All fields trong ProductRequest là required
    Product product = productService.update(id, request);
    return ResponseEntity.ok(product);
}

PATCH: Update only provided fields (partial update)

@PatchMapping("/{id}")
public ResponseEntity<Product> patchProduct(
        @PathVariable Long id,
        @RequestBody Map<String, Object> updates) {
    // Chỉ update các fields có trong request
    Product product = productService.patch(id, updates);
    return ResponseEntity.ok(product);
}

Soft Delete Pattern

@Entity
public class Product {
    @Id
    private Long id;
    private String name;
    
    @Column(nullable = false)
    private Boolean deleted = false;
    
    private LocalDateTime deletedAt;
    
    public void softDelete() {
        this.deleted = true;
        this.deletedAt = LocalDateTime.now();
    }
}
 
@RestController
public class ProductController {
    
    // Soft delete (default behavior)
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteProduct(@PathVariable Long id) {
        productService.softDelete(id);
        return ResponseEntity.noContent().build();
    }
    
    // Restore soft-deleted product
    @PostMapping("/{id}/restore")
    @PreAuthorize("hasRole('ADMIN')")
    public ResponseEntity<Product> restoreProduct(@PathVariable Long id) {
        Product product = productService.restore(id);
        return ResponseEntity.ok(product);
    }
}

4. Performance & Scalability

Rate Limiting với Bucket4j

Prevent API abuse và ensure fair usage.

@Component
public class RateLimitInterceptor implements HandlerInterceptor {
    
    private final Map<String, Bucket> cache = new ConcurrentHashMap<>();
    
    @Override
    public boolean preHandle(
            HttpServletRequest request,
            HttpServletResponse response,
            Object handler) throws Exception {
        
        String apiKey = request.getHeader("X-API-Key");
        if (apiKey == null) {
            apiKey = request.getRemoteAddr();
        }
        
        Bucket bucket = resolveBucket(apiKey);
        
        if (bucket.tryConsume(1)) {
            return true;  // Allow request
        }
        
        // Rate limit exceeded
        response.setStatus(429);
        response.setHeader("X-Rate-Limit-Retry-After-Seconds", "60");
        response.getWriter().write("{\"error\": \"Rate limit exceeded\"}");
        return false;
    }
    
    private Bucket resolveBucket(String apiKey) {
        return cache.computeIfAbsent(apiKey, key -> {
            // 100 requests per minute
            Bandwidth limit = Bandwidth.simple(100, Duration.ofMinutes(1));
            return Bucket.builder().addLimit(limit).build();
        });
    }
}

ETags và Conditional Requests

Giảm bandwidth bằng cách gửi 304 Not Modified khi content không thay đổi.

@GetMapping("/{id}")
public ResponseEntity<Product> getProduct(
        @PathVariable Long id,
        @RequestHeader(value = "If-None-Match", required = false) String ifNoneMatch) {
    
    Product product = productService.findById(id)
        .orElseThrow(() -> new ResourceNotFoundException("Product not found"));
    
    String etag = "\"" + product.hashCode() + "\"";
    
    if (etag.equals(ifNoneMatch)) {
        return ResponseEntity
            .status(HttpStatus.NOT_MODIFIED)
            .eTag(etag)
            .build();
    }
    
    return ResponseEntity.ok()
        .eTag(etag)
        .body(product);
}

Cache-Control Headers

@GetMapping
public ResponseEntity<List<Product>> getProducts() {
    List<Product> products = productService.findAll();
    
    return ResponseEntity.ok()
        .cacheControl(CacheControl.maxAge(1, TimeUnit.HOURS).cachePublic())
        .body(products);
}

5. Security Considerations

Input Validation và Sanitization

public class ProductRequest {
    
    @NotBlank(message = "Name is required")
    @Size(min = 3, max = 100)
    @Pattern(regexp = "^[a-zA-Z0-9\\s-]+$")
    private String name;
    
    @NotNull
    @DecimalMin(value = "0.01")
    @DecimalMax(value = "999999.99")
    private BigDecimal price;
    
    @NotBlank
    @Pattern(regexp = "^(electronics|clothing|books|home|sports)$")
    private String category;
}

Security Headers

@Configuration
public class SecurityConfig {
    
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .headers(headers -> headers
                .contentSecurityPolicy(csp -> csp
                    .policyDirectives("default-src 'self'"))
                .frameOptions(frame -> frame.deny())
                .httpStrictTransportSecurity(hsts -> hsts
                    .includeSubDomains(true)
                    .maxAgeInSeconds(31536000))
            );
        return http.build();
    }
}

6. Production Readiness

Health Checks với Actuator

management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics
  endpoint:
    health:
      show-details: when-authorized
# Liveness probe
GET /actuator/health/liveness
 
# Readiness probe
GET /actuator/health/readiness

Structured Logging

@Service
public class ProductService {
    
    private static final Logger log = LoggerFactory.getLogger(ProductService.class);
    
    public Product create(ProductRequest request) {
        log.info("Creating product: {}",
            StructuredArguments.keyValue("productName", request.getName()));
        
        try {
            Product product = productRepository.save(Product.from(request));
            
            log.info("Product created successfully",
                StructuredArguments.keyValue("productId", product.getId()));
            
            return product;
            
        } catch (Exception e) {
            log.error("Failed to create product",
                StructuredArguments.keyValue("error", e.getMessage()), e);
            throw e;
        }
    }
}

Tổng Kết và Key Takeaways

✅ RESTful design: Dùng nouns, plural resources, proper HTTP methods
✅ HTTP status codes: 2xx success, 4xx client errors, 5xx server errors
✅ Consistent responses: Envelope pattern cho complex APIs
✅ Filtering & sorting: Dùng query parameters, support pagination
✅ PUT vs PATCH: PUT replaces, PATCH updates partially
✅ Soft deletes: Mark as deleted, giữ data cho recovery
✅ Rate limiting: Protect against abuse với Bucket4j
✅ ETags: Giảm bandwidth với conditional requests
✅ Caching: Dùng Cache-Control headers appropriately
✅ Security: Validate input, dùng security headers
✅ Monitoring: Health checks, structured logging, metrics
✅ Production-ready: Handle errors gracefully, log properly

Bước Tiếp Theo

Giờ bạn đã hiểu REST API best practices, khám phá các chủ đề liên quan:

Tiếp tục Spring Boot series:

Bài Viết Liên Quan:


Thuộc chuỗi bài Spring Boot Learning Roadmap

📬 Đă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.