Prakhar Singh

Spring REST: In-Depth Notes + Interview Questions

Cover Image for Spring REST: In-Depth Notes + Interview Questions
Prakhar Singh
Prakhar Singh

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

  1. Client-Server — UI and backend are separated
  2. Stateless — every request is self-contained (no session state on server)
  3. Cacheable — responses can be cached
  4. Uniform Interface — standard HTTP methods, URIs, status codes
  5. Layered System — client doesn't know if it's talking to a proxy or real server
  6. Code on Demand (optional) — server can send executable code

HTTP Methods → CRUD Mapping

HTTP MethodCRUDSafeIdempotent
GETRead✅✅
POSTCreate❌❌
PUTReplace (full update)❌✅
PATCHPartial update❌❌
DELETEDelete❌✅

Safe = does not modify data. Idempotent = calling multiple times gives same result.


2. Spring MVC Request Processing Flow

text
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

AnnotationDescription
@RestController@Controller + @ResponseBody — returns data, not views
@ControllerMVC controller — returns view names
@RequestMappingMap URL + HTTP method to class or method
@GetMappingShortcut for @RequestMapping(method = GET)
@PostMappingShortcut for POST
@PutMappingShortcut for PUT
@PatchMappingShortcut for PATCH
@DeleteMappingShortcut for DELETE

Parameter Annotations

AnnotationSourceExample
@PathVariableURL path segment/users/{id}
@RequestParamQuery string/users?page=0&size=10
@RequestBodyRequest body (JSON)POST/PUT body
@RequestHeaderHTTP headerAuthorization header
@CookieValueCookieSession cookies
@ModelAttributeForm dataHTML form POST
@Valid / @ValidatedTrigger validation on bodyWith @RequestBody

4. Building a Complete REST Controller

java
@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).

java
// 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

CodeMeaningWhen to use
200 OKSuccessGET, PUT, PATCH responses
201 CreatedResource createdPOST success
204 No ContentSuccess, no bodyDELETE, PATCH with no return
400 Bad RequestInvalid inputValidation failures
401 UnauthorizedNot authenticatedMissing/invalid token
403 ForbiddenNot authorizedAuthenticated but lacks permission
404 Not FoundResource missingEntity not found
409 ConflictState conflictDuplicate email, version mismatch
422 Unprocessable EntitySemantic validation errorBusiness rule violations
500 Internal Server ErrorUnexpected errorUnhandled exceptions

6. Request & Response DTOs

Never expose JPA entities directly in REST APIs. Use DTOs.

java
// 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)

java
@Service
public class UserService {

    public UserDTO toDTO(User user) {
        return new UserDTO(
            user.getId(),
            user.getName(),
            user.getEmail(),
            user.isActive(),
            user.getCreatedAt()
        );
    }
}
java
@Mapper(componentModel = "spring")
public interface UserMapper {
    UserDTO toDTO(User user);
    User toEntity(CreateUserRequest request);
}
xml
<!-- pom.xml -->
<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct</artifactId>
    <version>1.5.5.Final</version>
</dependency>

7. Bean Validation

xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

Validation Annotations

AnnotationDescription
@NotNullField must not be null
@NotBlankString must not be null or blank
@NotEmptyCollection/String must not be empty
@Size(min, max)String length or collection size
@Min(value)Minimum numeric value
@Max(value)Maximum numeric value
@EmailValid email format
@Pattern(regexp)Must match regex
@PositiveMust be > 0
@PositiveOrZeroMust be >= 0
@PastDate must be in the past
@FutureDate must be in the future
@ValidCascades validation to nested objects
java
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

java
// 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

java
@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)
        );
    }
}
java
// 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.

xml
<!-- Add XML support -->
<dependency>
    <groupId>com.fasterxml.jackson.dataformat</groupId>
    <artifactId>jackson-dataformat-xml</artifactId>
</dependency>
java
@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

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

AnnotationPurpose
@JsonProperty("user_name")Custom field name in JSON
@JsonIgnoreExclude 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
java
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)

java
@GetMapping("/api/v1/users")
@GetMapping("/api/v2/users")

2. Request Parameter Versioning

java
@GetMapping(value = "/users", params = "version=1")
@GetMapping(value = "/users", params = "version=2")

3. Header Versioning

java
@GetMapping(value = "/users", headers = "X-API-Version=1")
@GetMapping(value = "/users", headers = "X-API-Version=2")

4. Media Type (Content Negotiation) Versioning

java
@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.

xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>
java
@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:

json
{
  "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

java
@CrossOrigin(origins = "http://localhost:3000")
@GetMapping("/users")
public List<UserDTO> getUsers() { ... }

Controller-level

java
@CrossOrigin(origins = "*", allowedHeaders = "*")
@RestController
public class UserController { }
java
@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)

java
@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)

java
@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

FilterHandlerInterceptor
LevelServlet containerSpring MVC
Access to Spring beansLimitedFull
Request scopeAll requestsOnly mapped to DispatcherServlet
Use forSecurity, logging, encodingAuth, logging, locale

15. RestTemplate (Legacy — Synchronous)

Used to call external REST APIs.

java
@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).

xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
java
@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)

xml
<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
    <version>2.3.0</version>
</dependency>
properties
# application.properties
springdoc.api-docs.path=/api-docs
springdoc.swagger-ui.path=/swagger-ui.html
springdoc.swagger-ui.enabled=true
java
@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)

xml
<dependency>
    <groupId>com.bucket4j</groupId>
    <artifactId>bucket4j-core</artifactId>
    <version>8.7.0</version>
</dependency>
java
@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

text
✅ 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

text
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?

@Controller is used for MVC applications that return view names (HTML pages). It requires @ResponseBody on each method to return data. @RestController = @Controller + @ResponseBody applied to all methods. Every method returns data (JSON/XML) directly, not view names. Use @RestController for all REST APIs.


Q2. What is the difference between @RequestParam and @PathVariable?

@PathVariable — extracts value from the URL path segment:

text
GET /users/42  →  @PathVariable Long id = 42

@RequestParam — extracts value from the query string:

text
GET /users?page=0&size=10  →  @RequestParam int page = 0

Q3. What does @RequestBody do? What converts JSON to a Java object?

@RequestBody tells 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 via MappingJackson2HttpMessageConverter.


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 gives 200 OK. ResponseEntity lets you return 201 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 use PATCH.


Q7. What HTTP status code should a POST (create) endpoint return?

201 Created with a Location header pointing to the new resource URL.

java
return ResponseEntity.created(URI.create("/api/users/" + id)).body(dto);

Q8. How do you enable validation in Spring Boot REST?

  1. Add spring-boot-starter-validation dependency
  2. Annotate the request DTO fields with constraints (@NotBlank, @Email, etc.)
  3. Add @Valid or @Validated before @RequestBody in the controller
  4. Handle MethodArgumentNotValidException in @RestControllerAdvice

Intermediate Level

Q9. What is DispatcherServlet? Explain its role.

DispatcherServlet is the Front Controller in Spring MVC. All HTTP requests pass through it. It delegates to HandlerMapping to find the correct controller, then to HandlerAdapter to invoke it, then uses HttpMessageConverter to serialize the response. Spring Boot auto-registers it at /.


Q10. What is HttpMessageConverter? How does Spring decide which one to use?

HttpMessageConverter is an interface responsible for reading/writing HTTP request/response bodies. Spring uses Content Negotiation:

  • Content-Type header → selects converter for reading request body
  • Accept header → selects converter for writing response

Default 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 Accept header. Spring checks:

  1. Accept header (e.g., application/json, application/xml)
  2. URL suffix (deprecated in Spring 6)
  3. Request parameter (?format=json)

The first matching HttpMessageConverter is 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)

java
public 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-Origin headers. Global config via WebMvcConfigurer.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 @RestControllerAdvice with @ExceptionHandler methods:

  • Catches specific exceptions thrown anywhere in controller/service layer
  • Returns a consistent ErrorResponse JSON 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:

  1. URI (/v1/users) — most visible, easy to test, breaks REST's uniform interface principle
  2. Request param (?version=1) — messy URL
  3. Header (X-API-Version: 1) — clean URL, harder to test in browser
  4. Media type (application/vnd.app.v1+json) — most RESTful, complex

URI versioning is most commonly used in practice.


Q18. How do you implement pagination in a REST API?

Use Pageable from 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:

text
Request → SecurityFilterChain → DispatcherServlet → Controller

Key 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:

  1. Client sends credentials → server validates → returns JWT
  2. Client includes JWT in Authorization: Bearer <token> header
  3. Server validates signature on every request (stateless)

Spring implementation:

  • Custom JwtAuthenticationFilter extends OncePerRequestFilter
  • 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 → idempotent
  • POST → 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-Key header) to prevent duplicate creation.


Q22. How would you design a REST API that returns large datasets efficiently?

Multiple strategies:

  1. Pagination — PageRequest.of(page, size)
  2. Cursor-based pagination — more efficient for large datasets (no OFFSET)
  3. Projections/DTOs — fetch only needed fields
  4. Streaming — StreamingResponseBody or reactive Flux<T> to stream data without loading all in memory
  5. Compression — enable GZIP: server.compression.enabled=true
  6. Caching — @Cacheable with 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 ResponseEntity when you only need to set status without a custom body. Less flexible — prefer ResponseEntity for REST APIs.


Q24. How do you test a REST controller in Spring Boot?

Use @WebMvcTest for 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 @MockBean for 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. Use TestRestTemplate or MockMvc to 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!