597 lines
18 KiB
Markdown
597 lines
18 KiB
Markdown
# Spring Boot REST API Examples
|
|
|
|
## Complete CRUD REST API with Validation
|
|
|
|
### Entity with Validation
|
|
```java
|
|
@Entity
|
|
@Table(name = "users")
|
|
public class User {
|
|
@Id
|
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
private Long id;
|
|
|
|
@NotBlank(message = "Name is required")
|
|
@Size(min = 2, max = 100, message = "Name must be 2-100 characters")
|
|
private String name;
|
|
|
|
@NotBlank(message = "Email is required")
|
|
@Email(message = "Valid email required")
|
|
@Column(unique = true)
|
|
private String email;
|
|
|
|
@Min(value = 18, message = "Must be at least 18")
|
|
@Max(value = 120, message = "Invalid age")
|
|
private Integer age;
|
|
|
|
@Size(min = 8, max = 100, message = "Password must be 8-100 characters")
|
|
private String password;
|
|
|
|
@Column(name = "is_active")
|
|
private Boolean active = true;
|
|
|
|
private LocalDateTime createdAt;
|
|
private LocalDateTime updatedAt;
|
|
|
|
@PrePersist
|
|
protected void onCreate() {
|
|
createdAt = LocalDateTime.now();
|
|
updatedAt = LocalDateTime.now();
|
|
}
|
|
|
|
@PreUpdate
|
|
protected void onUpdate() {
|
|
updatedAt = LocalDateTime.now();
|
|
}
|
|
}
|
|
```
|
|
|
|
### Service with Transaction Management
|
|
```java
|
|
@Service
|
|
@RequiredArgsConstructor
|
|
@Slf4j
|
|
@Transactional
|
|
public class UserService {
|
|
|
|
private final UserRepository userRepository;
|
|
private final EmailService emailService;
|
|
|
|
@Transactional(readOnly = true)
|
|
public Page<UserResponse> findAll(Pageable pageable) {
|
|
log.debug("Fetching users page {} size {}", pageable.getPageNumber(), pageable.getPageSize());
|
|
return userRepository.findAll(pageable)
|
|
.map(this::toResponse);
|
|
}
|
|
|
|
@Transactional(readOnly = true)
|
|
public UserResponse findById(Long id) {
|
|
log.debug("Looking for user with id {}", id);
|
|
return userRepository.findById(id)
|
|
.map(this::toResponse)
|
|
.orElseThrow(() -> new EntityNotFoundException("User not found"));
|
|
}
|
|
|
|
@Transactional
|
|
public UserResponse create(CreateUserRequest request) {
|
|
log.info("Creating user with email: {}", request.getEmail());
|
|
|
|
if (userRepository.existsByEmail(request.getEmail())) {
|
|
throw new BusinessException("Email already exists");
|
|
}
|
|
|
|
User user = new User();
|
|
user.setName(request.getName());
|
|
user.setEmail(request.getEmail());
|
|
user.setAge(request.getAge());
|
|
user.setPassword(passwordEncoder.encode(request.getPassword()));
|
|
|
|
User saved = userRepository.save(user);
|
|
emailService.sendWelcomeEmail(saved.getEmail(), saved.getName());
|
|
|
|
return toResponse(saved);
|
|
}
|
|
|
|
@Transactional
|
|
public UserResponse update(Long id, UpdateUserRequest request) {
|
|
User user = userRepository.findById(id)
|
|
.orElseThrow(() -> new EntityNotFoundException("User not found"));
|
|
|
|
if (request.getName() != null) {
|
|
user.setName(request.getName());
|
|
}
|
|
if (request.getEmail() != null) {
|
|
if (!user.getEmail().equals(request.getEmail()) &&
|
|
userRepository.existsByEmail(request.getEmail())) {
|
|
throw new BusinessException("Email already exists");
|
|
}
|
|
user.setEmail(request.getEmail());
|
|
}
|
|
if (request.getAge() != null) {
|
|
user.setAge(request.getAge());
|
|
}
|
|
|
|
User updated = userRepository.save(user);
|
|
return toResponse(updated);
|
|
}
|
|
|
|
@Transactional
|
|
public void delete(Long id) {
|
|
if (!userRepository.existsById(id)) {
|
|
throw new EntityNotFoundException("User not found");
|
|
}
|
|
|
|
User user = userRepository.findById(id).orElseThrow();
|
|
emailService.sendDeletionEmail(user.getEmail(), user.getName());
|
|
userRepository.deleteById(id);
|
|
}
|
|
|
|
private UserResponse toResponse(User user) {
|
|
return new UserResponse(
|
|
user.getId(),
|
|
user.getName(),
|
|
user.getEmail(),
|
|
user.getAge(),
|
|
user.getActive(),
|
|
user.getCreatedAt(),
|
|
user.getUpdatedAt()
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Controller with Proper HTTP Methods
|
|
```java
|
|
@RestController
|
|
@RequestMapping("/api/users")
|
|
@RequiredArgsConstructor
|
|
@Slf4j
|
|
public class UserController {
|
|
|
|
private final UserService userService;
|
|
|
|
@GetMapping
|
|
public ResponseEntity<Page<UserResponse>> getAllUsers(
|
|
@RequestParam(defaultValue = "0") int page,
|
|
@RequestParam(defaultValue = "10") int size,
|
|
@RequestParam(defaultValue = "createdAt") String sortBy,
|
|
@RequestParam(defaultValue = "DESC") String sortDirection) {
|
|
|
|
Sort sort = Sort.by(Sort.Direction.fromString(sortDirection), sortBy);
|
|
Pageable pageable = PageRequest.of(page, size, sort);
|
|
Page<UserResponse> users = userService.findAll(pageable);
|
|
|
|
HttpHeaders headers = new HttpHeaders();
|
|
headers.add("X-Total-Count", String.valueOf(users.getTotalElements()));
|
|
headers.add("X-Total-Pages", String.valueOf(users.getTotalPages()));
|
|
|
|
return ResponseEntity.ok()
|
|
.headers(headers)
|
|
.body(users);
|
|
}
|
|
|
|
@GetMapping("/{id}")
|
|
public ResponseEntity<UserResponse> getUserById(@PathVariable Long id) {
|
|
return ResponseEntity.ok(userService.findById(id));
|
|
}
|
|
|
|
@PostMapping
|
|
public ResponseEntity<UserResponse> createUser(@Valid @RequestBody CreateUserRequest request) {
|
|
UserResponse created = userService.create(request);
|
|
return ResponseEntity.status(HttpStatus.CREATED)
|
|
.header("Location", "/api/users/" + created.getId())
|
|
.body(created);
|
|
}
|
|
|
|
@PutMapping("/{id}")
|
|
public ResponseEntity<UserResponse> updateUser(
|
|
@PathVariable Long id,
|
|
@Valid @RequestBody UpdateUserRequest request) {
|
|
UserResponse updated = userService.update(id, request);
|
|
return ResponseEntity.ok(updated);
|
|
}
|
|
|
|
@PatchMapping("/{id}")
|
|
public ResponseEntity<UserResponse> patchUser(
|
|
@PathVariable Long id,
|
|
@Valid @RequestBody UpdateUserRequest request) {
|
|
UserResponse updated = userService.update(id, request);
|
|
return ResponseEntity.ok(updated);
|
|
}
|
|
|
|
@DeleteMapping("/{id}")
|
|
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
|
|
userService.delete(id);
|
|
return ResponseEntity.noContent().build();
|
|
}
|
|
}
|
|
```
|
|
|
|
## API Versioning Examples
|
|
|
|
### URL Versioning
|
|
```java
|
|
@RestController
|
|
@RequestMapping("/api/v1/users")
|
|
public class UserControllerV1 {
|
|
// Version 1 endpoints
|
|
}
|
|
|
|
@RestController
|
|
@RequestMapping("/api/v2/users")
|
|
public class UserControllerV2 {
|
|
// Version 2 endpoints with different response format
|
|
}
|
|
```
|
|
|
|
### Header Versioning
|
|
```java
|
|
@RestController
|
|
@RequestMapping("/api/users")
|
|
public class UserController {
|
|
|
|
@GetMapping
|
|
public ResponseEntity<UserResponse> getUsers(
|
|
@RequestHeader(value = "Accept-Version", defaultValue = "1.0") String version) {
|
|
|
|
if (version.equals("2.0")) {
|
|
return ResponseEntity.ok(v2UserResponse);
|
|
}
|
|
return ResponseEntity.ok(v1UserResponse);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Media Type Versioning
|
|
```java
|
|
@GetMapping(produces = {
|
|
"application/vnd.company.v1+json",
|
|
"application/vnd.company.v2+json"
|
|
})
|
|
public ResponseEntity<UserResponse> getUsers(
|
|
@RequestHeader("Accept") String accept) {
|
|
|
|
if (accept.contains("v2")) {
|
|
return ResponseEntity.ok(v2UserResponse);
|
|
}
|
|
return ResponseEntity.ok(v1UserResponse);
|
|
}
|
|
```
|
|
|
|
## HATEOAS Implementation
|
|
|
|
### Response with Links
|
|
```java
|
|
@Data
|
|
@NoArgsConstructor
|
|
@AllArgsConstructor
|
|
public class UserResponseWithLinks {
|
|
private Long id;
|
|
private String name;
|
|
private String email;
|
|
private Map<String, String> _links;
|
|
|
|
// Lombok generates constructors/getters/setters
|
|
}
|
|
|
|
@GetMapping("/{id}")
|
|
public ResponseEntity<UserResponseWithLinks> getUserWithLinks(@PathVariable Long id) {
|
|
UserResponse user = userService.findById(id);
|
|
|
|
Map<String, String> links = Map.of(
|
|
"self", "/api/users/" + id,
|
|
"all", "/api/users",
|
|
"update", "/api/users/" + id,
|
|
"delete", "/api/users/" + id
|
|
);
|
|
|
|
UserResponseWithLinks response = new UserResponseWithLinks(
|
|
user.getId(), user.getName(), user.getEmail(), links);
|
|
|
|
return ResponseEntity.ok(response);
|
|
}
|
|
```
|
|
|
|
### Advanced HATEOAS with Spring HATEOAS
|
|
```java
|
|
@RestController
|
|
@RequestMapping("/api/users")
|
|
@RequiredArgsConstructor
|
|
public class UserController {
|
|
|
|
private final UserService userService;
|
|
private final EntityLinks entityLinks;
|
|
|
|
@GetMapping
|
|
public ResponseEntity<CollectionModel<UserResponse>> getAllUsers() {
|
|
List<UserResponse> users = userService.findAll().stream()
|
|
.map(this::toResponse)
|
|
.collect(Collectors.toList());
|
|
|
|
CollectionModel<UserResponse> resource = CollectionModel.of(users);
|
|
resource.add(entityLinks.linkToCollectionResource(UserController.class).withSelfRel());
|
|
resource.add(entityLinks.linkToCollectionResource(UserController.class).withRel("users"));
|
|
|
|
return ResponseEntity.ok(resource);
|
|
}
|
|
|
|
@GetMapping("/{id}")
|
|
public ResponseEntity<EntityModel<UserResponse>> getUserById(@PathVariable Long id) {
|
|
UserResponse user = userService.findById(id);
|
|
|
|
EntityModel<UserResponse> resource = EntityModel.of(user);
|
|
resource.add(entityLinks.linkToItemResource(UserController.class, id).withSelfRel());
|
|
resource.add(entityLinks.linkToCollectionResource(UserController.class).withRel("users"));
|
|
resource.add(linkTo(methodOn(UserController.class).getUserOrders(id)).withRel("orders"));
|
|
|
|
return ResponseEntity.ok(resource);
|
|
}
|
|
|
|
private UserResponse toResponse(User user) {
|
|
return new UserResponse(
|
|
user.getId(),
|
|
user.getName(),
|
|
user.getEmail(),
|
|
user.getActive(),
|
|
user.getCreatedAt()
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
## Async Processing
|
|
|
|
### Asynchronous Controller
|
|
```java
|
|
@RestController
|
|
@RequestMapping("/api/users")
|
|
@RequiredArgsConstructor
|
|
public class AsyncUserController {
|
|
|
|
private final AsyncUserService asyncUserService;
|
|
|
|
@GetMapping("/{id}")
|
|
public CompletableFuture<ResponseEntity<UserResponse>> getUserById(@PathVariable Long id) {
|
|
return asyncUserService.getUserById(id)
|
|
.thenApply(ResponseEntity::ok)
|
|
.exceptionally(ex -> ResponseEntity.notFound().build());
|
|
}
|
|
|
|
@PostMapping
|
|
public CompletableFuture<ResponseEntity<UserResponse>> createUser(
|
|
@Valid @RequestBody CreateUserRequest request) {
|
|
return asyncUserService.createUser(request)
|
|
.thenApply(created ->
|
|
ResponseEntity.status(HttpStatus.CREATED)
|
|
.header("Location", "/api/users/" + created.getId())
|
|
.body(created))
|
|
.exceptionally(ex -> {
|
|
if (ex.getCause() instanceof BusinessException) {
|
|
return ResponseEntity.badRequest().build();
|
|
}
|
|
return ResponseEntity.internalServerError().build();
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
### Async Service Implementation
|
|
```java
|
|
@Service
|
|
@RequiredArgsConstructor
|
|
public class AsyncUserService {
|
|
|
|
private final UserService userService;
|
|
private final ExecutorService executor;
|
|
|
|
@Async
|
|
public CompletableFuture<UserResponse> getUserById(Long id) {
|
|
return CompletableFuture.supplyAsync(() -> userService.findById(id), executor);
|
|
}
|
|
|
|
@Async
|
|
public CompletableFuture<UserResponse> createUser(CreateUserRequest request) {
|
|
return CompletableFuture.supplyAsync(() -> userService.create(request), executor);
|
|
}
|
|
}
|
|
```
|
|
|
|
## File Upload and Download
|
|
|
|
### File Upload Controller
|
|
```java
|
|
@RestController
|
|
@RequestMapping("/api/files")
|
|
@RequiredArgsConstructor
|
|
public class FileController {
|
|
|
|
private final FileStorageService fileStorageService;
|
|
|
|
@PostMapping("/upload")
|
|
public ResponseEntity<FileUploadResponse> uploadFile(@RequestParam("file") MultipartFile file) {
|
|
if (file.isEmpty()) {
|
|
throw new BusinessException("File is empty");
|
|
}
|
|
|
|
String fileName = fileStorageService.storeFile(file);
|
|
String fileDownloadUri = ServletUriComponentsBuilder.fromCurrentContextPath()
|
|
.path("/api/files/download/")
|
|
.path(fileName)
|
|
.toUriString();
|
|
|
|
FileUploadResponse response = new FileUploadResponse(
|
|
fileName, fileDownloadUri, file.getContentType(), file.getSize());
|
|
|
|
return ResponseEntity.ok(response);
|
|
}
|
|
|
|
@GetMapping("/download/{fileName:.+}")
|
|
public ResponseEntity<Resource> downloadFile(@PathVariable String fileName) {
|
|
Resource resource = fileStorageService.loadFileAsResource(fileName);
|
|
|
|
return ResponseEntity.ok()
|
|
.contentType(MediaType.APPLICATION_OCTET_STREAM)
|
|
.header(HttpHeaders.CONTENT_DISPOSITION,
|
|
"attachment; filename=\"" + resource.getFilename() + "\"")
|
|
.body(resource);
|
|
}
|
|
}
|
|
```
|
|
|
|
### File Storage Service
|
|
```java
|
|
@Service
|
|
public class FileStorageService {
|
|
|
|
private final Path fileStorageLocation;
|
|
|
|
@Autowired
|
|
public FileStorageService(FileStorageProperties fileStorageProperties) {
|
|
this.fileStorageLocation = Paths.get(fileStorageProperties.getUploadDir())
|
|
.toAbsolutePath().normalize();
|
|
|
|
try {
|
|
Files.createDirectories(this.fileStorageLocation);
|
|
} catch (Exception ex) {
|
|
throw new FileStorageException("Could not create the directory where the uploaded files will be stored.", ex);
|
|
}
|
|
}
|
|
|
|
public String storeFile(MultipartFile file) {
|
|
String fileName = StringUtils.cleanPath(Objects.requireNonNull(file.getOriginalFilename()));
|
|
|
|
try {
|
|
if (fileName.contains("..")) {
|
|
throw new FileStorageException("Sorry! Filename contains invalid path sequence " + fileName);
|
|
}
|
|
|
|
Path targetLocation = this.fileStorageLocation.resolve(fileName);
|
|
Files.copy(file.getInputStream(), targetLocation, StandardCopyOption.REPLACE_EXISTING);
|
|
|
|
return fileName;
|
|
} catch (IOException ex) {
|
|
throw new FileStorageException("Could not store file " + fileName + ". Please try again!", ex);
|
|
}
|
|
}
|
|
|
|
public Resource loadFileAsResource(String fileName) {
|
|
try {
|
|
Path filePath = this.fileStorageLocation.resolve(fileName).normalize();
|
|
Resource resource = new UrlResource(filePath);
|
|
|
|
if (resource.exists() && resource.isReadable()) {
|
|
return resource;
|
|
} else {
|
|
throw new FileNotFoundException("File not found " + fileName);
|
|
}
|
|
} catch (Exception ex) {
|
|
throw new FileNotFoundException("File not found " + fileName, ex);
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## WebSocket Integration
|
|
|
|
### WebSocket Configuration
|
|
```java
|
|
@Configuration
|
|
@EnableWebSocketMessageBroker
|
|
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
|
|
|
@Override
|
|
public void configureMessageBroker(MessageBrokerRegistry config) {
|
|
config.enableSimpleBroker("/topic");
|
|
config.setApplicationDestinationPrefixes("/app");
|
|
}
|
|
|
|
@Override
|
|
public void registerStompEndpoints(StompEndpointRegistry registry) {
|
|
registry.addEndpoint("/ws")
|
|
.setAllowedOriginPatterns("*")
|
|
.withSockJS();
|
|
}
|
|
}
|
|
```
|
|
|
|
### WebSocket Controller
|
|
```java
|
|
@Controller
|
|
@RequiredArgsConstructor
|
|
public class WebSocketController {
|
|
|
|
private final SimpMessagingTemplate messagingTemplate;
|
|
|
|
@MessageMapping("/chat.sendMessage")
|
|
@SendTo("/topic/public")
|
|
public ChatMessage sendMessage(@Payload ChatMessage chatMessage) {
|
|
return chatMessage;
|
|
}
|
|
|
|
@MessageMapping("/chat.addUser")
|
|
@SendTo("/topic/public")
|
|
public ChatMessage addUser(@Payload ChatMessage chatMessage,
|
|
SimpMessageHeaderAccessor headerAccessor) {
|
|
headerAccessor.getSessionAttributes().put("username", chatMessage.getSender());
|
|
return chatMessage;
|
|
}
|
|
|
|
@Scheduled(fixedRate = 5000)
|
|
public void sendPeriodicUpdates() {
|
|
messagingTemplate.convertAndSend("/topic/updates",
|
|
new UpdateMessage("System update", LocalDateTime.now()));
|
|
}
|
|
}
|
|
```
|
|
|
|
### Frontend Integration Example
|
|
```javascript
|
|
// JavaScript WebSocket client
|
|
class WebSocketClient {
|
|
constructor(url) {
|
|
this.url = url;
|
|
this.stompClient = null;
|
|
this.connected = false;
|
|
}
|
|
|
|
connect() {
|
|
const socket = new SockJS(this.url);
|
|
this.stompClient = Stomp.over(socket);
|
|
|
|
this.stompClient.connect({}, (frame) => {
|
|
this.connected = true;
|
|
console.log('Connected: ' + frame);
|
|
|
|
// Subscribe to topics
|
|
this.stompClient.subscribe('/topic/public', (message) => {
|
|
this.onMessage(message);
|
|
});
|
|
|
|
this.stompClient.subscribe('/topic/updates', (update) => {
|
|
this.onUpdate(update);
|
|
});
|
|
}, (error) => {
|
|
this.connected = false;
|
|
console.error('Error: ' + error);
|
|
});
|
|
}
|
|
|
|
sendMessage(message) {
|
|
if (this.connected) {
|
|
this.stompClient.send("/app/chat.sendMessage", {}, JSON.stringify(message));
|
|
}
|
|
}
|
|
|
|
onMessage(message) {
|
|
const chatMessage = JSON.parse(message.body);
|
|
console.log('Received message:', chatMessage);
|
|
// Display message in UI
|
|
}
|
|
|
|
onUpdate(update) {
|
|
const updateMessage = JSON.parse(update.body);
|
|
console.log('Received update:', updateMessage);
|
|
// Update UI with system messages
|
|
}
|
|
}
|
|
``` |