Tutorials Logic, IN info@tutorialslogic.com

Spring REST API @RestController, @RequestMapping, JSON

REST Request Pipeline

A Spring MVC REST request moves through routing, argument binding, validation, controller coordination, application services, and HTTP message conversion. @RestController makes return values response bodies, while mapping annotations connect HTTP methods and paths to handler methods.

A production-quality API is more than a controller that returns JSON. It needs stable transport DTOs, correct method and status semantics, consistent error documents, bounded collections, authorization at the resource boundary, and tests that verify the HTTP contract without coupling clients to implementation details.

Resource Routes

Name routes after resources and use HTTP methods to express the operation. GET /api/orders reads a collection, POST creates within it, GET /api/orders/{id} reads one resource, PUT replaces state, PATCH applies a partial change, and DELETE removes it. Avoid action-heavy paths when standard semantics already describe the intent.

Mapping annotations can constrain path, method, consumed media type, and produced media type. Bind path identifiers with @PathVariable, query controls with @RequestParam, and JSON payloads with @RequestBody. Keep transport input separate from persistence entities.

Complete REST Controller

Complete REST Controller
package com.example.controller;

import com.example.entity.User;
import com.example.service.UserService;
import org.springframework.data.domain.*;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;
import jakarta.validation.Valid;
import java.net.URI;

@RestController
@RequestMapping("/api/v1/users")
@CrossOrigin(origins = "http://localhost:3000") // Allow CORS from React app
public class UserRestController {

    private final UserService userService;

    public UserRestController(UserService userService) {
        this.userService = userService;
    }

    // GET /api/v1/users?page=0&size=10&sort=username
    @GetMapping
    public ResponseEntity<Page<User>> getUsers(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "10") int size,
            @RequestParam(defaultValue = "id") String sort) {
        Page<User> users = userService.getUsers(page, size, sort);
        return ResponseEntity.ok(users);
    }

    // GET /api/v1/users/42
    @GetMapping("/{id}")
    public ResponseEntity<User> getUser(@PathVariable Long id) {
        User user = userService.getUserById(id);
        return ResponseEntity.ok(user);
    }

    // POST /api/v1/users
    @PostMapping
    public ResponseEntity<User> createUser(@Valid @RequestBody User user) {
        User created = userService.createUser(user);
        URI location = URI.create("/api/v1/users/" + created.getId());
        return ResponseEntity.created(location).body(created); // 201 Created
    }

    // PUT /api/v1/users/42
    @PutMapping("/{id}")
    public ResponseEntity<User> updateUser(@PathVariable Long id,
                                            @Valid @RequestBody User user) {
        User updated = userService.updateUser(id, user);
        return ResponseEntity.ok(updated);
    }

    // DELETE /api/v1/users/42
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
        userService.deleteUser(id);
        return ResponseEntity.noContent().build(); // 204 No Content
    }
}

DTO Validation

Use request DTOs to declare the fields a client may send. Bean Validation annotations describe structural rules such as required text, length, ranges, and valid email shape. @Valid triggers validation during binding, before business logic runs.

Validation cannot decide every business rule. Availability, ownership, state transitions, and uniqueness require services and database constraints. Treat client validation as useful feedback, server validation as mandatory, and database constraints as the final integrity boundary.

Validated Create Request

Validated Create Request
public record CreateOrderRequest(
    @jakarta.validation.constraints.NotBlank String customerReference,
    @jakarta.validation.constraints.Positive int quantity
) {}

@PostMapping
ResponseEntity<OrderResponse> create(
        @jakarta.validation.Valid @RequestBody CreateOrderRequest request) {
    OrderResponse created = service.create(request);
    URI location = URI.create("/api/orders/" + created.id());
    return ResponseEntity.created(location).body(created);
}

A successful creation returns 201 and a Location header that identifies the new order. Invalid structural input is rejected before service.create() runs.

HTTP Responses

Return a body object directly when 200 with normal negotiation is sufficient. Use ResponseEntity when the handler must control status or headers. Creation commonly returns 201 with Location, deletion may return 204, a missing resource returns 404, incompatible state can return 409, and malformed input returns 400.

Keep response DTO fields deliberate and backwards-compatible. Avoid leaking stack traces, entity internals, lazy relationships, security decisions, or database column names. Dates, money, identifiers, and enums need explicit JSON contracts.

Outcome Status Body expectation
Representation returned 200 OK Resource or collection DTO
Resource created 201 Created Resource DTO and Location
Success without content 204 No Content No body
Invalid request shape 400 Bad Request Stable validation details
Resource absent 404 Not Found Stable problem document
State conflict 409 Conflict Conflict explanation safe for client

Problem Responses

Centralize exception-to-response mapping with @RestControllerAdvice so controllers focus on successful coordination. Map known domain failures deliberately and let unexpected exceptions become a generic 500 response while detailed diagnostics stay in server logs.

Spring supports ProblemDetail for structured HTTP error responses. Keep type, title, status, detail, instance, and any extension fields stable enough for clients. Do not expose internal exception text automatically.

@ControllerAdvice Global Exception Handler

@ControllerAdvice Global Exception Handler
import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import java.util.*;

@RestControllerAdvice // Handles exceptions for all @RestController classes
public class GlobalExceptionHandler {

    // Handle resource not found
    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
        ErrorResponse error = new ErrorResponse(404, ex.getMessage());
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
    }

    // Handle validation errors
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, String>> handleValidation(
            MethodArgumentNotValidException ex) {
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getAllErrors().forEach(error -> {
            String field = ((FieldError) error).getField();
            String message = error.getDefaultMessage();
            errors.put(field, message);
        });
        return ResponseEntity.badRequest().body(errors);
    }

    // Handle all other exceptions
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGeneral(Exception ex) {
        ErrorResponse error = new ErrorResponse(500, "Internal server error");
        return ResponseEntity.internalServerError().body(error);
    }

    record ErrorResponse(int status, String message) {}
}

Collections and CORS

Collection endpoints need bounded pagination, deterministic sorting, and an allowlist of filter and sort fields. Returning every row makes latency and memory usage grow with the database. Include navigation or page metadata that matches the client workflow.

CORS is a browser access-control mechanism, not authentication. Configure exact trusted origins, methods, headers, and credential behavior. A wildcard origin cannot be combined safely with credentialed browser requests. Authorization must still run for every protected request.

Global CORS Configuration

Global CORS Configuration
@Configuration
public class CorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
                .allowedOrigins("http://localhost:3000", "https://myapp.com")
                .allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")
                .allowedHeaders("*")
                .allowCredentials(true)
                .maxAge(3600); // Cache preflight for 1 hour
    }
}

// Or use @CrossOrigin on individual controllers/methods:
// @CrossOrigin(origins = "*", maxAge = 3600)
// @CrossOrigin(origins = {"http://localhost:3000", "https://myapp.com"})

Contract Tests

A focused MVC test should verify route, status, content type, important JSON fields, validation errors, and exception mapping. Service behavior can be mocked in a controller slice, while broader integration tests prove serialization, security, persistence, and configuration together.

MockMvc Contract Check

MockMvc Contract Check
@Test
void rejectsMissingCustomerReference() throws Exception {
    mvc.perform(post("/api/orders")
            .contentType(MediaType.APPLICATION_JSON)
            .content("{\"customerReference\":\"\",\"quantity\":2}"))
        .andExpect(status().isBadRequest())
        .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_PROBLEM_JSON));
}
Before you move on

API Contract Review

5 checks
  • Use resource routes and HTTP methods with deliberate semantics.
  • Bind clients to DTOs rather than persistence entities.
  • Return statuses, headers, and bodies that describe the actual outcome.
  • Centralize stable validation and problem responses.
  • Bound collections and test the observable HTTP contract.

REST Boundary Mistakes

  • Entity returned directly

    Map persistence state to a stable response DTO.
  • Every exception becomes 500

    Map known validation, absence, authorization, and conflict cases deliberately.
  • CORS treated as security

    Configure CORS for browser access and enforce authentication and authorization separately.

Try this next

Complete One API Slice

0 of 3 completed

  1. Implement POST and GET with request/response DTOs, 201 Location, and 404 handling.
  2. Return a consistent problem document for blank references and non-positive quantities.
  3. Verify success, creation, validation failure, missing resource, conflict, and unauthorized access.

REST Design Questions

Use it when the handler needs explicit status or headers; a plain DTO is enough for an ordinary 200 response body.

For nontrivial applications, use a service boundary so business rules and transactions remain independent of HTTP.

No. It validates the bound object; concurrent writes and final integrity still require service rules and database constraints.

Browse Free Tutorials

Explore 500+ free tutorials across 20+ languages and frameworks.