C
Spring Boot/Web MVC/Lesson 06

REST API CRUD in Practice — From Start to Finish

1 hr·theory
This chapter
2/2

REST API CRUD in Practice — From Start to Finish

🎯 After Reading This Lesson

After reading this lesson, you will be confident doing all three of the following:

  • ✅ Implement all four REST API operations (GET/POST/PUT/DELETE) using @RestController + @RequestMapping
  • ✅ Understand and use the differences between @RequestBody, @PathVariable, and @RequestParam
  • ✅ Apply a global exception handling pattern with @RestControllerAdvice

Keep these learning goals as a checklist, and close the lesson once you can answer all of them.

The Four HTTP Methods of CRUD

The Core Contract of REST

OperationHTTP MethodURL ExampleResponse
ListGET/users200 + array
Get oneGET/users/1200 + object / 404
CreatePOST/users201 + created object
UpdatePUT/users/1200 + updated object
DeleteDELETE/users/1204 (No Content)

URLs are nouns (resources), actions are HTTP methods — that is the essence of REST.

1. User Domain + DTO

java
@Getter @Setter
@NoArgsConstructor @AllArgsConstructor
public class User {
    private Long id;
    private String email;
    private String name;
}

@Getter @Setter
public class UserCreateRequest {
    private String email;
    private String name;
}

Separating Entity and DTO is the standard approach. DTOs are exclusively for API input/output, while Entities are exclusively for database mapping.

2. Temporary Storage (In-Memory Instead of a Database)

java
@Repository
public class UserRepository {
    private final Map<Long, User> store = new ConcurrentHashMap<>();
    private final AtomicLong seq = new AtomicLong();

    public List<User> findAll()            { return new ArrayList<>(store.values()); }
    public Optional<User> findById(Long id) { return Optional.ofNullable(store.get(id)); }
    public User save(User u) {
        if (u.getId() == null) u.setId(seq.incrementAndGet());
        store.put(u.getId(), u);
        return u;
    }
    public void deleteById(Long id) { store.remove(id); }
}

3. Service Layer

java
@Service
@RequiredArgsConstructor
public class UserService {
    private final UserRepository repo;

    public List<User> list() { return repo.findAll(); }

    public User get(Long id) {
        return repo.findById(id)
            .orElseThrow(() -> new NotFoundException("User not found: " + id));
    }

    public User create(UserCreateRequest req) {
        return repo.save(new User(null, req.getEmail(), req.getName()));
    }

    public User update(Long id, UserCreateRequest req) {
        User u = get(id);
        u.setEmail(req.getEmail());
        u.setName(req.getName());
        return repo.save(u);
    }

    public void delete(Long id) { repo.deleteById(id); }
}

The Controller does not touch the Repository directly — the Service layer owns the business logic.

Controller — @RequestBody · @PathVariable · ResponseEntity

Roles of the Four Annotations

  • @RequestBody — Deserializes the JSON in the HTTP body into an object
  • @PathVariable — Extracts values like {id} from the URL path
  • @RequestParam — Extracts query string values like ?page=1
  • ResponseEntity — Gives you direct control over status code + headers + body

Full Controller Code

java
@RestController
@RequestMapping("/users")
@RequiredArgsConstructor
public class UserController {
    private final UserService service;

    // GET /users
    @GetMapping
    public List<User> list() {
        return service.list();
    }

    // GET /users/{id}
    @GetMapping("/{id}")
    public User get(@PathVariable Long id) {
        return service.get(id);
    }

    // POST /users
    @PostMapping
    public ResponseEntity<User> create(@RequestBody UserCreateRequest req) {
        User created = service.create(req);
        return ResponseEntity
            .status(HttpStatus.CREATED)            // 201
            .body(created);
    }

    // PUT /users/{id}
    @PutMapping("/{id}")
    public User update(@PathVariable Long id, @RequestBody UserCreateRequest req) {
        return service.update(id, req);
    }

    // DELETE /users/{id}
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> delete(@PathVariable Long id) {
        service.delete(id);
        return ResponseEntity.noContent().build();    // 204
    }
}

When the Response Body Is an Object — Jackson Serializes Automatically

When @RestController is present, all return values are automatically converted to JSON. You can return a Map, List, or DTO directly without any extra configuration.

When You Need to Be Explicit About the Status Code

For GET requests, just return the object. For POST, return 201. For DELETE, return 204. Use ResponseEntity when you need to make those codes explicit.

In practice, some teams standardize all responses with ResponseEntity, while others return plain objects for simple GET calls. Both are valid approaches.

Global Exception Handling — @ControllerAdvice

try-catch Everywhere Is a Nightmare

java
// ❌ Handling exceptions in every controller
@GetMapping("/{id}")
public ResponseEntity<?> get(@PathVariable Long id) {
    try {
        return ResponseEntity.ok(service.get(id));
    } catch (NotFoundException e) {
        return ResponseEntity.status(404).body(e.getMessage());
    }
}

try-catch in dozens of methods → boilerplate explosion.

@ControllerAdviceA Dedicated Exception-Handling Bean

java
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(NotFoundException.class)
    public ResponseEntity<ErrorResponse> handleNotFound(NotFoundException e) {
        return ResponseEntity
            .status(HttpStatus.NOT_FOUND)
            .body(new ErrorResponse("NOT_FOUND", e.getMessage()));
    }

    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<ErrorResponse> handleBadInput(IllegalArgumentException e) {
        return ResponseEntity
            .status(HttpStatus.BAD_REQUEST)
            .body(new ErrorResponse("BAD_REQUEST", e.getMessage()));
    }

    @ExceptionHandler(Exception.class)   // *last resort*
    public ResponseEntity<ErrorResponse> handleAll(Exception e) {
        log.error("Unhandled exception", e);
        return ResponseEntity
            .status(HttpStatus.INTERNAL_SERVER_ERROR)
            .body(new ErrorResponse("INTERNAL_ERROR", "Server error"));
    }
}

@Getter @AllArgsConstructor
class ErrorResponse {
    private String code;
    private String message;
}

All exceptions from every controller are handled in one place. Controller code can then focus purely on business logic.

Custom Exception Classes

java
public class NotFoundException extends RuntimeException {
    public NotFoundException(String msg) { super(msg); }
}

Extending RuntimeException means no throws declaration required. Creating domain-specific exceptions (UserNotFoundException, OrderNotFoundException) makes the intent of the code explicit.

curl Testing

bash
# 1. Create
curl -X POST http://localhost:8080/users \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]","name":"Alice"}'
# → 201 {"id":1,"email":"[email protected]","name":"Alice"}

# 2. Retrieve
curl http://localhost:8080/users/1
# → 200 {"id":1,...}

# 3. Retrieve a non-existent ID
curl -i http://localhost:8080/users/999
# → 404 {"code":"NOT_FOUND","message":"User not found: 999"}

# 4. Delete
curl -X DELETE http://localhost:8080/users/1 -i
# → 204 No Content

Postman users can save all four requests as a Collection for one-click testing.

🤖 Try Asking AI Like This

Once you understand the concepts in this lesson, you can give AI specific, precise instructions. Instead of a vague 'fix this,' you can make vocabulary-driven requests — and that is where token savings begin.

  • 'Add global exception handling based on @ControllerAdvice to this controller.'
  • 'Create a controller with four User CRUD API endpoints (GET/POST/PUT/DELETE).'
  • 'Wrap these responses in ResponseEntity and make sure 201 and 204 status codes are returned correctly.'

Why This Reduces Token Usage

Without understanding the concepts, even after receiving an AI response you end up asking 'What does that mean?' again. That follow-up question is what burns through tokens. Learn the concept once, and the conversation ends in a single exchange.

REST API CRUD in Practice — From Start to Finish - Spring Boot