पाठ 7 / 25

REST Controllers

Methods return data; Jackson writes JSON.

@RestController and DTO records

A @RestController class maps HTTP requests to methods with @GetMapping, @PostMapping and friends; return values are converted to JSON by Jackson. Use small records as DTOs (request and response shapes) rather than exposing JPA entities directly, so the API stays stable when the database model changes. ResponseEntity lets you set status codes and headers, for example 201 Created with a Location header.

Controllers, validation and errors

Spring MVC maps HTTP requests to controller methods, validates input and produces consistent error responses.

Four ideas: controllers, request mapping, validation, problem details.
Figure 3.1 — Controllers, mapping, validation and errors.

The product controller

This file is from a demo "shop" service generated by start.spring.io for Spring Boot 4.1.1 and Java 21 (Temurin 21.0.12), built with Maven 3.9.16; the project compiled and its tests passed. Request and response records, constructor injection, path variables, a query parameter and a 201 response with Location.

package com.example.shop;

import jakarta.validation.Valid;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
import java.net.URI;
import java.util.List;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/products")
public class ProductController {

    public record CreateProduct(@NotBlank String name,
                                @NotNull @DecimalMin("0.01") BigDecimal price) { }

    public record ProductView(Long id, String name, BigDecimal price, String currency) { }

    private final ProductService service;
    private final ShopProperties props;

    public ProductController(ProductService service, ShopProperties props) {
        this.service = service;
        this.props = props;
    }

    @GetMapping("/{id}")
    public ProductView get(@PathVariable long id) {
        return view(service.get(id));
    }

    @GetMapping
    public List<ProductView> cheaperThan(@RequestParam BigDecimal max) {
        return service.cheaperThan(max).stream().limit(props.maxPageSize()).map(this::view).toList();
    }

    @PostMapping
    public ResponseEntity<ProductView> create(@Valid @RequestBody CreateProduct body) {
        Product saved = service.create(body.name(), body.price());
        return ResponseEntity.created(URI.create("/api/products/" + saved.getId())).body(view(saved));
    }

    private ProductView view(Product p) {
        return new ProductView(p.getId(), p.getName(), p.getPrice(), props.currency());
    }
}

Calling the API, run

I packaged the demo with mvn package, started it with java -jar on port 8085 and called it with curl; the output is copied from that run. GET returns the product as JSON including the configured currency; POST returns 201 with the new resource's Location.

curl -s localhost:8085/api/products/1
curl -s -i -X POST -H "content-type: application/json" -d '{"name":"Stapler","price":250}' localhost:8085/api/products

Output:

{"id":1,"name":"Notebook","price":120.00,"currency":"INR"}
HTTP/1.1 201 
Location: /api/products/4
{"id":4,"name":"Stapler","price":250,"currency":"INR"}

त्वरित जाँच: Why return DTO records instead of JPA entities?

  • The API stays stable and does not leak database structure
  • Entities cannot be serialised
  • Records are faster to query
  • Spring forbids returning entities
Answer

The API stays stable and does not leak database structure — Separate API and persistence models.