Starter for building WebFlux applications using Spring Framework's Reactive Web support
—
Spring WebFlux supports traditional Spring MVC-style controllers enhanced with reactive return types. This programming model allows developers familiar with Spring MVC to adopt reactive programming while maintaining familiar annotation-based patterns.
@RestController
public @interface RestController {
@AliasFor(annotation = Controller.class)
String value() default "";
}
@Controller
@Component
public @interface Controller {
String value() default "";
}@RequestMapping
public @interface RequestMapping {
String name() default "";
String[] value() default {};
String[] path() default {};
RequestMethod[] method() default {};
String[] params() default {};
String[] headers() default {};
String[] consumes() default {};
String[] produces() default {};
}
@GetMapping
public @interface GetMapping {
String name() default "";
String[] value() default {};
String[] path() default {};
String[] params() default {};
String[] headers() default {};
String[] consumes() default {};
String[] produces() default {};
}
@PostMapping
public @interface PostMapping { /* same structure as GetMapping */ }
@PutMapping
public @interface PutMapping { /* same structure as GetMapping */ }
@DeleteMapping
public @interface DeleteMapping { /* same structure as GetMapping */ }
@PatchMapping
public @interface PatchMapping { /* same structure as GetMapping */ }@PathVariable
public @interface PathVariable {
@AliasFor("name")
String value() default "";
String name() default "";
boolean required() default true;
}@RequestParam
public @interface RequestParam {
@AliasFor("name")
String value() default "";
String name() default "";
boolean required() default true;
String defaultValue() default ValueConstants.DEFAULT_NONE;
}@RequestBody
public @interface RequestBody {
boolean required() default true;
}@RequestHeader
public @interface RequestHeader {
@AliasFor("name")
String value() default "";
String name() default "";
boolean required() default true;
String defaultValue() default ValueConstants.DEFAULT_NONE;
}@CookieValue
public @interface CookieValue {
@AliasFor("name")
String value() default "";
String name() default "";
boolean required() default true;
String defaultValue() default ValueConstants.DEFAULT_NONE;
}@ResponseBody
public @interface ResponseBody {
}
@ResponseStatus
public @interface ResponseStatus {
HttpStatus code() default HttpStatus.INTERNAL_SERVER_ERROR;
@AliasFor("code")
HttpStatus value() default HttpStatus.INTERNAL_SERVER_ERROR;
String reason() default "";
}// Reactor Mono for single values
public abstract class Mono<T> implements CorePublisher<T> {
public static <T> Mono<T> just(T data);
public static <T> Mono<T> empty();
public static <T> Mono<T> error(Throwable error);
public static <T> Mono<T> fromCallable(Callable<? extends T> supplier);
public static <T> Mono<T> fromSupplier(Supplier<? extends T> supplier);
public <R> Mono<R> map(Function<? super T, ? extends R> mapper);
public <R> Mono<R> flatMap(Function<? super T, ? extends Mono<? extends R>> transformer);
public Mono<T> filter(Predicate<? super T> tester);
public Mono<T> switchIfEmpty(Mono<? extends T> alternate);
}// Reactor Flux for multiple values
public abstract class Flux<T> implements CorePublisher<T> {
public static <T> Flux<T> just(T... data);
public static <T> Flux<T> empty();
public static <T> Flux<T> error(Throwable error);
public static <T> Flux<T> fromIterable(Iterable<? extends T> it);
public static <T> Flux<T> fromArray(T[] array);
public <R> Flux<R> map(Function<? super T, ? extends R> mapper);
public <R> Flux<R> flatMap(Function<? super T, ? extends Publisher<? extends R>> mapper);
public Flux<T> filter(Predicate<? super T> predicate);
public Flux<T> take(long n);
public Flux<T> skip(long n);
public Mono<List<T>> collectList();
}public class ResponseEntity<T> extends HttpEntity<T> {
public ResponseEntity(HttpStatus status);
public ResponseEntity(T body, HttpStatus status);
public ResponseEntity(MultiValueMap<String, String> headers, HttpStatus status);
public ResponseEntity(T body, MultiValueMap<String, String> headers, HttpStatus status);
public HttpStatus getStatusCode();
public static BodyBuilder status(HttpStatus status);
public static <T> ResponseEntity<T> ok(T body);
public static <T> ResponseEntity<T> created(URI location);
public static <T> ResponseEntity<T> notFound();
public static <T> ResponseEntity<T> badRequest();
}@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/{id}")
public Mono<ResponseEntity<User>> getUser(@PathVariable String id) {
return userService.findById(id)
.map(ResponseEntity::ok)
.defaultIfEmpty(ResponseEntity.notFound().build());
}
@GetMapping
public Flux<User> getAllUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) {
return userService.findAll(page, size);
}
@PostMapping
public Mono<ResponseEntity<User>> createUser(@RequestBody Mono<User> userMono) {
return userMono
.flatMap(userService::save)
.map(user -> ResponseEntity.created(URI.create("/api/users/" + user.getId())).body(user));
}
@PutMapping("/{id}")
public Mono<ResponseEntity<User>> updateUser(
@PathVariable String id,
@RequestBody Mono<User> userMono) {
return userMono
.flatMap(user -> userService.update(id, user))
.map(ResponseEntity::ok)
.defaultIfEmpty(ResponseEntity.notFound().build());
}
@DeleteMapping("/{id}")
public Mono<ResponseEntity<Void>> deleteUser(@PathVariable String id) {
return userService.deleteById(id)
.then(Mono.just(ResponseEntity.noContent().<Void>build()));
}
}@RestController
@RequestMapping("/api/data")
public class DataStreamController {
@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> streamData() {
return Flux.interval(Duration.ofSeconds(1))
.map(i -> "Data point: " + i)
.take(10);
}
@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public Mono<String> uploadFile(@RequestPart("file") Mono<FilePart> fileMono) {
return fileMono
.flatMap(file -> file.transferTo(Paths.get("/tmp/" + file.filename())))
.then(Mono.just("File uploaded successfully"));
}
@GetMapping("/search")
public Flux<SearchResult> search(
@RequestParam String query,
@RequestHeader(name = "Accept-Language", defaultValue = "en") String language) {
return searchService.search(query, language);
}
}@RestController
public class UserController {
@GetMapping("/users/{id}")
public Mono<User> getUser(@PathVariable String id) {
return userService.findById(id)
.switchIfEmpty(Mono.error(new UserNotFoundException("User not found: " + id)));
}
@ExceptionHandler(UserNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public Mono<ErrorResponse> handleUserNotFound(UserNotFoundException ex) {
return Mono.just(new ErrorResponse("USER_NOT_FOUND", ex.getMessage()));
}
@ExceptionHandler(ValidationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Mono<ErrorResponse> handleValidation(ValidationException ex) {
return Mono.just(new ErrorResponse("VALIDATION_ERROR", ex.getMessage()));
}
}@RestController
public class MediaController {
@GetMapping(value = "/xml-data", produces = MediaType.APPLICATION_XML_VALUE)
public Mono<XmlData> getXmlData() {
return dataService.getXmlData();
}
@PostMapping(value = "/json-data",
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
public Mono<JsonResponse> processJsonData(@RequestBody Mono<JsonRequest> request) {
return request.flatMap(dataService::processJson);
}
@GetMapping(value = "/csv-export", produces = "text/csv")
public Flux<String> exportCsv() {
return dataService.getAllRecords()
.map(record -> String.join(",", record.getFields()));
}
}@RestController
@Validated
public class ValidatedController {
@PostMapping("/users")
public Mono<User> createUser(@Valid @RequestBody Mono<User> userMono) {
return userMono.flatMap(userService::save);
}
@GetMapping("/users/{id}")
public Mono<User> getUser(@PathVariable @Pattern(regexp = "^[0-9]+$") String id) {
return userService.findById(id);
}
@GetMapping("/users")
public Flux<User> getUsers(
@RequestParam @Min(0) int page,
@RequestParam @Min(1) @Max(100) int size) {
return userService.findAll(page, size);
}
}Spring WebFlux automatically handles content negotiation based on Accept headers and produces/consumes attributes in mapping annotations.
All controller methods returning reactive types are processed asynchronously, allowing the server to handle thousands of concurrent requests with minimal thread usage.
When using Flux return types, Spring WebFlux automatically applies backpressure, ensuring that slow consumers don't overwhelm the system.
Install with Tessl CLI
npx tessl i tessl/maven-org-springframework-boot--spring-boot-starter-webflux