RESTful Web Services with Spring Boot
Source: Spring Boot in Practice — Chapter 7 by Somnath Musib (Manning). Concepts rewritten for this course; companion samples: spring-boot-in-practice/repo ch07.
Expose Course Tracker as JSON APIs: controllers, status codes, exceptions, MockMvc tests, OpenAPI docs, versioning, and JWT resource-server security.
Editors do not compile or run Spring Boot. Check uses sourceChecks only. Snippets above exercises show the usual shape with inline comments; type the code yourself. Paste is disabled on Spring Boot lessons.
What this lesson covers
- Designing CRUD REST endpoints with
@RestController - Exception handling (
@ControllerAdvice, HTTP status codes) - Integration testing with MockMvc
- Documenting APIs with OpenAPI / springdoc
- Versioning strategies (URI, query param, header, media type)
- Securing APIs with OAuth2 resource server + JWT (overview)
1. Building a REST API
REST clients request a resource; the server returns a representation (usually JSON). Spring Boot + Jackson map JSON ↔ Java types.
Typical stack:
Client → @RestController → service interface → repository → DB
Prefer plural nouns in paths (/courses), not verbs (/getCourses). The HTTP method already is the verb:
| Method | Path | Purpose |
|---|---|---|
| GET | /courses/ | List all |
| GET | /courses/{id} | One course |
| GET | /courses/category/{name} | Filter by category |
| POST | /courses/ | Create |
| PUT | /courses/{id} | Update |
| DELETE | /courses/{id} | Delete one |
| DELETE | /courses/ | Delete all |
@RestController = @Controller + @ResponseBody (return value goes in the HTTP body, not a view).
@RestController
@RequestMapping("/courses/") // base path for this resource
public class CourseController {
private final CourseService courseService;
@GetMapping // GET /courses/
public Iterable<Course> getAllCourses() {
return courseService.getCourses();
}
@GetMapping("{id}") // GET /courses/1
public Optional<Course> getCourseById(@PathVariable("id") long courseId) {
return courseService.getCourseById(courseId);
}
@PostMapping // POST /courses/ — JSON body → Course
public Course createCourse(@RequestBody Course course) {
return courseService.createCourse(course);
}
@PutMapping("{id}")
public void updateCourse(@PathVariable("id") long courseId, @RequestBody Course course) {
courseService.updateCourse(courseId, course);
}
@DeleteMapping("{id}")
public void deleteCourseById(@PathVariable("id") long courseId) {
courseService.deleteCourseById(courseId);
}
}
Wire the controller to a service interface (not the impl class) so you can swap implementations later.
@RestController
@RequestMapping("/courses/")
public class CourseController {
// inject CourseService here
@GetMapping
public Iterable<Course> getAllCourses() {
return null; // replace
}
@GetMapping("{id}")
public Optional<Course> getCourseById(@PathVariable("id") long courseId) {
return Optional.empty(); // replace
}
@PostMapping
public Course createCourse(@RequestBody Course course) {
return null; // replace
}
}
Returning an entity often yields 200. Many APIs use @ResponseStatus(HttpStatus.CREATED) on POST and 204 NO_CONTENT on delete/update-with-no-body. Pick status codes deliberately — clients depend on them.
REST design quiz
1 / 2@RestController differs from @Controller mainly because…
Pick an answer to see if you are right.
2. Exception handling
Unhandled deletes on missing IDs often become 500 with stack traces — useless and leaky. Map domain errors to meaningful statuses:
- Missing resource → 404
- Malformed JSON / validation → 400
Throw a custom CourseNotFoundException, then handle it globally:
@ControllerAdvice // applies across controllers
public class CourseTrackerGlobalExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(CourseNotFoundException.class)
public ResponseEntity<?> handleCourseNotFound(
CourseNotFoundException ex, WebRequest request) {
return handleExceptionInternal(
ex, ex.getMessage(), new HttpHeaders(), HttpStatus.NOT_FOUND, request); // 404
}
}
In the service, fail closed:
courseRepository.findById(courseId)
.orElseThrow(() -> new CourseNotFoundException("No course with id " + courseId));
@ControllerAdvice
public class CourseTrackerGlobalExceptionHandler extends ResponseEntityExceptionHandler {
// handle CourseNotFoundException → 404
}
3. Testing with MockMvc
CLI tools (HTTPie, cURL) and GUIs (Postman) are fine for exploration. For regressions, use MockMvc: Spring MVC request handling with mock request/response — no real HTTP server required for many tests.
@SpringBootTest
@AutoConfigureMockMvc
class CourseTrackerApiApplicationTests {
@Autowired MockMvc mockMvc;
@Test
void testPostCourse() throws Exception {
String json = """
{"name":"Rapid Spring Boot","category":"Spring","rating":5,"description":"..."}
""";
mockMvc.perform(post("/courses/")
.contentType("application/json")
.content(json))
.andExpect(status().isCreated()) // or isOk() depending on your API
.andExpect(jsonPath("$.name").value("Rapid Spring Boot"));
}
}
Key pieces: @SpringBootTest, @AutoConfigureMockMvc, fluent perform / andExpect, often jsonPath for JSON asserts.
@SpringBootTest
@AutoConfigureMockMvc
class CourseApiTests {
@Autowired
private MockMvc mockMvc;
@Test
void missingCourseIsNotFound() throws Exception {
// GET /courses/100 → expect 404
}
}
WebTestClient (reactive / WebFlux-friendly) is another option; MockMvc is the classic servlet-stack choice.
4. Documenting with OpenAPI
API docs are a contract: paths, bodies, status codes, security. OpenAPI is the spec; Swagger UI is a common viewer. In Spring Boot, springdoc-openapi introspects your app and serves UI (historically /swagger-ui.html).
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>1.5.9</version> <!-- book-era artifact; Boot 3 often uses springdoc-openapi-starter-webmvc-ui -->
</dependency>
Annotate controllers:
@Tag(name = "Course Controller", description = "Manage courses")
@RestController
@RequestMapping("/courses/")
public class CourseController {
@GetMapping
@ResponseStatus(HttpStatus.OK)
@Operation(summary = "List all courses")
public Iterable<Course> getAllCourses() { ... }
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
@Operation(summary = "Create a course")
public Course createCourse(@Valid @RequestBody Course course) { ... }
}
Optional metadata bean:
@Bean
OpenAPI customOpenAPI(
@Value("${app.description}") String description,
@Value("${app.version}") String version) {
return new OpenAPI().info(new Info()
.title("Course Tracker API")
.version(version)
.description(description));
}
Export machine-readable docs from /v3/api-docs (JSON) for Swagger Editor / Codegen.
@RestController
@RequestMapping("/courses/")
@Tag(name = "Course Controller", description = "Manage courses in Course Tracker")
public class CourseController {
@GetMapping
@ResponseStatus(code = HttpStatus.OK)
@Operation(summary = "Provides all courses available")
public Iterable<Course> getAllCourses() {
return courseService.getCourses();
}
}
5. API versioning
Breaking changes (e.g. adding required price) should not silently break existing clients. Common strategies:
| Strategy | Example |
|---|---|
| URI | /courses/v1, /courses/v2 |
| Request param | /courses/?version=v2 |
| Custom header | X-API-VERSION: v2 |
| Media type (Accept) | Accept: application/vnd.sbip.app-v2+json |
URI versioning (clearest in demos):
@RestController
@RequestMapping("/courses/v1")
public class LegacyCourseController { ... }
@RestController
@RequestMapping("/courses/v2")
public class ModernCourseController { ... }
Request parameter:
@GetMapping(params = "version=v1")
public Iterable<Course> getAllLegacyCourses() { ... }
@GetMapping(params = "version=v2")
public Iterable<ModernCourse> getAllModernCourses() { ... }
Custom header:
@GetMapping(headers = "X-API-VERSION=v2")
public Iterable<ModernCourse> getAllModernCourses() { ... }
Accept / content negotiation:
@GetMapping(produces = "application/vnd.sbip.app-v2+json")
public Iterable<ModernCourse> getAllModernCourses() { ... }
No single industry winner (Amazon, GitHub, Microsoft, Twitter pick different styles). Choose deliberately and document it.
@RestController
@RequestMapping("/courses/")
public class VersionedCourseController {
@GetMapping(/* v1 */)
public Iterable<Course> getAllLegacyCourses() {
return List.of();
}
@GetMapping(/* v2 */)
public Iterable<ModernCourse> getAllModernCourses() {
return List.of();
}
}
Versioning quiz
1 / 2@GetMapping(headers = "X-API-VERSION=v2") selects handlers by…
Pick an answer to see if you are right.
6. Securing REST APIs
HTTP Basic is simple (username/password, often Base64 over the wire) — fine for local demos, weak for public APIs without HTTPS and without central identity. Prefer OAuth2 / OIDC with a token from an authorization server (e.g. Keycloak).
Typical Bearer JWT flow:
- Client calls API without token → 401 +
WWW-Authenticate: Bearer - Client obtains
access_tokenfrom the auth server - Client retries with
Authorization: Bearer <jwt> - Resource server validates JWT and authorizes
Boot dependencies (resource server):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:9999/auth/realms/master
Use @AuthenticationPrincipal Jwt jwt to read claims (e.g. username). Fix IDOR (accessing another user’s resource by ID) with method security:
@EnableMethodSecurity // Boot 3; older books: @EnableGlobalMethodSecurity(prePostEnabled = true)
@GetMapping("{id}")
@PreAuthorize("hasAuthority('SCOPE_course:read')")
@PostAuthorize("@getAuthor.apply(returnObject, principal.claims['user_name'])")
public Optional<Course> getCourseById(@PathVariable("id") long courseId) {
return courseRepository.findById(courseId);
}
Scopes like course:read / course:write limit what a token may do even after authentication.
# JWT issuer — Keycloak realm example from the book era
spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:9999/auth/realms/master
@GetMapping("{id}")
public Optional<Course> getCourseById(@PathVariable("id") long courseId) {
return courseRepository.findById(courseId);
}
Chapter checklist
| Topic | Idea |
|---|---|
| REST controller | @RestController, nouns, GET/POST/PUT/DELETE |
| Layering | Controller → service interface → repository |
| Errors | Custom exceptions + @ControllerAdvice → correct status |
| Tests | @AutoConfigureMockMvc + MockMvc + jsonPath |
| Docs | springdoc + @Tag / @Operation / OpenAPI bean |
| Versioning | URI, param, header, or Accept — pick and stick |
| Security | Prefer JWT resource server over Basic for public APIs; scopes + @PreAuthorize / @PostAuthorize |
Full samples: ch07 on GitHub.
Next in the book: reactive apps with WebFlux → Lesson 6.
Chapter wrap-up
1 / 3Deleting a missing course should usually return…
Pick an answer to see if you are right.