Spring REST: In-Depth Notes + Interview Questions



Table of Contents
Spring REST: In-Depth Notes + Interview Questions
1. What is REST?
REST (Representational State Transfer) is an architectural style for building web APIs over HTTP.
REST Constraints
- Client-Server — UI and backend are separated
- Stateless — every request is self-contained (no session state on server)
- Cacheable — responses can be cached
- Uniform Interface — standard HTTP methods, URIs, status codes
- Layered System — client doesn't know if it's talking to a proxy or real server
- Code on Demand (optional) — server can send executable code
HTTP Methods → CRUD Mapping
| HTTP Method | CRUD | Safe | Idempotent |
|---|---|---|---|
| GET | Read | ✅ | ✅ |
| POST | Create | ❌ | ❌ |
| PUT | Replace (full update) | ❌ | ✅ |
| PATCH | Partial update | ❌ | ❌ |
| DELETE | Delete | ❌ | ✅ |
Safe = does not modify data. Idempotent = calling multiple times gives same result.
2. Spring MVC Request Processing Flow
Client Request
↓
DispatcherServlet ← Front Controller (single entry point)
↓
HandlerMapping ← Finds the right controller method
↓
HandlerAdapter ← Invokes the controller method
↓
Controller Method ← Business logic / service call
↓
MessageConverter ← Serializes return value to JSON/XML
↓
HTTP Response
Key class: DispatcherServlet — registered automatically by Spring Boot.
3. Core Annotations
Controller Annotations
| Annotation | Description |
|---|---|
@RestController | @Controller + @ResponseBody — returns data, not views |
@Controller | MVC controller — returns view names |
@RequestMapping | Map URL + HTTP method to class or method |
@GetMapping | Shortcut for @RequestMapping(method = GET) |
@PostMapping | Shortcut for POST |
@PutMapping | Shortcut for PUT |
@PatchMapping | Shortcut for PATCH |
@DeleteMapping | Shortcut for DELETE |
Parameter Annotations
| Annotation | Source | Example |
|---|---|---|
@PathVariable | URL path segment | /users/{id} |
@RequestParam | Query string | /users?page=0&size=10 |
@RequestBody | Request body (JSON) | POST/PUT body |
@RequestHeader | HTTP header | Authorization header |
@CookieValue | Cookie | Session cookies |
@ModelAttribute | Form data | HTML form POST |
@Valid / @Validated | Trigger validation on body | With @RequestBody |
4. Building a Complete REST Controller
@RestController
@RequestMapping("/api/v1/users")
public class UserController {
private final UserService userService;
// Constructor injection (preferred)
public UserController(UserService userService) {
this.userService = userService;
}
// GET all with pagination
@GetMapping
public ResponseEntity<Page<UserDTO>> getAllUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(defaultValue = "id") String sortBy) {
Pageable pageable = PageRequest.of(page, size, Sort.by(sortBy));
return ResponseEntity.ok(userService.getAllUsers(pageable));
}
// GET by ID
@GetMapping("/{id}")
public ResponseEntity<UserDTO> getUserById(@PathVariable Long id) {
return ResponseEntity.ok(userService.getUserById(id));
}
// POST — create
@PostMapping
public ResponseEntity<UserDTO> createUser(@Valid @RequestBody CreateUserRequest request) {
UserDTO created = userService.createUser(request);
URI location = URI.create("/api/v1/users/" + created.getId());
return ResponseEntity.created(location).body(created);
}
// PUT — full update
@PutMapping("/{id}")
public ResponseEntity<UserDTO> updateUser(
@PathVariable Long id,
@Valid @RequestBody UpdateUserRequest request) {
return ResponseEntity.ok(userService.updateUser(id, request));
}
// PATCH — partial update
@PatchMapping("/{id}/status")
public ResponseEntity<Void> updateStatus(
@PathVariable Long id,
@RequestParam boolean active) {
userService.updateStatus(id, active);
return ResponseEntity.noContent().build();
}
// DELETE
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
return ResponseEntity.noContent().build();
}
}
5. ResponseEntity
ResponseEntity<T> gives full control over the HTTP response (status, headers, body).
// 200 OK with body
return ResponseEntity.ok(data);
// 201 Created with Location header
return ResponseEntity.created(URI.create("/api/users/1")).body(dto);
// 204 No Content
return ResponseEntity.noContent().build();
// 404 Not Found
return ResponseEntity.notFound().build();
// Custom status + headers
return ResponseEntity
.status(HttpStatus.ACCEPTED)
.header("X-Custom-Header", "value")
.body(data);
// Using builder pattern
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.contentType(MediaType.APPLICATION_JSON)
.body(errorResponse);
Common HTTP Status Codes
| Code | Meaning | When to use |
|---|---|---|
200 OK | Success | GET, PUT, PATCH responses |
201 Created | Resource created | POST success |
204 No Content | Success, no body | DELETE, PATCH with no return |
400 Bad Request | Invalid input | Validation failures |
401 Unauthorized | Not authenticated | Missing/invalid token |
403 Forbidden | Not authorized | Authenticated but lacks permission |
404 Not Found | Resource missing | Entity not found |
409 Conflict | State conflict | Duplicate email, version mismatch |
422 Unprocessable Entity | Semantic validation error | Business rule violations |
500 Internal Server Error | Unexpected error | Unhandled exceptions |
6. Request & Response DTOs
Never expose JPA entities directly in REST APIs. Use DTOs.
// Request DTO
public record CreateUserRequest(
@NotBlank(message = "Name is required")
String name,
@Email(message = "Invalid email format")
@NotBlank(message = "Email is required")
String email,
@Size(min = 8, message = "Password must be at least 8 characters")
String password
) { }
// Response DTO
public record UserDTO(
Long id,
String name,
String email,
boolean active,
LocalDateTime createdAt
) { }
DTO Mapping (Manual)
@Service
public class UserService {
public UserDTO toDTO(User user) {
return new UserDTO(
user.getId(),
user.getName(),
user.getEmail(),
user.isActive(),
user.getCreatedAt()
);
}
}
DTO Mapping (MapStruct — recommended)
@Mapper(componentModel = "spring")
public interface UserMapper {
UserDTO toDTO(User user);
User toEntity(CreateUserRequest request);
}
<!-- pom.xml -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.5.5.Final</version>
</dependency>
7. Bean Validation
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
Validation Annotations
| Annotation | Description |
|---|---|
@NotNull | Field must not be null |
@NotBlank | String must not be null or blank |
@NotEmpty | Collection/String must not be empty |
@Size(min, max) | String length or collection size |
@Min(value) | Minimum numeric value |
@Max(value) | Maximum numeric value |
@Email | Valid email format |
@Pattern(regexp) | Must match regex |
@Positive | Must be > 0 |
@PositiveOrZero | Must be >= 0 |
@Past | Date must be in the past |
@Future | Date must be in the future |
@Valid | Cascades validation to nested objects |
public record AddressRequest(
@NotBlank String street,
@NotBlank String city,
@Pattern(regexp = "^[0-9]{6}$", message = "Invalid PIN code") String pinCode
) { }
public record CreateUserRequest(
@NotBlank String name,
@Email String email,
@Valid AddressRequest address // cascades validation
) { }
Custom Validator
// 1. Define annotation
@Constraint(validatedBy = PhoneNumberValidator.class)
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidPhone {
String message() default "Invalid phone number";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
// 2. Implement validator
public class PhoneNumberValidator implements ConstraintValidator<ValidPhone, String> {
@Override
public boolean isValid(String phone, ConstraintValidatorContext ctx) {
return phone != null && phone.matches("^\\+?[0-9]{10,13}$");
}
}
// 3. Use it
public record CreateUserRequest(
@ValidPhone String phone
) { }
8. Global Exception Handling
@RestControllerAdvice
public class GlobalExceptionHandler {
// Validation errors
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidation(MethodArgumentNotValidException ex) {
Map<String, String> errors = new LinkedHashMap<>();
ex.getBindingResult().getFieldErrors()
.forEach(err -> errors.put(err.getField(), err.getDefaultMessage()));
return ResponseEntity.badRequest().body(
new ErrorResponse("VALIDATION_ERROR", "Invalid input", errors)
);
}
// Custom business exception
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(
new ErrorResponse("NOT_FOUND", ex.getMessage(), null)
);
}
// Constraint violations (path/query param validation)
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<ErrorResponse> handleConstraintViolation(ConstraintViolationException ex) {
return ResponseEntity.badRequest().body(
new ErrorResponse("CONSTRAINT_VIOLATION", ex.getMessage(), null)
);
}
// Fallback
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleAll(Exception ex) {
return ResponseEntity.internalServerError().body(
new ErrorResponse("INTERNAL_ERROR", "Unexpected error occurred", null)
);
}
}
// Error response structure
public record ErrorResponse(
String code,
String message,
Map<String, String> details
) { }
// Custom exception
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String message) {
super(message);
}
}
9. Content Negotiation
Spring can return different formats based on Accept header.
<!-- Add XML support -->
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
@GetMapping(
value = "/users/{id}",
produces = { MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE }
)
public ResponseEntity<UserDTO> getUser(@PathVariable Long id) {
return ResponseEntity.ok(userService.getUserById(id));
}
Client sends: Accept: application/xml → gets XML response.
10. Jackson Configuration
application.properties
# Date format spring.jackson.date-format=yyyy-MM-dd HH:mm:ss spring.jackson.time-zone=UTC # Serialization spring.jackson.serialization.write-dates-as-timestamps=false spring.jackson.serialization.fail-on-empty-beans=false # Deserialization spring.jackson.deserialization.fail-on-unknown-properties=false # Naming strategy spring.jackson.property-naming-strategy=SNAKE_CASE
Jackson Annotations
| Annotation | Purpose |
|---|---|
@JsonProperty("user_name") | Custom field name in JSON |
@JsonIgnore | Exclude field from serialization |
@JsonIgnoreProperties({"field1"}) | Ignore specific fields on class |
@JsonInclude(NON_NULL) | Exclude null fields from output |
@JsonFormat(pattern="dd-MM-yyyy") | Format dates |
@JsonAlias({"fname", "first_name"}) | Accept multiple names during deserialization |
@JsonSerialize(using=CustomSerializer.class) | Custom serializer |
public class UserDTO {
@JsonProperty("user_id")
private Long id;
@JsonIgnore
private String password;
@JsonInclude(JsonInclude.Include.NON_NULL)
private String middleName;
@JsonFormat(pattern = "dd-MM-yyyy HH:mm")
private LocalDateTime createdAt;
}
11. Versioning Strategies
1. URI Versioning (Most Common)
@GetMapping("/api/v1/users")
@GetMapping("/api/v2/users")
2. Request Parameter Versioning
@GetMapping(value = "/users", params = "version=1")
@GetMapping(value = "/users", params = "version=2")
3. Header Versioning
@GetMapping(value = "/users", headers = "X-API-Version=1")
@GetMapping(value = "/users", headers = "X-API-Version=2")
4. Media Type (Content Negotiation) Versioning
@GetMapping(value = "/users", produces = "application/vnd.company.v1+json")
@GetMapping(value = "/users", produces = "application/vnd.company.v2+json")
Recommendation: URI versioning (
/api/v1/) is the most visible, easy to test in a browser, and widely adopted.
12. HATEOAS (Hypermedia)
HATEOAS adds navigation links to API responses.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping("/{id}")
public EntityModel<UserDTO> getUser(@PathVariable Long id) {
UserDTO user = userService.getUserById(id);
return EntityModel.of(user,
linkTo(methodOn(UserController.class).getUser(id)).withSelfRel(),
linkTo(methodOn(UserController.class).getAllUsers(0, 10, "id")).withRel("all-users"),
linkTo(methodOn(UserController.class).deleteUser(id)).withRel("delete")
);
}
}
Response:
{
"id": 1,
"name": "John",
"_links": {
"self": { "href": "/api/users/1" },
"all-users": { "href": "/api/users?page=0&size=10&sortBy=id" },
"delete": { "href": "/api/users/1" }
}
}
13. Cross-Origin Resource Sharing (CORS)
Method-level
@CrossOrigin(origins = "http://localhost:3000")
@GetMapping("/users")
public List<UserDTO> getUsers() { ... }
Controller-level
@CrossOrigin(origins = "*", allowedHeaders = "*")
@RestController
public class UserController { }
Global Configuration (Recommended)
@Configuration
public class CorsConfig {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("http://localhost:3000", "https://myapp.com")
.allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
};
}
}
14. Interceptors & Filters
Filter (Servlet Level — runs before Spring)
@Component
public class RequestLoggingFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
System.out.println("Incoming: " + request.getMethod() + " " + request.getRequestURI());
long start = System.currentTimeMillis();
chain.doFilter(req, res);
System.out.println("Completed in " + (System.currentTimeMillis() - start) + "ms");
}
}
HandlerInterceptor (Spring MVC Level)
@Component
public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
String token = request.getHeader("Authorization");
if (token == null || !token.startsWith("Bearer ")) {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
return false; // Stop processing
}
return true; // Continue
}
@Override
public void afterCompletion(HttpServletRequest req, HttpServletResponse res,
Object handler, Exception ex) {
// Cleanup
}
}
// Register interceptor
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private AuthInterceptor authInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authInterceptor)
.addPathPatterns("/api/**")
.excludePathPatterns("/api/auth/**");
}
}
Filter vs Interceptor
| Filter | HandlerInterceptor | |
|---|---|---|
| Level | Servlet container | Spring MVC |
| Access to Spring beans | Limited | Full |
| Request scope | All requests | Only mapped to DispatcherServlet |
| Use for | Security, logging, encoding | Auth, logging, locale |
15. RestTemplate (Legacy — Synchronous)
Used to call external REST APIs.
@Service
public class ExternalService {
private final RestTemplate restTemplate;
public ExternalService(RestTemplateBuilder builder) {
this.restTemplate = builder
.setConnectTimeout(Duration.ofSeconds(5))
.setReadTimeout(Duration.ofSeconds(10))
.build();
}
// GET
public UserDTO getUser(Long id) {
return restTemplate.getForObject("https://api.example.com/users/{id}", UserDTO.class, id);
}
// GET with headers
public ResponseEntity<UserDTO> getUserWithHeaders(Long id) {
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer token123");
HttpEntity<Void> entity = new HttpEntity<>(headers);
return restTemplate.exchange(
"https://api.example.com/users/{id}",
HttpMethod.GET,
entity,
UserDTO.class,
id
);
}
// POST
public UserDTO createUser(CreateUserRequest request) {
return restTemplate.postForObject("https://api.example.com/users", request, UserDTO.class);
}
// DELETE
public void deleteUser(Long id) {
restTemplate.delete("https://api.example.com/users/{id}", id);
}
}
16. WebClient (Modern — Reactive / Non-Blocking)
Replacement for RestTemplate (works in both reactive and non-reactive apps).
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
@Service
public class ExternalService {
private final WebClient webClient;
public ExternalService(WebClient.Builder builder) {
this.webClient = builder
.baseUrl("https://api.example.com")
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.build();
}
// Blocking (synchronous usage)
public UserDTO getUser(Long id) {
return webClient.get()
.uri("/users/{id}", id)
.retrieve()
.bodyToMono(UserDTO.class)
.block(); // blocks — use in non-reactive code
}
// Non-blocking (reactive)
public Mono<UserDTO> getUserAsync(Long id) {
return webClient.get()
.uri("/users/{id}", id)
.retrieve()
.onStatus(HttpStatusCode::is4xxClientError,
res -> Mono.error(new ResourceNotFoundException("User not found")))
.bodyToMono(UserDTO.class);
}
// POST
public Mono<UserDTO> createUser(CreateUserRequest request) {
return webClient.post()
.uri("/users")
.bodyValue(request)
.retrieve()
.bodyToMono(UserDTO.class);
}
}
17. API Documentation with Springdoc OpenAPI (Swagger)
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.3.0</version>
</dependency>
# application.properties springdoc.api-docs.path=/api-docs springdoc.swagger-ui.path=/swagger-ui.html springdoc.swagger-ui.enabled=true
@RestController
@RequestMapping("/api/v1/users")
@Tag(name = "User Management", description = "APIs for managing users")
public class UserController {
@Operation(summary = "Get user by ID", description = "Returns a single user")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "User found"),
@ApiResponse(responseCode = "404", description = "User not found")
})
@GetMapping("/{id}")
public ResponseEntity<UserDTO> getUserById(
@Parameter(description = "User ID", required = true)
@PathVariable Long id) {
return ResponseEntity.ok(userService.getUserById(id));
}
}
Access Swagger UI at: http://localhost:8080/swagger-ui.html
18. Rate Limiting (Using Bucket4j)
<dependency>
<groupId>com.bucket4j</groupId>
<artifactId>bucket4j-core</artifactId>
<version>8.7.0</version>
</dependency>
@Component
public class RateLimitInterceptor implements HandlerInterceptor {
private final Bucket bucket = Bucket.builder()
.addLimit(Bandwidth.classic(100, Refill.greedy(100, Duration.ofMinutes(1))))
.build();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
if (bucket.tryConsume(1)) {
return true;
}
response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
response.getWriter().write("Rate limit exceeded");
return false;
}
}
19. REST API Best Practices
URI Design
✅ GET /api/v1/users → list all users
✅ POST /api/v1/users → create user
✅ GET /api/v1/users/{id} → get user by id
✅ PUT /api/v1/users/{id} → full update
✅ PATCH /api/v1/users/{id} → partial update
✅ DELETE /api/v1/users/{id} → delete
✅ GET /api/v1/users/{id}/orders → nested resource
❌ /api/v1/getUsers
❌ /api/v1/createUser
❌ /api/v1/user_list
General Rules
- Use nouns, not verbs in URIs
- Use plural resource names (
/users, not/user) - Use kebab-case for multi-word paths (
/user-profiles) - Always version your API (
/api/v1/) - Return proper HTTP status codes
- Use DTOs — never expose entities
- Validate all incoming data
- Handle errors consistently with
@RestControllerAdvice - Use HTTPS in production
- Paginate large collections
- Add API documentation (Swagger/OpenAPI)
20. Complete Project Structure for REST API
src/main/java/com/example/
├── controller/
│ └── UserController.java
├── service/
│ ├── UserService.java ← Interface
│ └── impl/
│ └── UserServiceImpl.java ← Implementation
├── repository/
│ └── UserRepository.java
├── model/
│ └── entity/
│ └── User.java
├── dto/
│ ├── request/
│ │ ├── CreateUserRequest.java
│ │ └── UpdateUserRequest.java
│ └── response/
│ └── UserDTO.java
├── mapper/
│ └── UserMapper.java
├── exception/
│ ├── ResourceNotFoundException.java
│ ├── GlobalExceptionHandler.java
│ └── ErrorResponse.java
└── config/
├── SecurityConfig.java
├── CorsConfig.java
└── SwaggerConfig.java
Interview Questions — Spring REST
Beginner Level
Q1. What is the difference between @Controller and @RestController?
@Controlleris used for MVC applications that return view names (HTML pages). It requires@ResponseBodyon each method to return data.@RestController=@Controller+@ResponseBodyapplied to all methods. Every method returns data (JSON/XML) directly, not view names. Use@RestControllerfor all REST APIs.
Q2. What is the difference between @RequestParam and @PathVariable?
@PathVariable— extracts value from the URL path segment:textGET /users/42 → @PathVariable Long id = 42
@RequestParam— extracts value from the query string:textGET /users?page=0&size=10 → @RequestParam int page = 0
Q3. What does @RequestBody do? What converts JSON to a Java object?
@RequestBodytells Spring to read the HTTP request body and deserialize it into the annotated Java object. Jackson (ObjectMapper) handles the conversion automatically. Spring Boot auto-configures Jackson viaMappingJackson2HttpMessageConverter.
Q4. What is ResponseEntity? Why use it instead of returning the object directly?
ResponseEntity<T>wraps the response body, HTTP status code, and headers. Returning the object directly always gives200 OK.ResponseEntitylets you return201 Created,204 No Content,404 Not Found, custom headers, etc., giving full control over the HTTP response.
Q5. What is @RestControllerAdvice? How does it differ from @ControllerAdvice?
Both provide global exception handling across all controllers.
@ControllerAdvice— for MVC apps (returns view names)@RestControllerAdvice=@ControllerAdvice+@ResponseBody— for REST APIs (returns JSON error responses)
Q6. What is the difference between PUT and PATCH?
PUT— replaces the entire resource. Fields not included are set to null/default.PATCH— applies a partial update. Only the fields provided are modified. Example: updating only a user's phone number should usePATCH.
Q7. What HTTP status code should a POST (create) endpoint return?
201 Createdwith aLocationheader pointing to the new resource URL.javareturn ResponseEntity.created(URI.create("/api/users/" + id)).body(dto);
Q8. How do you enable validation in Spring Boot REST?
- Add
spring-boot-starter-validationdependency- Annotate the request DTO fields with constraints (
@NotBlank,- Add
@Validor@Validatedbefore@RequestBodyin the controller- Handle
MethodArgumentNotValidExceptionin@RestControllerAdvice
Intermediate Level
Q9. What is DispatcherServlet? Explain its role.
DispatcherServletis the Front Controller in Spring MVC. All HTTP requests pass through it. It delegates toHandlerMappingto find the correct controller, then toHandlerAdapterto invoke it, then usesHttpMessageConverterto serialize the response. Spring Boot auto-registers it at/.
Q10. What is HttpMessageConverter? How does Spring decide which one to use?
HttpMessageConverteris an interface responsible for reading/writing HTTP request/response bodies. Spring uses Content Negotiation:
Content-Typeheader → selects converter for reading request bodyAcceptheader → selects converter for writing responseDefault converters:
MappingJackson2HttpMessageConverter(JSON),StringHttpMessageConverter, etc.
Q11. What is content negotiation? How does it work in Spring?
Content negotiation is the mechanism by which the server determines the response format based on the client's
Acceptheader. Spring checks:
Acceptheader (e.g.,application/json,application/xml)- URL suffix (deprecated in Spring 6)
- Request parameter (
?format=json)The first matching
HttpMessageConverteris used.
Q12. What is the difference between @Valid and @Validated?
@Valid— standard JSR-303/Bean Validation annotation, supports nested validation@Validated— Spring-specific, supports validation groups (apply different constraints in different scenarios)javapublic interface OnCreate { } public interface OnUpdate { } public record UserRequest( @NotNull(groups = OnCreate.class) String name, @NotNull(groups = OnUpdate.class) Long id ) { } @PostMapping public void create(@Validated(OnCreate.class) @RequestBody UserRequest req) { }
Q13. What is CORS? How do you configure it globally in Spring?
CORS (Cross-Origin Resource Sharing) is a browser security mechanism that blocks requests from a different origin (protocol + domain + port) unless the server explicitly allows it.
Spring adds
Access-Control-Allow-Originheaders. Global config viaWebMvcConfigurer.addCorsMappings()is the cleanest approach.
Q14. What is the difference between a Filter and a HandlerInterceptor?
Filter — Servlet-level, runs before Spring context. Can intercept ALL requests including static resources. Used for encoding, logging, security (e.g., Spring Security uses filters). HandlerInterceptor — Spring MVC level. Only intercepts requests mapped to
DispatcherServlet. Has access to handler (controller) information and Spring beans. Used for auth checks, locale, logging.
Q15. How do you handle exceptions globally in a Spring REST application?
Use
@RestControllerAdvicewith@ExceptionHandlermethods:
- Catches specific exceptions thrown anywhere in controller/service layer
- Returns a consistent
ErrorResponseJSON with proper HTTP status code- Avoids try-catch in every controller method
Q16. What is the difference between RestTemplate and WebClient?
RestTemplate— synchronous, blocking HTTP client. Each call blocks the thread until response arrives. Being deprecated in newer Spring versions.WebClient— non-blocking, reactive HTTP client from Spring WebFlux. Can be used in both reactive and traditional apps. Supports async/reactive chains. Preferred for new code.
Q17. How do you version a REST API? What are the trade-offs?
Four strategies:
- URI (
/v1/users) — most visible, easy to test, breaks REST's uniform interface principle- Request param (
?version=1) — messy URL- Header (
X-API-Version: 1) — clean URL, harder to test in browser- Media type (
application/vnd.app.v1+json) — most RESTful, complexURI versioning is most commonly used in practice.
Q18. How do you implement pagination in a REST API?
Use
Pageablefrom Spring Data:java@GetMapping public Page<UserDTO> getUsers( @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "10") int size) { return userService.getUsers(PageRequest.of(page, size)); }Response includes
content,totalElements,totalPages,number,size.
Advanced Level
Q19. How does Spring Security integrate with Spring REST? What is the request processing order?
Spring Security uses a filter chain that runs before
DispatcherServlet:textRequest → SecurityFilterChain → DispatcherServlet → ControllerKey filters:
UsernamePasswordAuthenticationFilter,JwtAuthenticationFilter(custom),ExceptionTranslationFilter,FilterSecurityInterceptor. For REST APIs, session-based auth is replaced with stateless JWT.
Q20. What is JWT? How does it work with Spring REST?
JWT (JSON Web Token) — a compact, self-contained token with three parts: Header.Payload.Signature.
Flow:
- Client sends credentials → server validates → returns JWT
- Client includes JWT in
Authorization: Bearer <token>header- Server validates signature on every request (stateless)
Spring implementation:
- Custom
JwtAuthenticationFilterextendsOncePerRequestFilter- Extracts and validates token, sets
SecurityContext- No server-side session storage needed
Q21. What is idempotency? Why does it matter in REST API design?
An operation is idempotent if calling it multiple times produces the same result as calling it once.
GET,PUT,DELETE→ idempotentPOST→ NOT idempotent (creates a new resource each time)Matters in practice: if a client retries a request (network failure), idempotent operations are safe to retry. For POST, implement idempotency keys (
Idempotency-Keyheader) to prevent duplicate creation.
Q22. How would you design a REST API that returns large datasets efficiently?
Multiple strategies:
- Pagination —
PageRequest.of(page, size)- Cursor-based pagination — more efficient for large datasets (no OFFSET)
- Projections/DTOs — fetch only needed fields
- Streaming —
StreamingResponseBodyor reactiveFlux<T>to stream data without loading all in memory- Compression — enable GZIP:
server.compression.enabled=true- Caching —
@Cacheablewith Redis for frequently read data
Q23. What is @ResponseStatus? When would you use it?
Sets the HTTP status code for a controller method or exception class:
java@ResponseStatus(HttpStatus.CREATED) @PostMapping public UserDTO createUser(@RequestBody CreateUserRequest req) { ... } @ResponseStatus(HttpStatus.NOT_FOUND) public class ResourceNotFoundException extends RuntimeException { ... }Simpler alternative to
ResponseEntitywhen you only need to set status without a custom body. Less flexible — preferResponseEntityfor REST APIs.
Q24. How do you test a REST controller in Spring Boot?
Use
@WebMvcTestfor unit-testing the controller layer (only loads web layer, not full context):java@WebMvcTest(UserController.class) class UserControllerTest { @Autowired private MockMvc mockMvc; @MockBean private UserService userService; @Test void shouldReturnUser() throws Exception { when(userService.getUserById(1L)).thenReturn(new UserDTO(1L, "John", "john@mail.com")); mockMvc.perform(get("/api/v1/users/1") .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(jsonPath("$.name").value("John")) .andExpect(jsonPath("$.email").value("john@mail.com")); } @Test void shouldReturn404WhenUserNotFound() throws Exception { when(userService.getUserById(99L)).thenThrow(new ResourceNotFoundException("Not found")); mockMvc.perform(get("/api/v1/users/99")) .andExpect(status().isNotFound()); } }
Q25. What is the difference between @WebMvcTest and @SpringBootTest for REST testing?
@WebMvcTest— loads only the web layer (controllers, filters, advice). Fast. Use@MockBeanfor service dependencies. Best for controller unit tests.
@SpringBootTest— loads the full application context. Slower. Use for integration tests with real database or full service interaction. UseTestRestTemplateorMockMvcto make requests.java@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class UserIntegrationTest { @Autowired private TestRestTemplate restTemplate; @Test void fullFlowTest() { ResponseEntity<UserDTO> response = restTemplate.getForEntity("/api/v1/users/1", UserDTO.class); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); } }
Happy Coding!