HTTP Topic

REST API Design Best Practices

What You'll Learn:

  • Richardson Maturity Model for REST APIs

  • Resource naming conventions and URL design

  • Hypermedia (HATEOAS) and API discoverability

Detailed Explanation:

Richardson Maturity Model

Level 0 - The Swamp of POX (Plain Old XML):

  • Single POST endpoint

  • All operations via POST

  • No HTTP semantics

Level 1 - Resources:

  • Multiple URLs for different resources

  • Still mostly POST operations

Level 2 - HTTP Verbs:

  • Proper use of HTTP methods (GET, POST, PUT, DELETE)

  • HTTP status codes for responses

Level 3 - Hypermedia Controls (HATEOAS):

  • Links for resource navigation

  • Self-describing APIs

Resource Design Principles

1. Use Nouns for Resources:

# Good
GET /api/users
GET /api/users/123
POST /api/users

# Avoid
GET /api/getUser
POST /api/createUser

2. Use Plural Resource Names:

# Good
GET /api/users
GET /api/users/123/orders

# Avoid
GET /api/user
GET /api/user/123/order

3. Use Sub-resources for Relations:

GET /api/users/123/orders          # User's orders
GET /api/users/123/orders/456      # Specific order for user
POST /api/users/123/orders         # Create order for user

4. Use Query Parameters for Filtering:

GET /api/users?role=admin&status=active
GET /api/orders?startDate=2023-01-01&endDate=2023-01-31
GET /api/products?category=electronics&priceRange=100-500

API Versioning Strategies

1. URL Versioning:

GET /api/v1/users
GET /api/v2/users

2. Query Parameter Versioning:

GET /api/users?version=1
GET /api/users?version=2

3. Header Versioning:

GET /api/users
API-Version: 1

GET /api/users
API-Version: 2

4. Content Negotiation Versioning:

GET /api/users
Accept: application/vnd.company.v1+json

GET /api/users
Accept: application/vnd.company.v2+json
HATEOAS (Hypermedia as the Engine of Application State)

Spring HATEOAS Implementation:

@RestController
@RequestMapping("/api/users")
public class UserController {
    
    @GetMapping("/{id}")
    public EntityModel<User> getUser(@PathVariable Long id) {
        User user = userService.findById(id);
        
        return EntityModel.of(user,
            linkTo(methodOn(UserController.class).getUser(id)).withSelfRel(),
            linkTo(methodOn(UserController.class).getUserOrders(id)).withRel("orders"),
            linkTo(methodOn(UserController.class).updateUser(id, null)).withRel("update"),
            linkTo(methodOn(UserController.class).deleteUser(id)).withRel("delete")
        );
    }
    
    @GetMapping
    public CollectionModel<EntityModel<User>> getUsers(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "20") int size) {
        
        List<EntityModel<User>> users = userService.findPage(page, size)
            .stream()
            .map(user -> EntityModel.of(user,
                linkTo(methodOn(UserController.class).getUser(user.getId())).withSelfRel()
            ))
            .collect(Collectors.toList());
        
        Link selfLink = linkTo(methodOn(UserController.class).getUsers(page, size)).withSelfRel();
        Link nextLink = linkTo(methodOn(UserController.class).getUsers(page + 1, size)).withRel("next");
        Link prevLink = page > 0 ? 
            linkTo(methodOn(UserController.class).getUsers(page - 1, size)).withRel("prev") : null;
        
        CollectionModel<EntityModel<User>> collection = CollectionModel.of(users, selfLink);
        if (prevLink != null) {
            collection.add(prevLink);
        }
        collection.add(nextLink);
        
        return collection;
    }
}

HATEOAS Response Example:

json

{
  "id": 123,
  "name": "John Doe",
  "email": "john@example.com",
  "_links": {
    "self": {
      "href": "http://localhost:8080/api/users/123"
    },
    "orders": {
      "href": "http://localhost:8080/api/users/123/orders"
    },
    "update": {
      "href": "http://localhost:8080/api/users/123"
    },
    "delete": {
      "href": "http://localhost:8080/api/users/123"
    }
  }
}

Pagination and Sorting

Pagination Implementation:

@GetMapping
public ResponseEntity<Page<User>> getUsers(
        @RequestParam(defaultValue = "0") int page,
        @RequestParam(defaultValue = "20") int size,
        @RequestParam(defaultValue = "name,asc") String[] sort) {
    
    Pageable pageable = PageRequest.of(page, size, getSort(sort));
    Page<User> users = userService.findAll(pageable);
    
    return ResponseEntity.ok()
        .header("X-Total-Count", String.valueOf(users.getTotalElements()))
        .header("X-Total-Pages", String.valueOf(users.getTotalPages()))
        .body(users);
}

private Sort getSort(String[] sort) {
    if (sort.length >= 2) {
        return Sort.by(new Sort.Order(
            Sort.Direction.fromString(sort[1]),
            sort[0]
        ));
    }
    return Sort.unsorted();
}

Pagination Response Headers:

HTTP/1.1 200 OK
Content-Type: application/json
X-Total-Count: 150
X-Total-Pages: 8
X-Current-Page: 0
Link: </api/users?page=1&size=20>; rel="next", </api/users?page=7&size=20>; rel="last"

API Documentation with OpenAPI

SpringDoc OpenAPI Configuration:

@Configuration
public class OpenApiConfig {
    
    @Bean
    public OpenAPI customOpenAPI() {
        return new OpenAPI()
            .info(new Info()
                .title("User API")
                .version("1.0.0")
                .description("API for user management")
                .contact(new Contact()
                    .name("API Support")
                    .email("support@example.com")))
            .addSecurityItem(new SecurityRequirement().addList("bearerAuth"))
            .components(new Components()
                .addSecuritySchemes("bearerAuth", new SecurityScheme()
                    .type(SecurityScheme.Type.HTTP)
                    .scheme("bearer")
                    .bearerFormat("JWT")));
    }
}

Controller Documentation:

@RestController
@RequestMapping("/api/users")
@Tag(name = "Users", description = "User management API")
public class UserController {
    
    @Operation(summary = "Get user by ID")
    @ApiResponses(value = {
        @ApiResponse(responseCode = "200", description = "User found"),
        @ApiResponse(responseCode = "404", description = "User not found")
    })
    @GetMapping("/{id}")
    public User getUser(@Parameter(description = "User ID") @PathVariable Long id) {
        return userService.findById(id);
    }
    
    @Operation(summary = "Create new user")
    @PostMapping
    public User createUser(@RequestBody User user) {
        return userService.create(user);
    }
}

 

Comments (0)

No comments yet. Be the first to comment!

LecturreAI Assistant

Hello! I'm LecturreAI

What would you like to learn today?