# 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 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> 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 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 getUserById(@PathVariable Long id) { return ResponseEntity.ok(userService.findById(id)); } @PostMapping public ResponseEntity 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 updateUser( @PathVariable Long id, @Valid @RequestBody UpdateUserRequest request) { UserResponse updated = userService.update(id, request); return ResponseEntity.ok(updated); } @PatchMapping("/{id}") public ResponseEntity patchUser( @PathVariable Long id, @Valid @RequestBody UpdateUserRequest request) { UserResponse updated = userService.update(id, request); return ResponseEntity.ok(updated); } @DeleteMapping("/{id}") public ResponseEntity 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 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 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 _links; // Lombok generates constructors/getters/setters } @GetMapping("/{id}") public ResponseEntity getUserWithLinks(@PathVariable Long id) { UserResponse user = userService.findById(id); Map 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> getAllUsers() { List users = userService.findAll().stream() .map(this::toResponse) .collect(Collectors.toList()); CollectionModel 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> getUserById(@PathVariable Long id) { UserResponse user = userService.findById(id); EntityModel 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> getUserById(@PathVariable Long id) { return asyncUserService.getUserById(id) .thenApply(ResponseEntity::ok) .exceptionally(ex -> ResponseEntity.notFound().build()); } @PostMapping public CompletableFuture> 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 getUserById(Long id) { return CompletableFuture.supplyAsync(() -> userService.findById(id), executor); } @Async public CompletableFuture 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 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 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 } } ```