Building REST APIs with Spring MVC
In Spring MVC, you build REST APIs by defining controller classes that map HTTP requests to Java methods. In modern Spring Boot applications, this is usually done with @RestController.
A typical REST API is organized like this:
HTTP Request
↓
Controller
↓
Service
↓
Repository
↓
Database
Each layer has a clear responsibility:
| Layer | Responsibility |
|---|---|
| Controller | Handles HTTP requests and responses |
| Service | Contains business logic |
| Repository | Handles database access |
| Entity | Represents database tables |
| DTO | Represents API request/response data |
1. Add the Spring Web Dependency
For Maven:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
If you need validation:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
If you use JPA:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
2. Create a REST Controller
Use @RestController for REST APIs. It combines @Controller and @ResponseBody, meaning returned objects are written directly to the HTTP response, usually as JSON.
package com.example.demo.user;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@GetMapping("/api/hello")
public String hello() {
return "Hello, REST API!";
}
}
Calling:
GET /api/hello
returns:
Hello, REST API!
3. Design Resource-Based URLs
REST APIs should use nouns for resources and HTTP methods for actions.
Good:
GET /api/users
GET /api/users/1
POST /api/users
PUT /api/users/1
DELETE /api/users/1
Avoid action-style URLs like:
/api/getUsers
/api/createUser
/api/deleteUser
The HTTP method already describes the operation.
4. Create DTOs for Request and Response Bodies
Avoid exposing database entities directly from your API. Use DTOs instead.
package com.example.demo.user;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
public record CreateUserRequest(
@NotBlank String name,
@NotBlank @Email String email
) {
}
package com.example.demo.user;
public record UserResponse(
Long id,
String name,
String email
) {
}
DTOs keep your API contract separate from your database model.
5. Create REST Endpoints
A controller for basic CRUD operations might look like this:
package com.example.demo.user;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping
public List<UserResponse> findAll() {
return userService.findAll();
}
@GetMapping("/{id}")
public UserResponse findById(@PathVariable Long id) {
return userService.findById(id);
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public UserResponse create(@Valid @RequestBody CreateUserRequest request) {
return userService.create(request);
}
@PutMapping("/{id}")
public UserResponse update(
@PathVariable Long id,
@Valid @RequestBody CreateUserRequest request
) {
return userService.update(id, request);
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(@PathVariable Long id) {
userService.delete(id);
}
}
Key annotations:
| Annotation | Purpose |
|---|---|
@RestController |
Marks the class as a REST controller |
@RequestMapping |
Defines a base URL |
@GetMapping |
Handles HTTP GET |
@PostMapping |
Handles HTTP POST |
@PutMapping |
Handles HTTP PUT |
@DeleteMapping |
Handles HTTP DELETE |
@PathVariable |
Reads values from the URL path |
@RequestBody |
Reads JSON from the request body |
@Valid |
Triggers Jakarta Bean Validation |
@ResponseStatus |
Sets the HTTP response status |
6. Put Business Logic in a Service
Controllers should stay thin. Put business rules in a service class.
package com.example.demo.user;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserService {
public List<UserResponse> findAll() {
// Load users from repository
return List.of();
}
public UserResponse findById(Long id) {
// Find user by id
return new UserResponse(id, "Alice", "[email protected]");
}
public UserResponse create(CreateUserRequest request) {
// Create user
return new UserResponse(1L, request.name(), request.email());
}
public UserResponse update(Long id, CreateUserRequest request) {
// Update user
return new UserResponse(id, request.name(), request.email());
}
public void delete(Long id) {
// Delete user
}
}
In a real application, the service would call a repository.
7. Use Spring Data JPA for Persistence
If your API stores data in a database, create an entity and repository.
package com.example.demo.user;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.Getter;
import lombok.Setter;
@Entity
@Getter
@Setter
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
}
package com.example.demo.user;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<User, Long> {
}
By extending JpaRepository, you automatically get methods like:
findAll();
findById(id);
save(entity);
delete(entity);
existsById(id);
8. Return Proper HTTP Status Codes
Use status codes that match the result:
| Situation | Status |
|---|---|
| Successful read | 200 OK |
| Created resource | 201 Created |
| Deleted resource | 204 No Content |
| Invalid request | 400 Bad Request |
| Missing resource | 404 Not Found |
| Conflict | 409 Conflict |
| Server error | 500 Internal Server Error |
For creation, you can also return a Location header:
package com.example.demo.user;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.util.UriComponentsBuilder;
import java.net.URI;
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@PostMapping
public ResponseEntity<UserResponse> create(
@Valid @RequestBody CreateUserRequest request,
UriComponentsBuilder uriBuilder
) {
UserResponse response = userService.create(request);
URI location = uriBuilder
.path("/api/users/{id}")
.buildAndExpand(response.id())
.toUri();
return ResponseEntity.created(location).body(response);
}
}
9. Handle Errors Globally
Use @RestControllerAdvice to return consistent JSON errors.
package com.example.demo.exception;
import java.time.Instant;
import java.util.List;
public record ApiError(
int status,
String error,
String message,
String path,
Instant timestamp,
List<FieldErrorDetail> fieldErrors
) {
public record FieldErrorDetail(
String field,
String message
) {
}
}
package com.example.demo.exception;
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String message) {
super(message);
}
}
package com.example.demo.exception;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.*;
import java.time.Instant;
import java.util.List;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ApiError handleNotFound(
ResourceNotFoundException ex,
HttpServletRequest request
) {
return new ApiError(
404,
"Not Found",
ex.getMessage(),
request.getRequestURI(),
Instant.now(),
List.of()
);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ApiError handleValidation(
MethodArgumentNotValidException ex,
HttpServletRequest request
) {
List<ApiError.FieldErrorDetail> fieldErrors = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(error -> new ApiError.FieldErrorDetail(
error.getField(),
error.getDefaultMessage()
))
.toList();
return new ApiError(
400,
"Bad Request",
"Validation failed",
request.getRequestURI(),
Instant.now(),
fieldErrors
);
}
}
10. Test Your API
Example using curl:
curl -X POST http://localhost:8080/api/users \
-H "Content-Type: application/json" \
-d '{"name":"Alice","email":"[email protected]"}'
Get all users:
curl http://localhost:8080/api/users
Get one user:
curl http://localhost:8080/api/users/1
Update a user:
curl -X PUT http://localhost:8080/api/users/1 \
-H "Content-Type: application/json" \
-d '{"name":"Alice Smith","email":"[email protected]"}'
Delete a user:
curl -X DELETE http://localhost:8080/api/users/1
11. Add Pagination for List Endpoints
For large collections, avoid returning everything at once.
package com.example.demo.user;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping
public Page<UserResponse> findAll(Pageable pageable) {
return userService.findAll(pageable);
}
}
Then clients can call:
GET /api/users?page=0&size=10
GET /api/users?page=0&size=10&sort=name,asc
Recommended Checklist
When building REST APIs with Spring MVC:
- Use
@RestController. - Use resource-based URLs like
/api/users. - Use HTTP methods correctly:
GET,POST,PUT,PATCH,DELETE. - Keep controllers thin.
- Put business logic in services.
- Use repositories for database access.
- Use DTOs instead of exposing entities.
- Validate request bodies with
jakarta.validation. - Handle errors globally with
@RestControllerAdvice. - Return correct HTTP status codes.
- Add pagination for collection endpoints.
- Use
jakarta.*imports in modern Spring Boot applications.
A clean REST API usually follows this shape:
Controller → Service → Repository → Database
That structure keeps your Spring MVC API easier to test, maintain, and evolve.
