Java Web Routing
Building APIs with Spring Boot
Explanation
Spring Boot for Web APIs
Spring Boot simplifies Java web development with convention over configuration. It's mature, well-documented, and powers countless production systems.
Key Concepts
- Controllers: Handle HTTP requests
- Services: Business logic layer
- Repositories: Data access layer
- Dependency Injection: Automatic wiring
Spring Annotations
| Annotation | Purpose |
|------------|---------|
| @RestController | REST API controller |
| @GetMapping | Handle GET requests |
| @PostMapping | Handle POST requests |
| @RequestBody | Parse JSON body |
| @PathVariable | URL path parameter |
Demonstration
Example 1: Basic Spring Boot API
// User.java
package com.example.demo.model;
public record User(
Long id,
String name,
String email,
String role
) {}
// CreateUserRequest.java
package com.example.demo.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
public record CreateUserRequest(
@NotBlank @Size(min = 2, max = 50)
String name,
@NotBlank @Email
String email,
String role
) {}
// UserController.java
package com.example.demo.controller;
import com.example.demo.model.User;
import com.example.demo.dto.CreateUserRequest;
import com.example.demo.service.UserService;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
// GET /api/users
@GetMapping
public ResponseEntity<Map<String, Object>> listUsers(
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int perPage,
@RequestParam(required = false) String role
) {
List<User> users = userService.findAll(page, perPage, role);
long total = userService.count(role);
return ResponseEntity.ok(Map.of(
"data", users,
"page", page,
"perPage", perPage,
"total", total
));
}
// GET /api/users/{id}
@GetMapping("/{id}")
public ResponseEntity<Map<String, User>> getUser(@PathVariable Long id) {
return userService.findById(id)
.map(user -> ResponseEntity.ok(Map.of("data", user)))
.orElse(ResponseEntity.notFound().build());
}
// POST /api/users
@PostMapping
public ResponseEntity<Map<String, User>> createUser(
@Valid @RequestBody CreateUserRequest request
) {
User user = userService.create(request);
return ResponseEntity
.status(HttpStatus.CREATED)
.body(Map.of("data", user));
}
// PUT /api/users/{id}
@PutMapping("/{id}")
public ResponseEntity<Map<String, User>> updateUser(
@PathVariable Long id,
@Valid @RequestBody CreateUserRequest request
) {
return userService.update(id, request)
.map(user -> ResponseEntity.ok(Map.of("data", user)))
.orElse(ResponseEntity.notFound().build());
}
// DELETE /api/users/{id}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
if (userService.delete(id)) {
return ResponseEntity.noContent().build();
}
return ResponseEntity.notFound().build();
}
}
Example 2: Service Layer
// UserService.java
package com.example.demo.service;
import com.example.demo.model.User;
import com.example.demo.dto.CreateUserRequest;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
@Service
public class UserService {
private final Map<Long, User> users = new ConcurrentHashMap<>();
private final AtomicLong idCounter = new AtomicLong(0);
public List<User> findAll(int page, int perPage, String role) {
return users.values().stream()
.filter(u -> role == null || role.equals(u.role()))
.skip((long) (page - 1) * perPage)
.limit(perPage)
.toList();
}
public long count(String role) {
return users.values().stream()
.filter(u -> role == null || role.equals(u.role()))
.count();
}
public Optional<User> findById(Long id) {
return Optional.ofNullable(users.get(id));
}
public User create(CreateUserRequest request) {
Long id = idCounter.incrementAndGet();
User user = new User(
id,
request.name(),
request.email(),
request.role() != null ? request.role() : "user"
);
users.put(id, user);
return user;
}
public Optional<User> update(Long id, CreateUserRequest request) {
return findById(id).map(existing -> {
User updated = new User(
id,
request.name(),
request.email(),
request.role() != null ? request.role() : existing.role()
);
users.put(id, updated);
return updated;
});
}
public boolean delete(Long id) {
return users.remove(id) != null;
}
}
Example 3: Error Handling
// GlobalExceptionHandler.java
package com.example.demo.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.HashMap;
import java.util.Map;
@RestControllerAdvice
public class GlobalExceptionHandler {
// Handle validation errors
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, Object>> handleValidationErrors(
MethodArgumentNotValidException ex
) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getAllErrors().forEach(error -> {
String fieldName = ((FieldError) error).getField();
String message = error.getDefaultMessage();
errors.put(fieldName, message);
});
return ResponseEntity
.badRequest()
.body(Map.of(
"error", "Validation failed",
"details", errors
));
}
// Handle custom exceptions
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<Map<String, String>> handleNotFound(
ResourceNotFoundException ex
) {
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(Map.of("error", ex.getMessage()));
}
// Handle all other exceptions
@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String, String>> handleGenericException(
Exception ex
) {
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Map.of("error", "Internal server error"));
}
}
// ResourceNotFoundException.java
package com.example.demo.exception;
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String message) {
super(message);
}
}
Example 4: Interceptors and Filters
// LoggingInterceptor.java
package com.example.demo.interceptor;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
@Component
public class LoggingInterceptor implements HandlerInterceptor {
private static final Logger logger = LoggerFactory.getLogger(LoggingInterceptor.class);
@Override
public boolean preHandle(
HttpServletRequest request,
HttpServletResponse response,
Object handler
) {
request.setAttribute("startTime", System.currentTimeMillis());
return true;
}
@Override
public void afterCompletion(
HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex
) {
long startTime = (Long) request.getAttribute("startTime");
long duration = System.currentTimeMillis() - startTime;
logger.info("[{}] {} {} - {}ms",
response.getStatus(),
request.getMethod(),
request.getRequestURI(),
duration
);
}
}
// AuthFilter.java
package com.example.demo.filter;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Component
public class AuthFilter implements Filter {
@Override
public void doFilter(
ServletRequest request,
ServletResponse response,
FilterChain chain
) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResponse) response;
// Skip auth for public endpoints
if (req.getRequestURI().startsWith("/api/public")) {
chain.doFilter(request, response);
return;
}
String authHeader = req.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
res.getWriter().write("{\"error\": \"Unauthorized\"}");
return;
}
// Validate token...
chain.doFilter(request, response);
}
}
// WebConfig.java
package com.example.demo.config;
import com.example.demo.interceptor.LoggingInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
private final LoggingInterceptor loggingInterceptor;
public WebConfig(LoggingInterceptor loggingInterceptor) {
this.loggingInterceptor = loggingInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loggingInterceptor);
}
}
Key Takeaways:
- Spring Boot simplifies Java web development
- Use records for DTOs (Java 16+)
- Validation annotations ensure data integrity
- Global exception handlers centralize error logic
- Interceptors/filters handle cross-cutting concerns
Imitation
Challenge 1: Add Pagination Response
Task: Create a generic paginated response wrapper.
Solution
// PagedResponse.java
public record PagedResponse<T>(
List<T> data,
int page,
int perPage,
long total,
int totalPages
) {
public static <T> PagedResponse<T> of(
List<T> data,
int page,
int perPage,
long total
) {
int totalPages = (int) Math.ceil((double) total / perPage);
return new PagedResponse<>(data, page, perPage, total, totalPages);
}
}
// Usage in controller
@GetMapping
public ResponseEntity<PagedResponse<User>> listUsers(
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int perPage
) {
List<User> users = userService.findAll(page, perPage);
long total = userService.count();
return ResponseEntity.ok(
PagedResponse.of(users, page, perPage, total)
);
}
Practice
Exercise 1: Complete CRUD API
Difficulty: Intermediate
Build a complete API with:
- Multiple resources
- Relationships between entities
- Search/filter capabilities
Exercise 2: JWT Authentication
Difficulty: Advanced
Implement authentication:
- Login endpoint returning JWT
- Token validation filter
- Role-based access control
Summary
What you learned:
- Spring Boot REST controllers
- Service layer patterns
- Validation and error handling
- Interceptors and filters
- Response standardization
Next Steps:
- Read: Java OOP
- Practice: Build a complete API
- Explore: Spring Security
