Spring REST is built on Spring MVC: @RestController combines @Controller and @ResponseBody,
causing every handler method to serialize its return value directly into the HTTP response body — no view resolution involved.
spring-boot-starter-webmvc pulls in Jackson, which handles JSON serialization/deserialization automatically.
The REST server in this project is a standalone Spring Boot web app; the REST client is a separate Spring Boot app running with WebApplicationType.NONE,
using RestTemplate to drive HTTP calls against it.
@ResponseBody to every handler method: the return value is serialized directly into the HTTP response body (via HttpMessageConverter) instead of being resolved as a view name.@RestController is omitted, @ResponseBody can be declared at the method level (or class level on a plain @Controller) to achieve the same effect on individual handlers.ResponseEntity<T> — this gives explicit control over the HTTP status code and response headers in addition to the body — necessary when the response status must vary based on business logic (e.g., 200 vs. 404).@GetMapping — maps HTTP GET requests; used for reading/retrieving a resource.@PostMapping — maps HTTP POST requests; used for creating a resource.@PutMapping — maps HTTP PUT requests; used for updating a resource (full replacement).@PatchMapping — maps HTTP PATCH requests; used for updating a resource (partial update).@DeleteMapping — maps HTTP DELETE requests; used for deleting a resource.HttpStatus.CREATED (201) for @PostMapping, HttpStatus.NO_CONTENT (204) for @DeleteMapping, HttpStatus.NOT_FOUND (404) for PUT/PATCH/DELETE when the target resource does not exist.@ResponseStatus also returns a ResponseEntity, the ResponseEntity status takes precedence — @ResponseStatus is ignored.spring-boot-starter-webmvc. The client suppresses Tomcat startup via WebApplicationType.NONE — it still needs RestTemplate, which lives in the spring-web module transitively included by this starter.mtitek-spring-rest to reuse the AppProfile model class. The server JAR must be installed in the local Maven repository before building the client.<!-- REST server -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<!-- REST client: adds the above + the server artifact -->
<dependency>
<groupId>com.mtitek.spring</groupId>
<artifactId>mtitek-spring-rest</artifactId>
<version>${project.version}</version>
</dependency>
@NoArgsConstructor(access = AccessLevel.PRIVATE, force = true): Jackson requires a no-arg constructor for deserialization. Making it PRIVATE hides it from application code; Jackson uses reflection and can invoke private constructors.Role enum is serialized by Jackson as its name string by default ("USER", "ADMIN", "SUPPORT"). A mismatch on deserialization throws HttpMessageNotReadableException.@Data
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PRIVATE, force = true)
public class AppProfile {
private String id;
private String name;
private Role role;
public enum Role {
USER, ADMIN, SUPPORT
}
}
produces = "application/json" at the class level pins the response Content-Type for all methods. consumes = "application/json" on @PostMapping rejects requests without Content-Type: application/json with 415 Unsupported Media Type.postAppProfile, @ResponseStatus(HttpStatus.CREATED) is declared but the returned ResponseEntity uses HttpStatus.OK — the effective status is 200, not 201.ConcurrentHashMap.put — the response body contains the old state of the resource, not the updated state.AppProfile.Role.valueOf(role) in putAppProfileByIdRole throws an unhandled IllegalArgumentException for invalid role strings — results in a 500 response.@DeleteMapping returns 204 for all deletions, including when the key does not exist — ConcurrentHashMap.remove is a no-op for missing keys, so no 404 is ever returned from this endpoint.@RestController
@RequestMapping(path = "/api/appProfiles", produces = "application/json")
public class AppProfileController {
Map<String, AppProfile> appProfiles = new ConcurrentHashMap<>();
@GetMapping()
public Iterable<AppProfile> getAppProfiles() { ... }
@GetMapping("/{id}")
public ResponseEntity<AppProfile> appProfileById(@PathVariable("id") String id) { ... }
@PostMapping(consumes = "application/json")
@ResponseStatus(HttpStatus.CREATED)
public ResponseEntity<AppProfile> postAppProfile(@RequestBody AppProfile appProfile) { ... }
@PutMapping(path = "/{id}")
public ResponseEntity<AppProfile> putAppProfileById(@PathVariable("id") String id, @RequestBody AppProfile appProfileParam) { ... }
@PutMapping(path = "/{id}/{role}")
public ResponseEntity<AppProfile> putAppProfileByIdRole(@PathVariable("id") String id, @PathVariable("role") String role) { ... }
@PatchMapping(path = "/{id}")
public ResponseEntity<AppProfile> patchAppProfile(@PathVariable("id") String id, @RequestBody AppProfile patch) { ... }
@DeleteMapping(path = "/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void deleteAppProfile(@PathVariable("id") String id) { ... }
}
WebApplicationType.NONE prevents Spring Boot from starting an embedded Tomcat server in the client application while still running a full Spring context.RestTemplate is registered as a @Bean in the main application class — thread-safe and shared across all client services.restTemplate.exchange() with ParameterizedTypeReference<List<AppProfile>> is required to deserialize a generic collection — without it, type erasure causes Jackson to produce LinkedHashMap entries instead of AppProfile objects.getForObject returns the deserialized body only; getForEntity returns a ResponseEntity with access to status code and headers.restTemplate.put() and restTemplate.delete() both return void — use restTemplate.exchange() to access the response.HttpClientErrorException.NotFound must be caught before the parent HttpClientErrorException. RestTemplate throws HttpClientErrorException for 4xx, HttpServerErrorException for 5xx, and ResourceAccessException on connection failure.// GET all — ParameterizedTypeReference required for generic collection
List<AppProfile> appProfiles = restTemplate.exchange(
BASE_URL + BASE_PATH, HttpMethod.GET, null,
new ParameterizedTypeReference<List<AppProfile>>() {}
).getBody();
// GET by id — body only vs. full ResponseEntity
AppProfile appProfile = restTemplate.getForObject(BASE_URL + BASE_PATH + "/{id}", AppProfile.class, 1);
ResponseEntity<AppProfile> responseEntity = restTemplate.getForEntity(BASE_URL + BASE_PATH + "/{id}", AppProfile.class, 1);
// PUT and DELETE — void return
restTemplate.put(BASE_URL + BASE_PATH + "/{id}", appProfile, appProfile.getId());
restTemplate.delete(BASE_URL + BASE_PATH + "/{id}", 3);