Tutorial Series Part 2: Designing REST APIs and Controllers

Date: December 24, 2024


Welcome back to our Spring Boot 3 and Java 17 tutorial series! In Part 1, we set up our project and created a simple REST endpoint. Now, we’re going to take a deeper dive into designing robust REST APIs and controllers.


What You’ll Learn in This Part

  • Designing RESTful API endpoints with best practices
  • Using DTOs (Data Transfer Objects) to separate API models from persistence entities
  • Validating API requests using javax.validation annotations
  • Handling errors gracefully with @ControllerAdvice and custom exception handlers
  • Example of a CRUD controller for a simple User resource

Step 1: Define the User DTO

Instead of exposing our database entities directly (a bad practice), we create a UserDTO class that represents the API request and response model.

package com.example.demoapp.dto;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

public class UserDTO {

    private Long id;

    @NotBlank(message = "Name is mandatory")
    @Size(min = 2, max = 50)
    private String name;

    @Email(message = "Email should be valid")
    @NotBlank(message = "Email is mandatory")
    private String email;

    // getters and setters omitted for brevity
}

Here, we use Jakarta Bean Validation annotations (@NotBlank, @Email, @Size) to validate incoming data automatically.


Step 2: Create a Basic User Entity (for later persistence)

For now, just a simple entity to match:

package com.example.demoapp.model;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;

@Entity
public class User {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

    private String email;

    // getters and setters
}

Step 3: Implement the UserController

Let’s create a REST controller for basic user operations.

package com.example.demoapp.controller;

import com.example.demoapp.dto.UserDTO;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.*;

@RestController
@RequestMapping("/api/users")
public class UserController {

    // Simple in-memory user store for demo
    private Map<Long, UserDTO> userStore = new HashMap<>();
    private Long idCounter = 1L;

    // Create user
    @PostMapping
    public ResponseEntity<UserDTO> createUser(@Valid @RequestBody UserDTO userDto) {
        userDto.setId(idCounter++);
        userStore.put(userDto.getId(), userDto);
        return new ResponseEntity<>(userDto, HttpStatus.CREATED);
    }

    // Get user by id
    @GetMapping("/{id}")
    public ResponseEntity<UserDTO> getUser(@PathVariable Long id) {
        UserDTO user = userStore.get(id);
        if (user == null) {
            return new ResponseEntity<>(HttpStatus.NOT_FOUND);
        }
        return ResponseEntity.ok(user);
    }

    // Get all users
    @GetMapping
    public List<UserDTO> getAllUsers() {
        return new ArrayList<>(userStore.values());
    }

    // Update user
    @PutMapping("/{id}")
    public ResponseEntity<UserDTO> updateUser(@PathVariable Long id, @Valid @RequestBody UserDTO userDto) {
        if (!userStore.containsKey(id)) {
            return new ResponseEntity<>(HttpStatus.NOT_FOUND);
        }
        userDto.setId(id);
        userStore.put(id, userDto);
        return ResponseEntity.ok(userDto);
    }

    // Delete user
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
        if (!userStore.containsKey(id)) {
            return new ResponseEntity<>(HttpStatus.NOT_FOUND);
        }
        userStore.remove(id);
        return ResponseEntity.noContent().build();
    }
}

Step 4: Handle Validation Errors Gracefully

Add a global exception handler to catch validation errors and return meaningful JSON responses.

package com.example.demoapp.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.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

import java.util.HashMap;
import java.util.Map;

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, String>> handleValidationExceptions(MethodArgumentNotValidException ex) {
        Map<String, String> errors = new HashMap<>();

        ex.getBindingResult().getAllErrors().forEach(error -> {
            String fieldName = ((FieldError) error).getField();
            String errorMessage = error.getDefaultMessage();
            errors.put(fieldName, errorMessage);
        });

        return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
    }
}

Step 5: Testing Your API

Run your application and test the API endpoints:

  • Create user:
curl -X POST http://localhost:8080/api/users \
-H "Content-Type: application/json" \
-d '{"name":"John Doe","email":"john.doe@example.com"}'

Response:

{
  "id": 1,
  "name": "John Doe",
  "email": "john.doe@example.com"
}
  • Get all users:
curl http://localhost:8080/api/users

Response:

[
  {
    "id": 1,
    "name": "John Doe",
    "email": "john.doe@example.com"
  }
]
  • Validation error example:
curl -X POST http://localhost:8080/api/users \
-H "Content-Type: application/json" \
-d '{"name":"","email":"not-an-email"}'

Response:

{
  "name": "Name is mandatory",
  "email": "Email should be valid"
}

Summary

In this part, you’ve learned how to:

  • Design REST APIs using Spring MVC annotations
  • Use DTOs with validation annotations for input safety
  • Create CRUD endpoints
  • Implement global exception handling for validation errors

What’s Next?

In Part 3, we’ll integrate persistence with JPA and Hibernate, replacing the in-memory store with a real database backend.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *