REST API patterns for Spring Boot including controllers, services, repositories, DTOs, validation, and error handling.
npx claudepluginhub sumanpapanaboina1983/adlc-accelerator-kit-pluginsThis skill uses the workspace's default tool permissions.
```
Provides Spring Boot patterns for layered architecture, REST controllers, JPA repositories/entities, services, DTOs, mappers, and configuration.
Provides Spring Boot patterns for REST API design, layered architecture (Controller-Service-Repository), Spring Data JPA repositories, transactional services, DTO validation, and global exception handling. Useful for scalable Java backends.
Provides REST API standards for Spring Boot covering URL design, HTTP methods, DTOs, validation, error handling, pagination, and security headers. Use when creating or reviewing endpoints.
Share bugs, ideas, or general feedback.
Controller (HTTP handling)
↓ calls
Service (Business logic)
↓ calls
Repository (Data access)
@RestController@RequestMapping("/api/v1/resource") for base path@GetMapping, @PostMapping, @PutMapping, @DeleteMapping@Valid on @RequestBody parametersResponseEntity<T> for explicit status codes@RestController
@RequestMapping("/api/v1/todos")
@RequiredArgsConstructor
public class TodoController {
private final TodoService todoService;
@GetMapping
public ResponseEntity<List<TodoResponse>> getAll() {
return ResponseEntity.ok(todoService.findAll());
}
@PostMapping
public ResponseEntity<TodoResponse> create(@Valid @RequestBody CreateTodoRequest request) {
TodoResponse created = todoService.create(request);
return ResponseEntity.status(HttpStatus.CREATED).body(created);
}
@GetMapping("/{id}")
public ResponseEntity<TodoResponse> getById(@PathVariable Long id) {
return ResponseEntity.ok(todoService.findById(id));
}
@PutMapping("/{id}")
public ResponseEntity<TodoResponse> update(
@PathVariable Long id,
@Valid @RequestBody UpdateTodoRequest request) {
return ResponseEntity.ok(todoService.update(id, request));
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable Long id) {
todoService.delete(id);
return ResponseEntity.noContent().build();
}
}
@Service@Transactional@Service
@RequiredArgsConstructor
public class TodoService {
private final TodoRepository todoRepository;
public List<TodoResponse> findAll() {
return todoRepository.findAll().stream()
.map(this::toResponse)
.toList();
}
@Transactional
public TodoResponse create(CreateTodoRequest request) {
Todo todo = new Todo();
todo.setTitle(request.title());
todo.setCompleted(false);
todo.setCreatedAt(Instant.now());
Todo saved = todoRepository.save(todo);
return toResponse(saved);
}
public TodoResponse findById(Long id) {
return todoRepository.findById(id)
.map(this::toResponse)
.orElseThrow(() -> new TodoNotFoundException(id));
}
@Transactional
public TodoResponse update(Long id, UpdateTodoRequest request) {
Todo todo = todoRepository.findById(id)
.orElseThrow(() -> new TodoNotFoundException(id));
todo.setTitle(request.title());
todo.setCompleted(request.completed());
Todo saved = todoRepository.save(todo);
return toResponse(saved);
}
@Transactional
public void delete(Long id) {
if (!todoRepository.existsById(id)) {
throw new TodoNotFoundException(id);
}
todoRepository.deleteById(id);
}
private TodoResponse toResponse(Todo todo) {
return new TodoResponse(
todo.getId(),
todo.getTitle(),
todo.isCompleted(),
todo.getCreatedAt()
);
}
}
@Repository annotation@Repository
@RequiredArgsConstructor
public class TodoRepository {
private final JdbcTemplate jdbc;
private static final RowMapper<Todo> ROW_MAPPER = (rs, rowNum) -> {
Todo todo = new Todo();
todo.setId(rs.getLong("id"));
todo.setTitle(rs.getString("title"));
todo.setCompleted(rs.getBoolean("completed"));
todo.setCreatedAt(rs.getTimestamp("created_at").toInstant());
return todo;
};
public List<Todo> findAll() {
return jdbc.query("SELECT * FROM todos ORDER BY created_at DESC", ROW_MAPPER);
}
public Optional<Todo> findById(Long id) {
try {
Todo todo = jdbc.queryForObject(
"SELECT * FROM todos WHERE id = ?",
ROW_MAPPER,
id
);
return Optional.ofNullable(todo);
} catch (EmptyResultDataAccessException e) {
return Optional.empty();
}
}
public boolean existsById(Long id) {
Integer count = jdbc.queryForObject(
"SELECT COUNT(*) FROM todos WHERE id = ?",
Integer.class,
id
);
return count != null && count > 0;
}
public Todo save(Todo todo) {
if (todo.getId() == null) {
KeyHolder keyHolder = new GeneratedKeyHolder();
jdbc.update(connection -> {
PreparedStatement ps = connection.prepareStatement(
"INSERT INTO todos (title, completed, created_at) VALUES (?, ?, ?)",
Statement.RETURN_GENERATED_KEYS
);
ps.setString(1, todo.getTitle());
ps.setBoolean(2, todo.isCompleted());
ps.setTimestamp(3, Timestamp.from(todo.getCreatedAt()));
return ps;
}, keyHolder);
todo.setId(Objects.requireNonNull(keyHolder.getKey()).longValue());
} else {
jdbc.update(
"UPDATE todos SET title = ?, completed = ? WHERE id = ?",
todo.getTitle(),
todo.isCompleted(),
todo.getId()
);
}
return todo;
}
public void deleteById(Long id) {
jdbc.update("DELETE FROM todos WHERE id = ?", id);
}
}
public record CreateTodoRequest(
@NotBlank(message = "Title is required")
@Size(max = 255, message = "Title must be less than 255 characters")
String title
) {}
public record UpdateTodoRequest(
@NotBlank(message = "Title is required")
@Size(max = 255, message = "Title must be less than 255 characters")
String title,
@NotNull(message = "Completed status is required")
Boolean completed
) {}
public record TodoResponse(
Long id,
String title,
boolean completed,
Instant createdAt
) {}
public record ErrorResponse(
int status,
String error,
String message,
Instant timestamp,
String path
) {}
// Custom exception
public class TodoNotFoundException extends RuntimeException {
public TodoNotFoundException(Long id) {
super("Todo not found with id: " + id);
}
}
// Global exception handler
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(TodoNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(
TodoNotFoundException ex,
HttpServletRequest request) {
ErrorResponse error = new ErrorResponse(
HttpStatus.NOT_FOUND.value(),
"Not Found",
ex.getMessage(),
Instant.now(),
request.getRequestURI()
);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidation(
MethodArgumentNotValidException ex,
HttpServletRequest request) {
String message = ex.getBindingResult().getFieldErrors().stream()
.map(e -> e.getField() + ": " + e.getDefaultMessage())
.collect(Collectors.joining(", "));
ErrorResponse error = new ErrorResponse(
HttpStatus.BAD_REQUEST.value(),
"Validation Failed",
message,
Instant.now(),
request.getRequestURI()
);
return ResponseEntity.badRequest().body(error);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGeneral(
Exception ex,
HttpServletRequest request) {
ErrorResponse error = new ErrorResponse(
HttpStatus.INTERNAL_SERVER_ERROR.value(),
"Internal Server Error",
"An unexpected error occurred",
Instant.now(),
request.getRequestURI()
);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
}
}
| Scenario | Status Code |
|---|---|
| GET success | 200 OK |
| POST success (created) | 201 Created |
| PUT/PATCH success | 200 OK |
| DELETE success | 204 No Content |
| Validation error | 400 Bad Request |
| Authentication required | 401 Unauthorized |
| Permission denied | 403 Forbidden |
| Resource not found | 404 Not Found |
| Conflict (duplicate) | 409 Conflict |
| Server error | 500 Internal Server Error |
// Request
@GetMapping
public ResponseEntity<Page<TodoResponse>> getAll(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(defaultValue = "createdAt") String sortBy,
@RequestParam(defaultValue = "desc") String sortDir) {
Sort sort = sortDir.equalsIgnoreCase("asc")
? Sort.by(sortBy).ascending()
: Sort.by(sortBy).descending();
Pageable pageable = PageRequest.of(page, size, sort);
Page<TodoResponse> todos = todoService.findAll(pageable);
return ResponseEntity.ok(todos);
}
// Response includes: content, totalElements, totalPages, size, number