Spring Boot Topic
Best Practices for REST API Design
Designing a REST API that is intuitive, maintainable, and scalable requires following established conventions and principles. Here's a comprehensive guide:
1. RESTful Principles & Naming Conventions
Use Nouns for Resources
# Good
GET /users
GET /users/123
POST /users
PUT /users/123
DELETE /users/123
# Avoid
GET /getUsers
POST /createUser
GET /getUserById/123
Use Plural Nouns
# Good
GET /users
GET /users/123/orders
GET /users/123/orders/456/items
# Avoid
GET /user
GET /user/123/order
Use HTTP Methods Properly
| Method | Description | Idempotent | Safe |
|---|---|---|---|
GET |
Retrieve resources | Yes | Yes |
POST |
Create new resource | No | No |
PUT |
Update/replace resource | Yes | No |
PATCH |
Partial update | No | No |
DELETE |
Delete resource | Yes | No |
2. API Versioning
URL Versioning (Most Common)
GET /api/v1/users
GET /api/v2/users
Header Versioning
GET /api/users
Accept: application/vnd.company.v1+json
Best Practices
-
Version from the start
-
Maintain backward compatibility
-
Document deprecation timelines
3. Request & Response Design
Filtering, Sorting, Pagination
# Filtering
GET /users?role=admin&status=active
# Sorting
GET /users?sort=name,-created_at # - for descending
# Pagination
GET /users?page=2&limit=20
# Field selection
GET /users?fields=id,name,email
Search Endpoints
# Use /search for complex queries
GET /users/search?q=john&location=ny
POST /users/search # For complex search criteria
4. HTTP Status Codes
Success Codes
-
200 OK- Successful GET, PUT, PATCH -
201 Created- Successful POST -
202 Accepted- Request accepted for processing -
204 No Content- Successful DELETE
Client Error Codes
-
400 Bad Request- Malformed request -
401 Unauthorized- Authentication needed -
403 Forbidden- Authenticated but no permission -
404 Not Found- Resource doesn't exist -
409 Conflict- Resource conflict
Server Error Codes
-
500 Internal Server Error- Generic server error -
502 Bad Gateway- Invalid response from upstream -
503 Service Unavailable- Temporary overload
5. Response Format
Consistent Response Wrapper
{
"data": {
"id": 123,
"name": "John Doe",
"email": "john@example.com"
},
"meta": {
"timestamp": "2024-01-15T10:30:00Z",
"version": "1.0"
}
}
Error Response Format
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Email is required",
"details": [
{
"field": "email",
"message": "Email cannot be blank"
}
],
"timestamp": "2024-01-15T10:30:00Z",
"traceId": "abc-123-xyz"
}
}
Pagination Response
{
"data": [...],
"pagination": {
"page": 2,
"limit": 20,
"total": 150,
"pages": 8
},
"links": {
"first": "/users?page=1&limit=20",
"prev": "/users?page=1&limit=20",
"next": "/users?page=3&limit=20",
"last": "/users?page=8&limit=20"
}
}
6. Security Best Practices
Authentication & Authorization
# Use Bearer tokens
Authorization: Bearer <jwt-token>
# API Keys for server-to-server
X-API-Key: <api-key>
Security Headers
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Content-Security-Policy: default-src 'self'
Rate Limiting
# Response headers for rate limiting
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 999
X-RateLimit-Reset: 1642230000
Retry-After: 60
7. Data Validation
Request Validation
// Example using Java Validation API
public class CreateUserRequest {
@NotBlank
@Email
private String email;
@Size(min = 8, max = 100)
private String password;
@NotNull
private UserRole role;
}
Validation Error Response
{
"error": {
"code": "VALIDATION_FAILED",
"message": "Request validation failed",
"details": [
{
"field": "email",
"message": "Must be a valid email address"
},
{
"field": "password",
"message": "Password must be at least 8 characters"
}
]
}
}
8. API Documentation
OpenAPI/Swagger Specification
openapi: 3.0.0
info:
title: User API
version: 1.0.0
paths:
/users:
get:
summary: Get all users
parameters:
- name: page
in: query
schema:
type: integer
minimum: 1
responses:
'200':
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/UserList'
9. Performance Optimization
Caching Headers
# Cache control
Cache-Control: public, max-age=300
ETag: "abc123"
Last-Modified: Mon, 15 Jan 2024 10:30:00 GMT
# Conditional requests
If-None-Match: "abc123"
If-Modified-Since: Mon, 15 Jan 2024 10:30:00 GMT
Compression
Accept-Encoding: gzip, deflate Content-Encoding: gzip
Partial Responses (GraphQL-like)
GET /users/123?fields=id,name,email GET /users/123?exclude=password,ssn
10. Code Structure & Organization
Controller Layer (Spring Boot Example)
@RestController
@RequestMapping("/api/v1/users")
@Validated
public class UserController {
@GetMapping
public ResponseEntity<PageResponse<UserResponse>> getUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(required = false) String sort) {
// Implementation
}
@GetMapping("/{id}")
public ResponseEntity<UserResponse> getUser(@PathVariable Long id) {
// Implementation
}
@PostMapping
public ResponseEntity<UserResponse> createUser(
@Valid @RequestBody CreateUserRequest request) {
// Implementation
}
@PutMapping("/{id}")
public ResponseEntity<UserResponse> updateUser(
@PathVariable Long id,
@Valid @RequestBody UpdateUserRequest request) {
// Implementation
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
// Implementation
return ResponseEntity.noContent().build();
}
}
Request/Response DTOs
public class CreateUserRequest {
@NotBlank
private String name;
@Email
private String email;
// Getters and setters
}
public class UserResponse {
private Long id;
private String name;
private String email;
private Instant createdAt;
// Getters and setters
}
11. Testing Best Practices
Integration Tests
@SpringBootTest
@AutoConfigureTestDatabase
class UserControllerTest {
@Test
void shouldCreateUser() {
// Given
CreateUserRequest request = new CreateUserRequest("John", "john@test.com");
// When
var response = restTemplate.postForEntity("/api/v1/users", request, UserResponse.class);
// Then
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
assertThat(response.getBody().getId()).isNotNull();
}
}
12. Monitoring & Logging
Structured Logging
@RestController
public class UserController {
private static final Logger logger = LoggerFactory.getLogger(UserController.class);
@GetMapping("/{id}")
public UserResponse getUser(@PathVariable Long id) {
logger.info("Get user request",
kv("userId", id),
kv("clientIp", getClientIp()));
// Implementation
}
}
Health Check Endpoint
GET /health
{
"status": "UP",
"components": {
"database": {
"status": "UP",
"details": {
"responseTime": 45
}
},
"diskSpace": {
"status": "UP"
}
}
}
13. Common Anti-Patterns to Avoid
Don't Do This
# Using verbs in URLs POST /getUsers GET /createUser # Returning plain errors HTTP 500 - Internal Server Error # Inconsistent naming GET /users GET /userOrders # No versioning GET /api/users # Returning HTML errors for API <html><body>Error 500</body></html>
In short
-
Use nouns for resources, verbs for HTTP methods
-
Implement proper HTTP status codes
-
Version your API from the start
-
Use consistent error response format
-
Implement authentication and authorization
-
Add rate limiting and throttling
-
Provide comprehensive documentation
-
Implement proper caching headers
-
Use pagination for large datasets
-
Validate all input data
-
Log appropriately for monitoring
-
Provide health check endpoints
-
Use HTTPS exclusively
-
Implement proper CORS policies
Following these best practices will result in a REST API that is secure, scalable, maintainable, and developer-friendly.
Comments (0)
No comments yet. Be the first to comment!