Advanced Java

REST APIs

Module 4

Building Modern Web Services

What is REST?

REST (Representational State Transfer) is an architectural style for distributed systems.


Key Principles:

REST vs SOAP

Feature REST SOAP
Protocol HTTP HTTP, SMTP, TCP
Message Format JSON, XML, HTML XML only
Complexity Simple Complex
Performance Faster (lightweight) Slower (verbose)
Use Case Web, mobile, microservices Enterprise, legacy systems

HTTP Methods

Method Purpose Idempotent Safe
GET Retrieve resource Yes Yes
POST Create resource No No
PUT Update/Replace resource Yes No
PATCH Partial update No No
DELETE Delete resource Yes No

HTTP Status Codes

Success (2xx):

Client Errors (4xx):

Server Errors (5xx):

Creating a REST Controller

@RestController
@RequestMapping("/api/users")
public class UserController {
    
    @Autowired
    private UserService userService;
    
    @GetMapping
    public List<User> getAllUsers() {
        return userService.findAll();
    }
    
    @GetMapping("/{id}")
    public User getUserById(@PathVariable Long id) {
        return userService.findById(id);
    }
    
    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public User createUser(@RequestBody User user) {
        return userService.create(user);
    }
    
    @PutMapping("/{id}")
    public User updateUser(@PathVariable Long id, @RequestBody User user) {
        return userService.update(id, user);
    }
    
    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void deleteUser(@PathVariable Long id) {
        userService.delete(id);
    }
}

Request Mapping Annotations

// Path variables
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) { }

// Query parameters
@GetMapping("/users")
public List<User> getUsers(@RequestParam String name,
                           @RequestParam(defaultValue = "0") int page) { }

// Request body
@PostMapping("/users")
public User createUser(@RequestBody User user) { }

// Request headers
@GetMapping("/users")
public List<User> getUsers(@RequestHeader("Authorization") String token) { }

// Multiple path variables
@GetMapping("/users/{userId}/posts/{postId}")
public Post getPost(@PathVariable Long userId, @PathVariable Long postId) { }

Request Body Validation

public class UserDTO {
    
    @NotNull(message = "Email is required")
    @Email(message = "Email must be valid")
    private String email;
    
    @NotBlank(message = "Name is required")
    @Size(min = 2, max = 100, message = "Name must be between 2 and 100 characters")
    private String name;
    
    @Min(value = 18, message = "Age must be at least 18")
    @Max(value = 120, message = "Age must be less than 120")
    private Integer age;
    
    @Pattern(regexp = "^\\+?[1-9]\\d{1,14}$", message = "Invalid phone number")
    private String phone;
}

@PostMapping("/users")
public User createUser(@Valid @RequestBody UserDTO userDTO) {
    return userService.create(userDTO);
}

Exception Handling

@ControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(UserNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public ErrorResponse handleUserNotFound(UserNotFoundException ex) {
        return new ErrorResponse(
            HttpStatus.NOT_FOUND.value(),
            ex.getMessage(),
            LocalDateTime.now()
        );
    }
    
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ErrorResponse handleValidationException(
            MethodArgumentNotValidException ex) {
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getFieldErrors()
            .forEach(error -> errors.put(error.getField(), 
                                        error.getDefaultMessage()));
        return new ErrorResponse(HttpStatus.BAD_REQUEST.value(),
                                "Validation failed", errors);
    }
}

Response Entity

Fine-grained control over HTTP response:

@GetMapping("/{id}")
public ResponseEntity<User> getUserById(@PathVariable Long id) {
    return userService.findById(id)
        .map(user -> ResponseEntity.ok()
            .header("X-Custom-Header", "value")
            .body(user))
        .orElse(ResponseEntity.notFound().build());
}

@PostMapping
public ResponseEntity<User> createUser(@Valid @RequestBody UserDTO userDTO) {
    User created = userService.create(userDTO);
    URI location = ServletUriComponentsBuilder
        .fromCurrentRequest()
        .path("/{id}")
        .buildAndExpand(created.getId())
        .toUri();
    return ResponseEntity.created(location).body(created);
}

DTOs (Data Transfer Objects)

Separate internal models from API contracts:

// Entity - internal model
@Entity
public class User {
    private Long id;
    private String email;
    private String password; // Don't expose!
    private String fullName;
}

// DTO - external API
public class UserDTO {
    private Long id;
    private String email;
    private String fullName;
}

// Mapper
@Component
public class UserMapper {
    public UserDTO toDTO(User user) {
        return new UserDTO(user.getId(), user.getEmail(), user.getFullName());
    }
    
    public User toEntity(UserDTO dto) {
        User user = new User();
        user.setEmail(dto.getEmail());
        user.setFullName(dto.getFullName());
        return user;
    }
}

Pagination and Sorting

@GetMapping
public Page<UserDTO> getUsers(
        @RequestParam(defaultValue = "0") int page,
        @RequestParam(defaultValue = "20") int size,
        @RequestParam(defaultValue = "id,asc") String[] sort) {
    
    Sort sortOrder = Sort.by(
        sort[1].equalsIgnoreCase("desc") ? 
            Sort.Direction.DESC : Sort.Direction.ASC, 
        sort[0]
    );
    
    Pageable pageable = PageRequest.of(page, size, sortOrder);
    return userService.findAll(pageable)
        .map(userMapper::toDTO);
}

// Response:
{
  "content": [...],
  "pageable": {...},
  "totalPages": 10,
  "totalElements": 200,
  "size": 20,
  "number": 0
}

API Versioning Strategies

1. URL Versioning (Most Common):

@RequestMapping("/api/v1/users")
@RequestMapping("/api/v2/users")

2. Header Versioning:

@GetMapping(value = "/api/users", headers = "API-Version=1")

3. Media Type Versioning:

@GetMapping(value = "/api/users", produces = "application/vnd.company.v1+json")

4. Query Parameter:

@GetMapping(value = "/api/users", params = "version=1")

CORS Configuration

Enable Cross-Origin Resource Sharing:

@Configuration
public class CorsConfig {
    
    @Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/api/**")
                    .allowedOrigins("http://localhost:3000", 
                                    "https://example.com")
                    .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                    .allowedHeaders("*")
                    .allowCredentials(true)
                    .maxAge(3600);
            }
        };
    }
}

Content Negotiation

Support multiple response formats:

@GetMapping(value = "/users/{id}", 
           produces = {MediaType.APPLICATION_JSON_VALUE, 
                      MediaType.APPLICATION_XML_VALUE})
public User getUser(@PathVariable Long id) {
    return userService.findById(id);
}

// Client requests with:
// Accept: application/json  → Returns JSON
// Accept: application/xml   → Returns XML

<dependency>
    <groupId>com.fasterxml.jackson.dataformat</groupId>
    <artifactId>jackson-dataformat-xml</artifactId>
</dependency>

OpenAPI / Swagger Documentation

<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
    <version>2.3.0</version>
</dependency>

@RestController
@RequestMapping("/api/users")
@Tag(name = "User Management", description = "APIs for managing users")
public class UserController {
    
    @Operation(summary = "Get user by ID", 
               description = "Returns a single user")
    @ApiResponses(value = {
        @ApiResponse(responseCode = "200", description = "User found"),
        @ApiResponse(responseCode = "404", description = "User not found")
    })
    @GetMapping("/{id}")
    public User getUserById(@PathVariable Long id) {
        return userService.findById(id);
    }
}

Access UI at: http://localhost:8080/swagger-ui.html

API Security: Basic Auth

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/public/**").permitAll()
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .httpBasic(withDefaults())
            .csrf(csrf -> csrf.disable());
        
        return http.build();
    }
}

API Security: JWT

JSON Web Tokens for stateless authentication:

@PostMapping("/login")
public ResponseEntity<AuthResponse> login(@RequestBody LoginRequest request) {
    Authentication authentication = authenticationManager.authenticate(
        new UsernamePasswordAuthenticationToken(
            request.getEmail(), request.getPassword()
        )
    );
    
    String token = jwtTokenProvider.generateToken(authentication);
    
    return ResponseEntity.ok(new AuthResponse(token));
}

// Protected endpoint
@GetMapping("/profile")
public User getProfile(@AuthenticationPrincipal UserDetails userDetails) {
    return userService.findByEmail(userDetails.getUsername());
}

Rate Limiting

Protect your API from abuse:

@Configuration
public class RateLimitConfig {
    
    @Bean
    public RateLimiter rateLimiter() {
        return RateLimiter.create(100); // 100 requests per second
    }
}

@RestControllerAdvice
public class RateLimitInterceptor {
    
    @Autowired
    private RateLimiter rateLimiter;
    
    @PreHandle
    public boolean preHandle(HttpServletRequest request) {
        if (!rateLimiter.tryAcquire()) {
            throw new TooManyRequestsException("Rate limit exceeded");
        }
        return true;
    }
}

Testing REST APIs

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
class UserControllerTest {
    
    @Autowired
    private MockMvc mockMvc;
    
    @Autowired
    private ObjectMapper objectMapper;
    
    @Test
    void shouldCreateUser() throws Exception {
        UserDTO user = new UserDTO("john@example.com", "John Doe");
        
        mockMvc.perform(post("/api/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(user)))
            .andExpect(status().isCreated())
            .andExpect(jsonPath("$.email").value("john@example.com"))
            .andExpect(header().exists("Location"));
    }
    
    @Test
    void shouldReturnNotFoundForInvalidId() throws Exception {
        mockMvc.perform(get("/api/users/999"))
            .andExpect(status().isNotFound());
    }
}

RESTful API Best Practices

1. Use Nouns for Resources

2. Use HTTP Methods Correctly

3. Use Plural Nouns

4. Use Proper Status Codes

Exercise 1: Build a REST API

Task: Create a complete REST API for a blog application


Requirements:

  1. CRUD operations for posts
  2. Request validation
  3. Exception handling
  4. Pagination and sorting
  5. DTOs for request/response
  6. Unit tests for all endpoints

Exercise 2: API Documentation

Task: Add comprehensive API documentation


Requirements:

  1. Add SpringDoc OpenAPI dependency
  2. Annotate controllers with @Operation
  3. Add API descriptions and examples
  4. Configure Swagger UI
  5. Add authentication to Swagger
  6. Export OpenAPI specification

Exercise 3: Security Implementation

Task: Secure your API


Requirements:

  1. Implement JWT authentication
  2. Create login/register endpoints
  3. Protect endpoints with @PreAuthorize
  4. Implement role-based access control
  5. Add CORS configuration
  6. Test security with MockMvc

Summary

In this module, you learned:


Next Module: Modern Java Features

Resources

Slide Overview