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. Performance Considerations

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));
    }
}

6.2 Pagination và Filtering

@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

10.2 Tools và Frameworks

  • Spring Framework
  • GraphQL Java
  • Swagger/OpenAPI
  • Postman
  • JUnit/REST Assured
  • Spring Cloud Contract
  • API Gateway Tools