A REST controller connects HTTP to the application core. It should not contain all domain rules. DTOs describe data at the transport boundary. Bean Validation checks basic input requirements. The learning project accepts a title and returns a normalised value. It has no durable storage and is not a copy of the protected PDF server API.
01Start the server project
Save both files in a new folder. mvn spring-boot:run starts on port 8282. Spring Boot 4.1 and Java 21 match the checked PDF codebase. The learning server remains local.
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>4.1.0</version></parent>
<groupId>example</groupId><artifactId>rest-learning</artifactId><version>1.0</version>
<properties><java.version>21</java.version></properties>
<dependencies>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId></dependency>
</dependencies>
<build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build>
</project>
Run / check
mvn spring-boot:run -Dspring-boot.run.arguments=--server.port=828202Read the DTO and controller
A record groups immutable input data. @RequestBody reads JSON. @Valid enables @NotBlank validation. @PostMapping names the route. ResponseEntity sets the HTTP status and response.
package tutorial;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@SpringBootApplication
@RestController
public class Application {
record Input(@NotBlank String title) {}
record Output(String title) {}
@PostMapping("/normalise")
ResponseEntity<Output> normalise(@Valid @RequestBody Input input) {
return ResponseEntity.ok(new Output(input.title().trim()));
}
public static void main(String[] args) { SpringApplication.run(Application.class, args); }
}
03Send a real request
Run the curl commands in a second terminal. The valid request should return 200. Whitespace-only input returns 400. The response normalises text but intentionally creates no persisted resource. We therefore do not use 201 Created here.
curl -i -H 'Content-Type: application/json' -d '{"title":" Letters "}' http://127.0.0.1:8282/normalise
curl -i -H 'Content-Type: application/json' -d '{"title":" "}' http://127.0.0.1:8282/normalise
04Add the domain boundary
In the real PDF project, JobRestController delegates to JobApplicationService. The service checks states, limits and storage workflows. Transport validation complements those rules rather than replacing them. Security and upload ownership are handled separately in the full server.
A valid title is normalised. A blank title produces HTTP 400.
Your exercise
Add a maximum length of 60 and test exactly 60 and 61 characters.
Show the solution and explanation
Also use @Size(max = 60) on title. Consider whether the domain length applies before or after trim. If it concerns normalised text, the service must check it after normalisation.
Project context and sources
The example is simplified for learning. The linked source version shows its use in the project and the checks applied there.