REST API Best Practices
REST (Representational State Transfer) is an architectural style for designing networked applications. This guide covers best practices for building robust, scalable REST APIs.
Core Principles
1. Use HTTP Methods Correctly
- GET: Retrieve resources (idempotent, safe)
- POST: Create new resources
- PUT: Update/replace entire resource (idempotent)
- PATCH: Partial update of resource
- DELETE: Remove resource (idempotent)
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping
public List<User> getAllUsers() { }
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) { }
@PostMapping
public User createUser(@RequestBody User user) { }
@PutMapping("/{id}")
public User updateUser(@PathVariable Long id, @RequestBody User user) { }
@PatchMapping("/{id}")
public User partialUpdate(@PathVariable Long id, @RequestBody Map<String, Object> updates) { }
@DeleteMapping("/{id}")
public void deleteUser(@PathVariable Long id) { }
}
2. Use Proper HTTP Status Codes
@PostMapping
public ResponseEntity<User> createUser(@RequestBody User user) {
User created = userService.save(user);
return ResponseEntity
.status(HttpStatus.CREATED)
.body(created);
}
@GetMapping("/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
return userService.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userService.delete(id);
return ResponseEntity.noContent().build();
}
Common Status Codes
| Code | Meaning | Use Case |
|---|---|---|
| 200 | OK | Successful GET, PUT, PATCH |
| 201 | Created | Successful POST |
| 204 | No Content | Successful DELETE |
| 400 | Bad Request | Invalid input |
| 401 | Unauthorized | Authentication required |
| 403 | Forbidden | Insufficient permissions |
| 404 | Not Found | Resource doesn't exist |
| 409 | Conflict | Resource conflict |
| 422 | Unprocessable Entity | Validation error |
| 500 | Internal Server Error | Server error |
URL Design
1. Use Nouns, Not Verbs
✅ Good
GET /api/users
POST /api/users
GET /api/users/123
PUT /api/users/123
DELETE /api/users/123
❌ Bad
GET /api/getUsers
POST /api/createUser
GET /api/getUserById/123
2. Use Plural Nouns
✅ Good: /api/users
❌ Bad: /api/user
3. Nested Resources
GET /api/users/123/orders
GET /api/users/123/orders/456
POST /api/users/123/orders
4. Filtering, Sorting, Pagination
GET /api/users?status=active&role=admin
GET /api/users?sort=createdAt&order=desc
GET /api/users?page=2&size=20
GET /api/users?search=john
@GetMapping
public Page<User> getUsers(
@RequestParam(required = false) String status,
@RequestParam(required = false) String role,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(defaultValue = "id") String sort
) {
Pageable pageable = PageRequest.of(page, size, Sort.by(sort));
return userService.findAll(status, role, pageable);
}
Request/Response Design
1. Use JSON
{
"id": 123,
"name": "John Doe",
"email": "[email protected]",
"createdAt": "2024-01-15T10:30:00Z"
}
2. Consistent Naming Convention
Use camelCase for JSON properties:
{
"firstName": "John",
"lastName": "Doe",
"phoneNumber": "+1234567890"
}
3. Error Response Format
public class ErrorResponse {
private String message;
private int status;
private LocalDateTime timestamp;
private List<String> errors;
// constructors, getters, setters
}
@ExceptionHandler(ValidationException.class)
public ResponseEntity<ErrorResponse> handleValidation(ValidationException ex) {
ErrorResponse error = new ErrorResponse(
"Validation failed",
400,
LocalDateTime.now(),
ex.getErrors()
);
return ResponseEntity.badRequest().body(error);
}
{
"message": "Validation failed",
"status": 400,
"timestamp": "2024-01-15T10:30:00Z",
"errors": [
"Email is required",
"Password must be at least 8 characters"
]
}
4. Pagination Response
public class PageResponse<T> {
private List<T> content;
private int page;
private int size;
private long totalElements;
private int totalPages;
private boolean last;
}
{
"content": [...],
"page": 0,
"size": 20,
"totalElements": 100,
"totalPages": 5,
"last": false
}
Versioning
1. URI Versioning (Recommended)
@RestController
@RequestMapping("/api/v1/users")
public class UserControllerV1 { }
@RestController
@RequestMapping("/api/v2/users")
public class UserControllerV2 { }
2. Header Versioning
@GetMapping(headers = "API-Version=1")
public List<User> getUsersV1() { }
@GetMapping(headers = "API-Version=2")
public List<User> getUsersV2() { }
3. Content Negotiation
@GetMapping(produces = "application/vnd.company.v1+json")
public List<User> getUsersV1() { }
@GetMapping(produces = "application/vnd.company.v2+json")
public List<User> getUsersV2() { }
Security
1. Authentication with JWT
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated()
)
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
2. Rate Limiting
@Component
public class RateLimitFilter extends OncePerRequestFilter {
private final Map<String, List<Long>> requestCounts = new ConcurrentHashMap<>();
private static final int MAX_REQUESTS = 100;
private static final long TIME_WINDOW = 60000; // 1 minute
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String clientId = getClientId(request);
if (isRateLimitExceeded(clientId)) {
response.setStatus(429);
response.getWriter().write("Rate limit exceeded");
return;
}
filterChain.doFilter(request, response);
}
}
3. Input Validation
public class UserRequest {
@NotBlank(message = "Name is required")
@Size(min = 2, max = 50)
private String name;
@Email(message = "Invalid email format")
@NotBlank
private String email;
@Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d]{8,}$")
private String password;
@Min(18)
@Max(120)
private Integer age;
}
@PostMapping
public ResponseEntity<User> createUser(@Valid @RequestBody UserRequest request) {
// Process request
}
4. CORS Configuration
@Configuration
public class CorsConfig {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://example.com")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
};
}
}
Documentation
Swagger/OpenAPI
@Configuration
public class SwaggerConfig {
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("User API")
.version("1.0")
.description("API for managing users")
.contact(new Contact()
.name("API Support")
.email("[email protected]")))
.addSecurityItem(new SecurityRequirement().addList("bearerAuth"))
.components(new Components()
.addSecuritySchemes("bearerAuth",
new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")));
}
}
@RestController
@RequestMapping("/api/users")
@Tag(name = "User Management", description = "APIs for managing users")
public class UserController {
@Operation(summary = "Get all users", description = "Returns a list of all users")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Successful operation"),
@ApiResponse(responseCode = "401", description = "Unauthorized")
})
@GetMapping
public List<User> getAllUsers() { }
}
Performance Optimization
1. Caching
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
return new ConcurrentMapCacheManager("users", "products");
}
}
@Service
public class UserService {
@Cacheable(value = "users", key = "#id")
public User findById(Long id) {
return userRepository.findById(id).orElseThrow();
}
@CacheEvict(value = "users", key = "#user.id")
public User update(User user) {
return userRepository.save(user);
}
}
2. Compression
server.compression.enabled=true
server.compression.mime-types=application/json,application/xml,text/html,text/xml,text/plain
server.compression.min-response-size=1024
3. Async Processing
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(100);
executor.initialize();
return executor;
}
}
@Service
public class NotificationService {
@Async
public CompletableFuture<Void> sendEmail(String to, String subject) {
// Send email
return CompletableFuture.completedFuture(null);
}
}
Testing
Unit Tests
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Test
void shouldReturnUser() throws Exception {
User user = new User(1L, "John", "[email protected]");
when(userService.findById(1L)).thenReturn(Optional.of(user));
mockMvc.perform(get("/api/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("John"))
.andExpect(jsonPath("$.email").value("[email protected]"));
}
@Test
void shouldReturn404WhenUserNotFound() throws Exception {
when(userService.findById(1L)).thenReturn(Optional.empty());
mockMvc.perform(get("/api/users/1"))
.andExpect(status().isNotFound());
}
}
Integration Tests
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class UserApiIntegrationTest {
@Autowired
private TestRestTemplate restTemplate;
@Test
void shouldCreateUser() {
UserRequest request = new UserRequest("John", "[email protected]");
ResponseEntity<User> response = restTemplate.postForEntity(
"/api/users",
request,
User.class
);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
assertThat(response.getBody().getName()).isEqualTo("John");
}
}
Monitoring
Actuator Endpoints
management.endpoints.web.exposure.include=health,info,metrics,prometheus
management.endpoint.health.show-details=always
management.metrics.export.prometheus.enabled=true
Custom Metrics
@Service
public class UserService {
private final MeterRegistry meterRegistry;
private final Counter userCreatedCounter;
public UserService(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
this.userCreatedCounter = Counter.builder("users.created")
.description("Number of users created")
.register(meterRegistry);
}
public User create(User user) {
User saved = userRepository.save(user);
userCreatedCounter.increment();
return saved;
}
}
Best Practices Summary
- Use proper HTTP methods and status codes
- Design intuitive, consistent URLs
- Implement proper error handling
- Version your API
- Secure your endpoints
- Validate all inputs
- Document your API
- Implement pagination for large datasets
- Use caching where appropriate
- Monitor and log API usage
- Write comprehensive tests
- Handle rate limiting
- Use HTTPS in production
- Implement proper CORS policies
- Keep responses consistent
- Go back to Frameworks
- Return to Home