Spring REST APICheat Sheet
Quick Reference · Spring Boot · REST · OpenAPI · Validation · Pagination
RESTSpring MVCSwaggerValidationPaginationWebClient
REST Six Principles
Uniform Interface
Unique resource IDs · uniform representations · self-descriptive msgs · client drives via hyperlinks (HATEOAS)
Client–Server
UI separated from data storage/server
Stateless
Every request contains ALL info to complete it — no server session
Cacheable
Response must label itself cacheable or non-cacheable
Layered System
Hierarchical layers; component behavior constrained (e.g. MCP)
Code on Demand
Optional — server can send executable code (applets/scripts)
HATEOAS
Client only needs URI. API responses include hypermedia links for self-discovery of next actions & resources.
RMM Richardson Maturity Model
L0Swamp of POX
One URI, one HTTP method. Body tells server what to do. Essentially RPC (SOAP/XML). Not REST.
L1Resources
Multiple URIs per resource, but still one HTTP method (usually POST) for all actions.
L2HTTP Verbs
Correct HTTP verbs (GET/POST/PUT/DELETE) + proper status codes (201, 404, 400).
L3Hypermedia Controls
HATEOAS — responses include _links for discoverability & self-documentation.
// L3 HATEOAS response example
{
"id": 123, "status": "SHIPPED",
"_links": {
"self": { "href": "/orders/123" },
"track": { "href": "/orders/123/tracking" },
"cancel": { "href": "/orders/123/cancel" }
}
}
HTTP Methods
| Method | Use | Idempotent | Body |
|---|---|---|---|
| GET | Read resource | ✓ | ✗ |
| POST | Create resource | ✗ | ✓ |
| PUT | Replace resource | ✓ | ✓ |
| PATCH | Partial update | ~ | ✓ |
| DELETE | Remove resource | ✓ | ✗ |
| HEAD | GET without body (existence check) | ✓ | ✗ |
| OPTIONS | List supported methods | ✓ | ✗ |
⚠ Anti-pattern: Never GET /users/delete?id=10 — GET has no side effects. Browsers/crawlers auto-call GETs.
Status Codes
200 OK201 Created204 No Content400 Bad Request401 Unauthorized403 Forbidden404 Not Found409 Conflict500 Internal
ERRORS Exception Handling
// 1. Custom Exception
public class EmployeeNotFoundException
extends RuntimeException {
public EmployeeNotFoundException(Long id) {
super("Employee not found: " + id);
}
}
// 2. ApiError response DTO
public class ApiError {
LocalDateTime timestamp;
int status;
String message;
Map<String, String> errors;
}
// 3. Global Handler
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(EmployeeNotFoundException.class)
public ResponseEntity<ApiError> handleNotFound(
EmployeeNotFoundException ex) {
ApiError err = new ApiError(
LocalDateTime.now(), 404, ex.getMessage(), null);
return ResponseEntity.status(404).body(err);
}
@ExceptionHandler(
MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ApiError handleValidation(
MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(
e -> errors.put(e.getField(),
e.getDefaultMessage()));
return new ApiError(
LocalDateTime.now(), 400, "Validation failed",
errors);
}
}
VALID Bean Validation
Dependency: spring-boot-starter-validation
// Model — Jakarta annotations
public class Employee {
@NotBlank(message = "Name required")
private String name;
@Email(message = "Invalid email")
@NotBlank
private String email;
@Min(0) @Max(500000)
private Double salary;
@NotNull @Size(min=2, max=50)
private String department;
@Pattern(regexp = "^[A-Z]{2}\d{4}$")
private String code;
}
// Controller — trigger with @Valid
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Employee create(
@Valid @RequestBody Employee employee) {
return service.create(employee);
}
// Throws MethodArgumentNotValidException on fail
All Annotations
@NotNull@NotEmpty@NotBlank@Size@Min@Max@Email@Pattern@Positive@Negative@Future@DecimalMin@DecimalMax@AssertTrue@AssertFalse@Null
@Valid triggers on: class property · method param · method return type
PAGE Pagination & Sorting
// Controller
@GetMapping
public Page<Employee> getAll(Pageable pageable) {
return service.getAll(pageable);
}
// Auto-maps: ?page=0&size=20&sort=name,asc
// Repository
public interface EmployeeRepo extends
PagingAndSortingRepository<Employee, Long> {}
// Exposes: /employees{?page,size,sort}
// Manual pagination (non-repo data, e.g. SOAP)
int start = (int) pageable.getOffset();
int end = Math.min(start + pageable.getPageSize(),
list.size());
return new PageImpl<>(
list.subList(start, end), pageable, list.size());
// Page object contents
page.getContent() // the list
page.getTotalElements() // total records
page.getTotalPages() // total pages
page.getNumber() // current page
page.isFirst() / isLast()
page.hasNext() / hasPrevious()
HATEOAS Discoverability
Use events to decouple link-building from controller. Publish event → listener adds Link headers. Keeps controller clean.
Test: first page has next no prev · middle has both · last has no next
DOCS OpenAPI / Swagger (springdoc)
Dependencies
springdoc-openapi-starter-webmvc-ui
(webflux → -webflux variant)
Default URLs
/v3/api-docs JSON spec
/swagger-ui/index.html
# application.properties
springdoc.api-docs.path=/api-docs
springdoc.swagger-ui.path=/swagger-ui-custom.html
springdoc.swagger-ui.tagsSorter=alpha
springdoc.writer-with-order-by-keys=true
server.forward-headers-strategy=framework # behind proxy
// Config Bean
@Bean public OpenAPI customOpenAPI() {
return new OpenAPI().info(new Info()
.title("Employee API").version("1.0")
.description("..."));
}
// Controller annotations
@Tag(name = "Employee API", description = "...")
@Operation(summary = "Create a new employee")
@ApiResponses({
@ApiResponse(responseCode = "201",
description = "Created"),
@ApiResponse(responseCode = "400",
description = "Validation error",
content = @Content(mediaType="application/json",
schema = @Schema(impl = ApiError.class)))
})
@Parameter(description = "employee id") @PathVariable
// Global tag ordering (controls Swagger display order)
@OpenAPIDefinition(tags = {
@Tag(name = "create"), @Tag(name = "find"),
@Tag(name = "update"), @Tag(name = "delete")
})
// Bean validation → auto schema constraints
// @ResponseStatus on advice → auto response codes
// Pageable → auto page/size/sort params (v1.6.0+)
Swagger fails? Check:
① GET /v3/api-docs returns JSON ② dependency present ③ path not customized ④ security not blocking ④ CSRF allowed for docs path ⑤ context path prefix set correctly
DESIGN API Design Best Practices
URI Design
✓/usersvs✗ /getUser
✓/users/12/ordersvs✗ /getUserOrders?id=12
✓kebab-casevs✗ camelCase in URI
✓/users (plural)vs✗ /user & /users mixed
Best Practices
›URIs = nouns, not verbs
›Use correct HTTP verb always
›Richardson Maturity Model
›Centralize errors (@ControllerAdvice)
›Version from day 1 (/api/v1/users)
›Stateless — JWT/OAuth2 for auth
›Filtering + pagination on list endpoints
›No file extensions in URIs
›Accept header for content negotiation
›No state stored on server
Versioning Strategies
// URI-based (separate classes)
@RequestMapping("/api/v1/users") class UserV1Controller
@RequestMapping("/api/v2/users") class UserV2Controller
// Header-based (same controller)
@GetMapping(headers = "X-API-VERSION=1")
public Employee getV1() { ... }
@GetMapping(headers = "X-API-VERSION=2")
public EmployeeV2 getV2() { ... }
CONTENT Content Negotiation
// pom.xml — add XML support
<dependency>jackson-dataformat-xml</dependency>
// DTO — mark XML root element
@JacksonXmlRootElement(localName = "employees")
public class EmployeeResponse { ... }
// Controller — support both
@RequestMapping(value="/employees",
produces = {"application/json","application/xml"})
// produces → filters by client Accept header
// consumes → filters by client Content-Type header
// Spring picks best match automatically
// Both on a method:
@PostMapping(
consumes = "application/json",
produces = {"application/json","application/xml"})
INTERNALS Spring REST Request Flow
Client Request
TCP :8080
→
Acceptor Thread
Queues request
→
Worker Thread
Handles from queue
→
DispatcherServlet
extends HttpServlet · doService()
→
HandlerMapping
getHandler() loop
→
Controller Method
@RestController
// DispatcherServlet initializes handler mappings on startup
// initHandlerMappings() called during instantiation
// HandlerMapping interface → maps requests to handlers
// Acceptor: queues incoming connections (rejects when full)
// Worker: executes requests in dedicated thread stacks
// Core Spring REST annotations
@RestController // @Controller + @ResponseBody
@RequestMapping("/api/v1/employees")
@GetMapping @PostMapping @PutMapping
@DeleteMapping @PatchMapping
@PathVariable @RequestParam @RequestBody
@ResponseStatus(HttpStatus.CREATED)
WEBCLIENT Consuming REST APIs
// 1. Dependency: spring-boot-starter-webflux
// 2. Config bean
@Configuration
public class WebClientConfig {
@Bean
public WebClient webClient() {
return WebClient.builder().build();
}
}
// 3. DTO
public class WeatherResponse {
private Double temperature;
private Double windspeed;
// constructor, getters, setters
}
// 4. Service
@Service
public class WeatherService {
private final WebClient webClient;
public WeatherResponse getWeather(String city) {
Map response = webClient.get()
.uri("https://api.weather.com/v1?city=" + city)
.retrieve()
.bodyToMono(Map.class)
.block();
Map curr = (Map) response.get("current_weather");
return new WeatherResponse(
curr.get("temperature"),
curr.get("windspeed"));
}
}
SCHEMA JSON Schema & DTO
JSON Schema
›Define types: string, object, array
›Constraints: regex, required fields
›Use $ref + definitions for reuse
›Nesting for complex structures
›JSON Hyper-schema: adds HTTP endpoints (href), methods & relationships (rel)
›schema: validates request body
›targetSchema: defines response shape
›Meta-schemas: schema that validates your schema
DTO Pattern
Never expose entities directly — schema changes break clients.
DTO = Data Transfer Object = contract between API and client.
Schema-First Tools
prmd→documentation
heroics→client libraries
committee→validation & testing
MAVEN OpenAPI Generation Plugin (pom.xml)
<plugin>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-maven-plugin</artifactId>
<version>1.4</version>
<executions>
<execution>
<phase>integration-test</phase>
<goals><goal>generate</goal></goals>
</execution>
</executions>
<configuration>
<apiDocsUrl>
http://localhost:8080/v3/api-docs
</apiDocsUrl>
<outputFileName>openapi.json</outputFileName>
<outputDir>${project.build.directory}</outputDir>
</configuration>
</plugin>
Key Dependency Summary
| spring-boot-starter-web | REST controllers, MVC, Tomcat |
| spring-boot-starter-validation | Jakarta Bean Validation |
| spring-boot-starter-webflux | WebClient (consume APIs), reactive |
| springdoc-openapi-starter-webmvc-ui | Swagger UI + OpenAPI docs |
| jackson-dataformat-xml | XML content negotiation |
| spring-boot-starter-data-jpa | PagingAndSortingRepository |
REST Notes
REST ≠ HTTP
REST defines HOW resource methods should behave. HTTP provides a protocol. An API can be RESTful without HTTP.
PUT vs PATCH
PUT = idempotent full replacement. PATCH = partial update, conditionally idempotent (multiple calls may differ).
Pagination as Representation
Page is NOT a resource. Treat it as a representation — use query params (?page=0&size=20&sort=name,asc).
Cross-cutting Concerns
Pagination, HATEOAS links, and error handling are cross-cutting — use events & advice to keep controllers clean.
SPRING REST CHEAT SHEET · QUICK REFERENCE