API Design trong Java
1. Lý thuyết và Khái niệm Cơ bản
1.1 API Design Principles
- RESTful Design
- GraphQL
- gRPC
- WebSocket
- API Versioning
- API Documentation
- Error Handling
- Security
- Rate Limiting
- Caching
1.2 API Standards
- HTTP Methods (GET, POST, PUT, DELETE, etc.)
- Status Codes
- Resource Naming
- Query Parameters
- Request/Response Headers
- Content Negotiation
- Authentication/Authorization
- Pagination
- Filtering
- Sorting
2. Best Practices và Design Patterns
2.1 RESTful API Design
@RestController
@RequestMapping("/api/v1/orders")
public class OrderController {
private final OrderService orderService;
@GetMapping
public ResponseEntity<Page<OrderDTO>> getOrders(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(defaultValue = "createdAt") String sortBy) {
Pageable pageable = PageRequest.of(page, size, Sort.by(sortBy));
return ResponseEntity.ok(orderService.findAll(pageable));
}
@GetMapping("/{id}")
public ResponseEntity<OrderDTO> getOrder(@PathVariable Long id) {
return orderService.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public ResponseEntity<OrderDTO> createOrder(
@Valid @RequestBody OrderRequest request) {
OrderDTO order = orderService.createOrder(request);
URI location = ServletUriComponentsBuilder
.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(order.getId())
.toUri();
return ResponseEntity.created(location).body(order);
}
@PutMapping("/{id}")
public ResponseEntity<OrderDTO> updateOrder(
@PathVariable Long id,
@Valid @RequestBody OrderRequest request) {
return ResponseEntity.ok(orderService.updateOrder(id, request));
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteOrder(@PathVariable Long id) {
orderService.deleteOrder(id);
return ResponseEntity.noContent().build();
}
}
2.2 GraphQL API Design
@Controller
public class GraphQLOrderController {
private final OrderService orderService;
@QueryMapping
public Page<Order> orders(
@Argument int page,
@Argument int size,
@Argument String sortBy) {
return orderService.findAll(
PageRequest.of(page, size, Sort.by(sortBy)));
}
@QueryMapping
public Order order(@Argument Long id) {
return orderService.findById(id)
.orElseThrow(() -> new OrderNotFoundException(id));
}
@MutationMapping
public Order createOrder(@Argument OrderInput input) {
return orderService.createOrder(input);
}
@SchemaMapping(typeName = "Order")
public Customer customer(Order order) {
return orderService.getCustomer(order.getCustomerId());
}
}
type Order {
id: ID!
customer: Customer!
items: [OrderItem!]!
total: Float!
status: OrderStatus!
createdAt: DateTime!
}
input OrderInput {
customerId: ID!
items: [OrderItemInput!]!
}
enum OrderStatus {
PENDING
PROCESSING
COMPLETED
CANCELLED
}
3. Anti-patterns và Common Pitfalls
3.1 API Design Anti-patterns
- Breaking Changes in Public APIs
- Inconsistent Error Responses
- Poor Resource Naming
- Exposing Internal Implementation
- No API Documentation
- Ignoring HTTP Standards
3.2 Common Mistakes
// Bad Practice: Inconsistent Error Handling
@RestController
public class BadApiController {
@GetMapping("/api/users/{id}")
public User getUser(@PathVariable Long id) {
try {
return userService.findById(id);
} catch (Exception e) {
return null; // Bad: Inconsistent error response
}
}
}
// Good Practice: Consistent Error Handling
@RestController
@RequestMapping("/api/users")
public class GoodApiController {
@GetMapping("/{id}")
public ResponseEntity<UserDTO> getUser(@PathVariable Long id) {
return userService.findById(id)
.map(ResponseEntity::ok)
.orElseThrow(() -> new ResourceNotFoundException(
"User not found with id: " + id));
}
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleResourceNotFound(
ResourceNotFoundException ex) {
ErrorResponse error = new ErrorResponse(
HttpStatus.NOT_FOUND.value(),
ex.getMessage());
return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
}
}
4. Ví dụ Code Thực tế
4.1 API Documentation
@RestController
@RequestMapping("/api/v1/products")
@Tag(name = "Product API", description = "Product management APIs")
public class ProductController {
@Operation(
summary = "Get all products",
description = "Returns a paginated list of products"
)
@ApiResponses({
@ApiResponse(
responseCode = "200",
description = "Successfully retrieved products",
content = @Content(
array = @ArraySchema(
schema = @Schema(implementation = ProductDTO.class)
)
)
),
@ApiResponse(
responseCode = "400",
description = "Invalid parameters",
content = @Content(
schema = @Schema(implementation = ErrorResponse.class)
)
)
})
@GetMapping
public ResponseEntity<Page<ProductDTO>> getProducts(
@Parameter(description = "Page number (0-based)")
@RequestParam(defaultValue = "0") int page,
@Parameter(description = "Page size")
@RequestParam(defaultValue = "10") int size) {
return ResponseEntity.ok(productService.findAll(
PageRequest.of(page, size)));
}
}
4.2 API Versioning
@RestController
public class VersionedApiController {
// URL Versioning
@GetMapping("/api/v1/users")
public List<UserV1DTO> getUsersV1() {
return userService.findAllV1();
}
@GetMapping("/api/v2/users")
public Page<UserV2DTO> getUsersV2(Pageable pageable) {
return userService.findAllV2(pageable);
}
// Header Versioning
@GetMapping(value = "/api/users",
headers = "X-API-VERSION=1")
public List<UserV1DTO> getUsersByHeader() {
return userService.findAllV1();
}
// Media Type Versioning
@GetMapping(value = "/api/users",
produces = "application/vnd.company.app-v1+json")
public List<UserV1DTO> getUsersByMediaType() {
return userService.findAllV1();
}
}
5. Use Cases và Scenarios
5.1 CRUD Operations
@RestController
@RequestMapping("/api/v1/customers")
public class CustomerController {
private final CustomerService customerService;
@GetMapping
public ResponseEntity<Page<CustomerDTO>> getCustomers(
CustomerSearchCriteria criteria,
Pageable pageable) {
return ResponseEntity.ok(
customerService.findAll(criteria, pageable));
}
@GetMapping("/{id}")
public ResponseEntity<CustomerDTO> getCustomer(
@PathVariable Long id) {
return customerService.findById(id)
.map(ResponseEntity::ok)
.orElseThrow(() -> new ResourceNotFoundException(id));
}
@PostMapping
public ResponseEntity<CustomerDTO> createCustomer(
@Valid @RequestBody CustomerRequest request) {
CustomerDTO customer = customerService.create(request);
return ResponseEntity
.created(getLocation(customer.getId()))
.body(customer);
}
@PutMapping("/{id}")
public ResponseEntity<CustomerDTO> updateCustomer(
@PathVariable Long id,
@Valid @RequestBody CustomerRequest request) {
return ResponseEntity.ok(
customerService.update(id, request));
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteCustomer(
@PathVariable Long id) {
customerService.delete(id);
return ResponseEntity.noContent().build();
}
}
5.2 Batch Operations
@RestController
@RequestMapping("/api/v1/products")
public class ProductBatchController {
@PostMapping("/batch")
public ResponseEntity<BatchResult> createProducts(
@Valid @RequestBody List<ProductRequest> requests) {
BatchResult result = productService.createBatch(requests);
return ResponseEntity.ok(result);
}
@PutMapping("/batch")
public ResponseEntity<BatchResult> updateProducts(
@Valid @RequestBody List<ProductUpdateRequest> requests) {
BatchResult result = productService.updateBatch(requests);
return ResponseEntity.ok(result);
}
@DeleteMapping("/batch")
public ResponseEntity<Void> deleteProducts(
@RequestBody List<Long> ids) {
productService.deleteBatch(ids);
return ResponseEntity.noContent().build();
}
}
6.1 Response Caching
@Configuration
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
return new ConcurrentMapCacheManager("products");
}
}
@RestController
@RequestMapping("/api/v1/products")
public class CachedProductController {
@Cacheable(value = "products", key = "#id")
@GetMapping("/{id}")
public ResponseEntity<ProductDTO> getProduct(@PathVariable Long id) {
return ResponseEntity.ok(productService.findById(id));
}
@CacheEvict(value = "products", key = "#id")
@PutMapping("/{id}")
public ResponseEntity<ProductDTO> updateProduct(
@PathVariable Long id,
@RequestBody ProductRequest request) {
return ResponseEntity.ok(productService.update(id, request));
}
}
@RestController
@RequestMapping("/api/v1/orders")
public class OrderSearchController {
@GetMapping("/search")
public ResponseEntity<Page<OrderDTO>> searchOrders(
@RequestParam(required = false) String customerName,
@RequestParam(required = false) OrderStatus status,
@RequestParam(required = false)
@DateTimeFormat(iso = DATE) LocalDate fromDate,
@RequestParam(required = false)
@DateTimeFormat(iso = DATE) LocalDate toDate,
@PageableDefault(size = 20) Pageable pageable) {
OrderSearchCriteria criteria = OrderSearchCriteria.builder()
.customerName(customerName)
.status(status)
.fromDate(fromDate)
.toDate(toDate)
.build();
return ResponseEntity.ok(
orderService.search(criteria, pageable));
}
}
7. Security Considerations
7.1 Authentication
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http)
throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/api/public/**").permitAll()
.antMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.oauth2ResourceServer()
.jwt();
return http.build();
}
}
7.2 Rate Limiting
@Configuration
public class RateLimitConfig {
@Bean
public RateLimiter rateLimiter() {
return RateLimiter.create(100.0); // 100 requests per second
}
}
@Component
public class RateLimitInterceptor implements HandlerInterceptor {
private final RateLimiter rateLimiter;
@Override
public boolean preHandle(
HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
if (!rateLimiter.tryAcquire()) {
response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
return false;
}
return true;
}
}
8. Testing Strategies
8.1 API Testing
@SpringBootTest(webEnvironment = RANDOM_PORT)
public class ProductApiTest {
@Autowired
private TestRestTemplate restTemplate;
@Test
public void whenGetProduct_thenSuccess() {
// given
Long productId = 1L;
// when
ResponseEntity<ProductDTO> response = restTemplate
.getForEntity("/api/v1/products/{id}",
ProductDTO.class,
productId);
// then
assertThat(response.getStatusCode())
.isEqualTo(HttpStatus.OK);
assertThat(response.getBody())
.isNotNull()
.hasFieldOrPropertyWithValue("id", productId);
}
}
8.2 Contract Testing
@SpringBootTest
public class ProductContractTest {
@Autowired
private MockMvc mockMvc;
@Test
public void validateGetProductContract() throws Exception {
mockMvc.perform(get("/api/v1/products/{id}", 1)
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").exists())
.andExpect(jsonPath("$.name").exists())
.andExpect(jsonPath("$.price").isNumber())
.andDo(document("get-product",
pathParameters(
parameterWithName("id")
.description("Product ID")
),
responseFields(
fieldWithPath("id")
.description("Product ID"),
fieldWithPath("name")
.description("Product name"),
fieldWithPath("price")
.description("Product price")
)
));
}
}
9. Monitoring và Troubleshooting
9.1 API Metrics
@Configuration
public class MetricsConfig {
@Bean
MeterRegistryCustomizer<MeterRegistry> metricsCommonTags() {
return registry -> registry.config()
.commonTags("application", "api-service");
}
}
@Aspect
@Component
public class ApiMetricsAspect {
private final MeterRegistry registry;
@Around("@annotation(org.springframework.web.bind.annotation.RequestMapping)")
public Object measureApiTiming(ProceedingJoinPoint point)
throws Throwable {
Timer.Sample sample = Timer.start(registry);
try {
return point.proceed();
} finally {
sample.stop(registry.timer("api.request",
"method", point.getSignature().getName()));
}
}
}
9.2 API Logging
@Aspect
@Component
public class ApiLoggingAspect {
private static final Logger log =
LoggerFactory.getLogger(ApiLoggingAspect.class);
@Around("@annotation(org.springframework.web.bind.annotation.RequestMapping)")
public Object logApiCall(ProceedingJoinPoint point)
throws Throwable {
String methodName = point.getSignature().getName();
Object[] args = point.getArgs();
log.info("API Call - Method: {}, Args: {}",
methodName,
Arrays.toString(args));
try {
Object result = point.proceed();
log.info("API Response - Method: {}, Result: {}",
methodName,
result);
return result;
} catch (Exception e) {
log.error("API Error - Method: {}, Error: {}",
methodName,
e.getMessage());
throw e;
}
}
}
10. References và Further Reading
10.1 API Design Resources
- RESTful Web Services Cookbook
- API Design Patterns
- GraphQL in Action
- Building Microservices
- REST API Design Rulebook
- Spring Framework
- GraphQL Java
- Swagger/OpenAPI
- Postman
- JUnit/REST Assured
- Spring Cloud Contract
- API Gateway Tools