WL
Java Full Stack Developer
Wassim Lagnaoui

Lesson 12: Spring Boot Web Development Basics

Master Spring Boot web development: build robust REST APIs with proper request handling, validation, error management, and professional web service patterns.

Introduction

Modern applications live on the web, serving millions of users through REST APIs that handle everything from simple data retrieval to complex business transactions. Spring Boot makes web development surprisingly straightforward, providing powerful annotations and conventions that transform plain Java classes into sophisticated web services. You can create endpoints that handle different HTTP methods, validate incoming data, manage errors gracefully, and return responses in various formats - all with minimal configuration. Whether you're building a mobile app backend, microservices for enterprise systems, or APIs for single-page applications, Spring Boot's web capabilities give you the tools to create professional, scalable web services. This lesson teaches you to build robust REST controllers that handle real-world scenarios including data validation, error handling, and content negotiation, setting the foundation for production-ready web applications.


REST Controller Basics

Definition

A REST controller in Spring Boot is a class that handles HTTP requests and returns responses, typically in JSON format. The @RestController annotation combines @Controller and @ResponseBody, automatically converting method return values to JSON. Controllers define endpoints (URLs) that clients can call to interact with your application's data and functionality. Each method in a controller can handle different HTTP operations and URL patterns.

Analogy

Think of a REST controller like a restaurant's ordering system. When customers (clients) want to interact with the restaurant, they don't go directly to the kitchen or storage room. Instead, they communicate through waiters (controller methods) who understand different types of requests: "I'd like to see the menu" (GET request), "I want to place an order" (POST request), "Can you change my order?" (PUT request), or "Cancel my dessert" (DELETE request). Each waiter specializes in handling specific types of requests and knows exactly how to communicate with the kitchen (business logic) and return the appropriate response to the customer. The waiter translates between the customer's natural language requests and the kitchen's operational procedures, just like a REST controller translates between HTTP requests and your application's business logic.

Examples

Basic REST controller structure:

@RestController
@RequestMapping("/api/users")
public class UserController {
    // Controller methods will be defined here
}

Simple GET endpoint:

@GetMapping("/hello")
public String sayHello() {
    return "Hello from Spring Boot!";
}

Returning JSON objects:

@GetMapping("/profile")
public User getUserProfile() {
    return new User("Alice", "alice@example.com", 25);
}

Multiple endpoints in one controller:

@RestController
@RequestMapping("/api/books")
public class BookController {
    @GetMapping
    public List getAllBooks() { return bookService.findAll(); }

    @GetMapping("/{id}")
    public Book getBook(@PathVariable Long id) { return bookService.findById(id); }
}

HTTP Methods Mapping

Definition

Different HTTP methods represent different types of operations: GET for retrieving data, POST for creating new resources, PUT for updating existing resources, DELETE for removing resources, and PATCH for partial updates. Spring Boot provides specific annotations (@GetMapping, @PostMapping, @PutMapping, @DeleteMapping) that map these HTTP methods to controller methods. This creates a clear, standardized way for clients to interact with your API.

Analogy

HTTP methods are like different types of interactions you have with a library system. When you want to browse available books, you use a catalog search (GET) - you're just looking, not changing anything. When you want to add a new book to your personal reading list, you submit a form (POST) to create a new entry. If you need to update your entire profile information, you fill out a complete update form (PUT) that replaces all your details. When you want to remove a book from your list, you use the delete function (DELETE). And if you just want to change your phone number without updating everything else, you use a quick edit feature (PATCH) for partial updates. Each interaction type serves a specific purpose and follows established conventions that everyone understands.

Examples

GET - Retrieve data:

@GetMapping("/products")
public List getAllProducts() {
    return productService.findAll();
}

POST - Create new resource:

@PostMapping("/products")
public Product createProduct(@RequestBody Product product) {
    return productService.save(product);
}

PUT - Update existing resource:

@PutMapping("/products/{id}")
public Product updateProduct(@PathVariable Long id, @RequestBody Product product) {
    return productService.update(id, product);
}

DELETE - Remove resource:

@DeleteMapping("/products/{id}")
public ResponseEntity deleteProduct(@PathVariable Long id) {
    productService.delete(id);
    return ResponseEntity.noContent().build();
}

Request Data Handling

Definition

Spring Boot provides various annotations to extract data from different parts of HTTP requests: @PathVariable for URL segments, @RequestParam for query parameters, @RequestBody for JSON payloads, and @RequestHeader for HTTP headers. These annotations automatically convert incoming data to Java objects and handle type conversion. Understanding how to capture and process request data is essential for building flexible APIs that can handle different client needs.

Analogy

Handling request data is like processing different types of mail delivery at a busy office. Sometimes the important information is written directly on the envelope (path variables) - like "Deliver to Building 3, Floor 2, Room 15" where each part tells you where to go. Other times, you get additional instructions on sticky notes (query parameters) like "urgent" or "fragile." The main content might be inside the package (request body) containing documents, forms, or products. And sometimes special handling instructions are written in the delivery service's tracking information (headers) indicating priority level or delivery method. Your job as the mail processor (controller method) is to examine all these different sources of information and route everything to the right destination with the appropriate handling.

Examples

Path variables - data from URL:

@GetMapping("/users/{userId}/orders/{orderId}")
public Order getOrder(@PathVariable Long userId, @PathVariable Long orderId) {
    return orderService.findByUserAndId(userId, orderId);
}

Query parameters - URL parameters:

@GetMapping("/products")
public List searchProducts(@RequestParam String category,
                                   @RequestParam(defaultValue = "0") int page) {
    return productService.findByCategory(category, page);
}

Request body - JSON data:

@PostMapping("/orders")
public Order createOrder(@RequestBody CreateOrderRequest request) {
    return orderService.create(request);
}

Request headers - HTTP header data:

@GetMapping("/profile")
public User getProfile(@RequestHeader("Authorization") String token) {
    User user = authService.getUserFromToken(token);
    return userService.getProfile(user.getId());
}

Response Handling

Definition

Spring Boot automatically converts method return values to appropriate response formats (usually JSON) and sets HTTP status codes. You can customize responses using ResponseEntity to control status codes, headers, and body content. Different return types serve different purposes: simple objects for success cases, ResponseEntity for complete control, and specific status codes for different scenarios like created resources, not found errors, or no content responses.

Analogy

Response handling is like a customer service representative responding to different types of inquiries. For simple questions, they provide straightforward answers (returning objects directly). For complex situations, they craft carefully worded responses that include not just the answer, but also the appropriate tone, urgency level, and follow-up instructions (ResponseEntity with custom headers and status codes). When someone asks for something that doesn't exist, they politely explain the situation (404 Not Found). When a request is successfully processed, they confirm completion with enthusiasm (201 Created). And when everything goes as expected, they provide exactly what was requested with professional efficiency (200 OK). The representative knows how to match their response style to the situation, just like Spring Boot controllers match response format to the request outcome.

Examples

Simple object return (200 OK by default):

@GetMapping("/user/{id}")
public User getUser(@PathVariable Long id) {
    return userService.findById(id);  // Returns 200 OK with JSON
}

Custom status codes with ResponseEntity:

@PostMapping("/users")
public ResponseEntity createUser(@RequestBody User user) {
    User savedUser = userService.save(user);
    return ResponseEntity.status(HttpStatus.CREATED).body(savedUser);
}

No content response:

@DeleteMapping("/users/{id}")
public ResponseEntity deleteUser(@PathVariable Long id) {
    userService.delete(id);
    return ResponseEntity.noContent().build();  // Returns 204 No Content
}

Custom headers in response:

@PostMapping("/upload")
public ResponseEntity uploadFile(@RequestParam MultipartFile file) {
    String fileId = fileService.save(file);
    return ResponseEntity.ok()
        .header("X-File-ID", fileId)
        .body("File uploaded successfully");
}

Data Validation

Definition

Data validation ensures that incoming request data meets your application's requirements before processing. Spring Boot integrates with Bean Validation (JSR-303) using annotations like @Valid, @NotNull, @NotBlank, @Email, and @Size to automatically validate request bodies and parameters. When validation fails, Spring Boot automatically returns detailed error responses with specific field-level error messages, helping clients understand exactly what needs to be corrected.

Analogy

Data validation is like security checkpoints at an airport or event venue. Before allowing people to enter, security personnel check that everyone has proper identification (required fields), that tickets are valid and not expired (format validation), that carry-on items meet size restrictions (length limits), and that prohibited items aren't present (business rule validation). If someone doesn't meet the requirements, security politely but firmly explains exactly what's wrong and what needs to be corrected before they can proceed. They don't just say "access denied" - they provide specific, actionable feedback like "your ID is expired" or "liquids must be in containers smaller than 100ml." This systematic checking prevents problems later and ensures everyone entering meets the established safety and security standards.

Examples

Basic validation annotations on model:

public class CreateUserRequest {
    @NotBlank(message = "Name is required")
    private String name;

    @Email(message = "Invalid email format")
    @NotBlank(message = "Email is required")
    private String email;
}

Validating request body:

@PostMapping("/users")
public ResponseEntity createUser(@Valid @RequestBody CreateUserRequest request) {
    User user = userService.create(request);
    return ResponseEntity.status(HttpStatus.CREATED).body(user);
}

Validating path variables and parameters:

@GetMapping("/users/{id}")
public User getUser(@PathVariable @Min(1) Long id) {
    return userService.findById(id);
}

@GetMapping("/users")
public List getUsers(@RequestParam @Size(min = 3) String search) {
    return userService.searchByName(search);
}

Custom validation messages:

public class Product {
    @NotNull(message = "Product name cannot be empty")
    @Size(min = 2, max = 100, message = "Product name must be 2-100 characters")
    private String name;

    @DecimalMin(value = "0.01", message = "Price must be at least 0.01")
    private BigDecimal price;
}

Error Handling

Definition

Proper error handling provides meaningful responses when things go wrong, using @ControllerAdvice to handle exceptions globally across all controllers. You can create custom exception classes for specific business errors and return appropriate HTTP status codes with helpful error messages. Spring Boot automatically handles validation errors, but you can customize error responses to provide consistent formatting and better user experience across your entire API.

Analogy

Error handling in web APIs is like having a well-trained customer service department that handles all problems consistently across different store locations. When issues occur - whether it's an out-of-stock item (resource not found), invalid payment information (validation error), or system maintenance (server error) - customers always receive professional, helpful responses that explain what happened and what they can do next. Instead of each store manager handling problems differently, there's a centralized customer service protocol that ensures everyone gets the same quality of support. The service representatives use clear, understandable language instead of technical jargon, provide specific error codes for tracking, and always suggest concrete next steps. This creates a reliable, professional experience regardless of what type of problem occurs or which part of the business the customer was interacting with.

Examples

Global exception handler:

@ControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(UserNotFoundException.class)
    public ResponseEntity handleUserNotFound(UserNotFoundException ex) {
        return ResponseEntity.notFound().build();
    }
}

Custom exception class:

public class UserNotFoundException extends RuntimeException {
    public UserNotFoundException(Long id) {
        super("User with ID " + id + " not found");
    }
}

Validation error handling:

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity> handleValidationErrors(
        MethodArgumentNotValidException ex) {
    Map errors = new HashMap<>();
    ex.getBindingResult().getFieldErrors().forEach(error ->
        errors.put(error.getField(), error.getDefaultMessage()));
    return ResponseEntity.badRequest().body(errors);
}

Standard error response format:

public class ErrorResponse {
    private String message;
    private int status;
    private long timestamp;

    public ErrorResponse(String message, int status) {
        this.message = message;
        this.status = status;
        this.timestamp = System.currentTimeMillis();
    }
}

Content Negotiation

Definition

Content negotiation allows your API to return data in different formats (JSON, XML, plain text) based on client requests, typically using the Accept header. Spring Boot automatically handles JSON conversion by default, but you can configure additional formats and use the produces attribute in mapping annotations to specify supported response formats. This flexibility allows the same endpoint to serve different types of clients with their preferred data formats.

Analogy

Content negotiation is like a multilingual tourist information center that can provide the same information in different languages and formats based on what visitors need. Some tourists prefer detailed written guides in English, others want simple maps in Spanish, and some need audio instructions in French. The information desk staff listens to what each visitor requests (Accept header) and provides exactly the same helpful information about local attractions, just packaged in the format that works best for that person. A business traveler might want a concise PDF they can quickly scan, while a family might prefer a colorful brochure with pictures. The core information never changes - only the presentation format adapts to serve each visitor's specific needs and preferences most effectively.

Examples

Specifying response format:

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

Multiple content types support:

@GetMapping(value = "/report",
           produces = {MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE})
public Report getReport() {
    return reportService.generateReport();
}

Accepting different input formats:

@PostMapping(value = "/users",
            consumes = MediaType.APPLICATION_JSON_VALUE,
            produces = MediaType.APPLICATION_JSON_VALUE)
public User createUser(@RequestBody User user) {
    return userService.save(user);
}

Custom response format based on request:

@GetMapping("/data")
public ResponseEntity getData(@RequestHeader("Accept") String acceptHeader) {
    if (acceptHeader.contains("xml")) {
        return ResponseEntity.ok().contentType(MediaType.APPLICATION_XML).body(dataAsXml);
    }
    return ResponseEntity.ok().body(dataAsJson);
}

CORS Configuration

Definition

Cross-Origin Resource Sharing (CORS) is a security mechanism that controls which domains can access your API from web browsers. By default, browsers block requests from different origins (different domain, port, or protocol) for security reasons. Spring Boot provides @CrossOrigin annotations and global CORS configuration to explicitly allow specific origins, methods, and headers, enabling your API to work with frontend applications hosted on different domains.

Analogy

CORS configuration is like setting up visitor access policies for a secure office building. By default, the building's security system (browser) only allows employees (same-origin requests) to access resources freely. However, you might need to allow business partners from other companies (different origins) to access specific meeting rooms or services. You create a visitor access list that specifies exactly which companies can visit, which floors they can access, what times they're allowed, and what type of identification they need to bring. This controlled access system maintains security while enabling necessary business collaboration. Without proper visitor policies, legitimate business partners would be turned away at the door, but with overly permissive policies, unauthorized people might gain access to sensitive areas.

Examples

Allow specific origin on controller:

@RestController
@CrossOrigin(origins = "http://localhost:3000")
public class ApiController {
    // All endpoints allow localhost:3000
}

Method-level CORS configuration:

@GetMapping("/data")
@CrossOrigin(origins = {"http://localhost:3000", "https://myapp.com"})
public List getData() {
    return dataService.findAll();
}

Global CORS configuration:

@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
                .allowedOrigins("http://localhost:3000")
                .allowedMethods("GET", "POST", "PUT", "DELETE");
    }
}

Permissive CORS for development:

@CrossOrigin(origins = "*", allowedHeaders = "*")
@RestController
public class DevController {
    // Only use "*" for development, never in production
}

Testing Web APIs

Definition

Spring Boot provides excellent testing support for web APIs using @WebMvcTest for controller-only tests and MockMvc for simulating HTTP requests. You can test different scenarios including successful operations, validation failures, error conditions, and edge cases without starting a full server. Testing ensures your API behaves correctly, handles errors gracefully, and maintains consistent behavior as your application evolves.

Analogy

Testing web APIs is like conducting fire drills and safety inspections in a building. You simulate various scenarios - normal operations (regular visitors), emergency situations (server errors), overcrowding (high load), and security breaches (invalid input) - to make sure all safety systems work correctly. During these drills, you don't need to create real emergencies; instead, you use simulation equipment that mimics real conditions. You test that fire alarms sound properly, emergency exits function, sprinkler systems activate, and staff respond appropriately. Similarly, API testing uses mock objects and simulated requests to verify that your controllers handle different situations correctly without needing real databases or external services. This systematic testing ensures your API will perform reliably when real users depend on it.

Examples

Basic controller test setup:

@WebMvcTest(UserController.class)
class UserControllerTest {
    @Autowired MockMvc mockMvc;
    @MockBean UserService userService;
}

Testing GET endpoint:

@Test
void shouldReturnUser() throws Exception {
    when(userService.findById(1L)).thenReturn(new User("Alice"));

    mockMvc.perform(get("/api/users/1"))
           .andExpect(status().isOk())
           .andExpect(jsonPath("$.name").value("Alice"));
}

Testing POST with validation:

@Test
void shouldCreateUser() throws Exception {
    mockMvc.perform(post("/api/users")
           .contentType(MediaType.APPLICATION_JSON)
           .content("{\"name\":\"Bob\",\"email\":\"bob@example.com\"}"))
           .andExpect(status().isCreated());
}

Testing error scenarios:

@Test
void shouldReturn404WhenUserNotFound() throws Exception {
    when(userService.findById(999L)).thenThrow(new UserNotFoundException(999L));

    mockMvc.perform(get("/api/users/999"))
           .andExpect(status().isNotFound());
}

Web API Best Practices

Definition

Professional web APIs follow consistent patterns: use appropriate HTTP methods and status codes, implement proper validation and error handling, return meaningful error messages, use consistent URL naming conventions, handle edge cases gracefully, and provide clear API documentation. These practices make your APIs easier to understand, integrate with, and maintain over time.

Analogy

Web API best practices are like establishing professional standards for a customer service department across a large company. Every interaction should follow the same high-quality protocols: use clear, consistent language that customers understand; provide helpful, specific responses rather than vague errors; follow the same procedures whether handling simple requests or complex problems; maintain professional courtesy even when declining requests; and ensure that any customer can get the same quality of service regardless of which representative they speak with. These standards create predictable, reliable experiences that build trust and make it easy for customers to understand how to interact with your business effectively. Just as good customer service practices lead to satisfied customers and successful business relationships, good API practices lead to satisfied developers and successful integrations.

Examples

Consistent URL naming:

// Good: RESTful resource naming
@RequestMapping("/api/users")     // Collection
@GetMapping("/{id}")              // Individual resource
@PostMapping                      // Create new resource
@PutMapping("/{id}")             // Update existing resource

Appropriate HTTP status codes:

@PostMapping
public ResponseEntity createUser(@RequestBody User user) {
    if (userService.emailExists(user.getEmail())) {
        return ResponseEntity.status(HttpStatus.CONFLICT).build(); // 409
    }
    User created = userService.save(user);
    return ResponseEntity.status(HttpStatus.CREATED).body(created); // 201
}

Meaningful error responses:

public class ApiError {
    private String message;
    private String errorCode;
    private List details;
    private LocalDateTime timestamp;

    // Provides context and actionable information
}

Input sanitization and validation:

@GetMapping("/search")
public List searchProducts(@RequestParam @Size(max=100) String query) {
    String sanitized = query.trim().toLowerCase();
    return productService.search(sanitized);
}

Summary

You've now mastered the fundamentals of Spring Boot web development, from creating REST controllers to handling complex validation and error scenarios. You understand how to map HTTP methods to controller methods, extract data from requests in various formats, customize responses with appropriate status codes, and validate incoming data to ensure quality. Error handling techniques and CORS configuration prepare your APIs for real-world deployment, while testing practices ensure reliability and maintainability. These skills enable you to build professional, robust web services that can handle production workloads and provide excellent developer experience for API consumers. Next, you'll learn how to integrate databases with Spring Data JPA, adding persistent data storage to your web applications and completing the foundation for full-stack development.

Programming Challenge

Challenge: Complete Blog Post Management API

Task: Build a comprehensive REST API for managing blog posts with proper validation, error handling, and testing.

Requirements:

  1. Create a BlogPost model with validation:
    • Title (required, 5-100 characters)
    • Content (required, minimum 50 characters)
    • Author email (required, valid email format)
    • Tags (optional, max 5 tags)
    • Creation and update timestamps
  2. Implement BlogController with endpoints:
    • GET /api/posts - Get all posts (with optional tag filter)
    • GET /api/posts/{id} - Get specific post
    • POST /api/posts - Create new post
    • PUT /api/posts/{id} - Update existing post
    • DELETE /api/posts/{id} - Delete post
    • GET /api/posts/search - Search posts by title or content
  3. Implement proper error handling:
    • Custom exception for post not found
    • Global exception handler with meaningful error responses
    • Validation error handling with field-specific messages
  4. Add CORS configuration for frontend integration
  5. Write comprehensive tests for all endpoints
  6. Use appropriate HTTP status codes and response formats

Bonus features:

  • Implement pagination for the posts list
  • Add sorting options (by date, title, author)
  • Create a simple statistics endpoint showing post counts
  • Add rate limiting headers to responses
  • Implement content negotiation for JSON/XML responses

Learning Goals: Practice building complete REST APIs with proper validation, error handling, testing, and professional API design patterns while integrating multiple Spring Boot web development concepts.