Modern, JVM-based framework for building modular, easily testable microservice and serverless applications with compile-time DI and fast startup.
—
Micronaut's HTTP server provides high-performance request handling with both blocking and non-blocking APIs, comprehensive routing, content negotiation, and error handling.
Define HTTP endpoints using controller classes with declarative routing annotations.
/**
* Basic controller with GET endpoint
*/
@Controller("/api/users")
public class UserController {
@Get("/{id}")
public User getUser(Long id) {
return userService.findById(id);
}
@Get
public List<User> getUsers(@QueryValue Optional<String> filter) {
return userService.findAll(filter.orElse(null));
}
@Post
public HttpResponse<User> createUser(@Body @Valid User user) {
User created = userService.save(user);
return HttpResponse.created(created);
}
@Put("/{id}")
public User updateUser(Long id, @Body @Valid User user) {
return userService.update(id, user);
}
@Delete("/{id}")
@Status(HttpStatus.NO_CONTENT)
public void deleteUser(Long id) {
userService.delete(id);
}
}Bind HTTP request data to method parameters with type conversion and validation.
/**
* Path parameters, query parameters, and headers
*/
@Controller("/api/products")
public class ProductController {
@Get("/{id}")
public Product getProduct(@PathVariable Long id,
@QueryValue Optional<Boolean> includeDetails,
@Header("Accept-Language") String language) {
return productService.findById(id, includeDetails.orElse(false), language);
}
@Get
public Page<Product> searchProducts(@QueryValue String q,
@QueryValue @Positive int page,
@QueryValue @Positive int size,
@QueryValue Optional<String> sort) {
return productService.search(q, page, size, sort.orElse("name"));
}
@Post("/bulk")
public List<Product> createProducts(@Body List<@Valid Product> products,
@Header("X-Batch-Id") String batchId) {
return productService.createBatch(products, batchId);
}
}
/**
* Form data and multipart handling
*/
@Controller("/upload")
public class FileUploadController {
@Post(value = "/", consumes = MediaType.MULTIPART_FORM_DATA)
public HttpResponse<String> upload(@Part CompletedFileUpload file,
@Part("description") String description) {
String filename = fileService.store(file, description);
return HttpResponse.ok("File uploaded: " + filename);
}
@Post(value = "/form", consumes = MediaType.APPLICATION_FORM_URLENCODED)
public HttpResponse<String> handleForm(@Body Map<String, String> formData) {
return HttpResponse.ok("Form processed");
}
}Handle different content types for request and response bodies.
/**
* Content type handling
*/
@Controller("/api/data")
public class DataController {
@Get(value = "/export", produces = {
MediaType.APPLICATION_JSON,
MediaType.APPLICATION_XML,
"text/csv"
})
public Object exportData(@Header("Accept") String acceptHeader) {
if (acceptHeader.contains("xml")) {
return new XmlDataResponse();
} else if (acceptHeader.contains("csv")) {
return new CsvDataResponse();
}
return new JsonDataResponse();
}
@Post(value = "/import", consumes = {
MediaType.APPLICATION_JSON,
MediaType.APPLICATION_XML
})
public HttpResponse<ImportResult> importData(@Body Object data) {
ImportResult result = dataService.importData(data);
return HttpResponse.ok(result);
}
}Handle requests reactively using reactive types for non-blocking I/O.
/**
* Reactive endpoints with Single, Maybe, and Flowable
*/
@Controller("/api/async")
public class AsyncController {
@Get("/user/{id}")
public Single<User> getUserAsync(Long id) {
return userService.findByIdAsync(id);
}
@Get("/users")
public Flowable<User> streamUsers(@QueryValue Optional<String> filter) {
return userService.streamAll(filter.orElse(null));
}
@Post("/user")
public Single<HttpResponse<User>> createUserAsync(@Body @Valid User user) {
return userService.saveAsync(user)
.map(HttpResponse::created);
}
@Get(value = "/events", produces = MediaType.TEXT_EVENT_STREAM)
public Publisher<Event> streamEvents() {
return eventService.streamEvents();
}
}
/**
* CompletableFuture support
*/
@Controller("/api/future")
public class FutureController {
@Get("/data/{id}")
public CompletableFuture<Data> getDataAsync(Long id) {
return dataService.fetchAsync(id);
}
}Handle errors and exceptions with custom error responses.
/**
* Exception handlers
*/
@Singleton
public class GlobalExceptionHandler implements ExceptionHandler<Exception, HttpResponse<?>> {
@Override
public HttpResponse<?> handle(HttpRequest request, Exception exception) {
return HttpResponse.serverError()
.body(Map.of("error", exception.getMessage()));
}
}
/**
* Specific exception handlers
*/
@Singleton
public class ValidationExceptionHandler
implements ExceptionHandler<ConstraintViolationException, HttpResponse<?>> {
@Override
public HttpResponse<?> handle(HttpRequest request,
ConstraintViolationException exception) {
List<String> errors = exception.getConstraintViolations()
.stream()
.map(ConstraintViolation::getMessage)
.collect(Collectors.toList());
return HttpResponse.badRequest()
.body(Map.of("errors", errors));
}
}
/**
* Controller-level error handling
*/
@Controller("/api/orders")
public class OrderController {
@Get("/{id}")
public Order getOrder(Long id) {
return orderService.findById(id);
}
@Error(exception = OrderNotFoundException.class)
public HttpResponse<Map<String, String>> handleOrderNotFound(OrderNotFoundException ex) {
return HttpResponse.notFound(Map.of("error", ex.getMessage()));
}
@Error(status = HttpStatus.BAD_REQUEST)
public HttpResponse<Map<String, String>> handleBadRequest() {
return HttpResponse.badRequest(Map.of("error", "Invalid request"));
}
}Implement request/response filtering for cross-cutting concerns.
/**
* HTTP filters for cross-cutting concerns
*/
@Filter("/api/**")
public class AuthenticationFilter implements HttpServerFilter {
@Override
public Publisher<MutableHttpResponse<?>> doFilter(HttpRequest<?> request,
ServerFilterChain chain) {
String authHeader = request.getHeaders().get("Authorization");
if (authHeader == null || !isValidToken(authHeader)) {
MutableHttpResponse<?> response = HttpResponse.unauthorized();
return Publishers.just(response);
}
return chain.proceed(request);
}
private boolean isValidToken(String token) {
// Token validation logic
return true;
}
}
/**
* CORS filter
*/
@Filter("/**")
public class CorsFilter implements HttpServerFilter {
@Override
public Publisher<MutableHttpResponse<?>> doFilter(HttpRequest<?> request,
ServerFilterChain chain) {
if (request.getMethod() == HttpMethod.OPTIONS) {
MutableHttpResponse<?> response = HttpResponse.ok()
.header("Access-Control-Allow-Origin", "*")
.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE")
.header("Access-Control-Allow-Headers", "Content-Type, Authorization");
return Publishers.just(response);
}
return chain.proceed(request)
.map(response -> response.header("Access-Control-Allow-Origin", "*"));
}
}Configure the HTTP server with custom settings and SSL support.
/**
* Server configuration properties
*/
@ConfigurationProperties("micronaut.server")
public class ServerConfiguration {
private int port = 8080;
private String host = "localhost";
private Duration readTimeout = Duration.ofSeconds(30);
private Duration writeTimeout = Duration.ofSeconds(30);
private int maxRequestSize = 1024 * 1024 * 10; // 10MB
// getters and setters
}
/**
* SSL configuration
*/
@ConfigurationProperties("micronaut.server.ssl")
public class SslConfiguration {
private boolean enabled = false;
private String keyStore;
private String keyStorePassword;
private String keyStoreType = "JKS";
private String trustStore;
private String trustStorePassword;
// getters and setters
}
/**
* Custom server configuration
*/
@Factory
public class ServerFactory {
@Bean
@Replaces(NettyHttpServerConfiguration.class)
public NettyHttpServerConfiguration customServerConfig() {
return new NettyHttpServerConfiguration() {
@Override
public int getMaxHeaderSize() {
return 16384; // 16KB headers
}
};
}
}// Core HTTP types
public interface HttpRequest<B> extends HttpMessage<B> {
HttpMethod getMethod();
URI getUri();
String getPath();
HttpParameters getParameters();
Map<String, Object> getAttributes();
Optional<B> getBody();
<T> Optional<T> getBody(Class<T> type);
}
public interface HttpResponse<B> extends HttpMessage<B> {
HttpStatus getStatus();
int code();
String reason();
static <T> MutableHttpResponse<T> ok();
static <T> MutableHttpResponse<T> created(T body);
static <T> MutableHttpResponse<T> badRequest();
static <T> MutableHttpResponse<T> notFound();
}
public interface MutableHttpResponse<B> extends HttpResponse<B>, MutableHttpMessage<B> {
MutableHttpResponse<B> status(HttpStatus status);
MutableHttpResponse<B> header(CharSequence name, CharSequence value);
MutableHttpResponse<B> body(B body);
}
// Filter interfaces
public interface HttpServerFilter extends ServerFilter {
Publisher<MutableHttpResponse<?>> doFilter(HttpRequest<?> request,
ServerFilterChain chain);
}
public interface ServerFilterChain {
Publisher<MutableHttpResponse<?>> proceed(HttpRequest<?> request);
}
// Exception handling
public interface ExceptionHandler<T extends Throwable, R> {
R handle(HttpRequest request, T exception);
}
// Route matching
public interface Router {
<T, R> Stream<UriRouteMatch<T, R>> find(HttpMethod httpMethod,
CharSequence uri,
HttpRequest<?> context);
<T, R> Optional<UriRouteMatch<T, R>> route(HttpMethod httpMethod,
CharSequence uri);
}
public interface RouteMatch<R> {
R execute(Map<String, Object> argumentValues);
Collection<Argument> getRequiredArguments();
boolean isExecutable();
}
// File upload
public interface CompletedFileUpload extends FileUpload {
byte[] getBytes();
InputStream getInputStream();
void moveTo(File destinationFile);
Optional<MediaType> getContentType();
}Install with Tessl CLI
npx tessl i tessl/maven-micronaut