Learning Paths
Last Updated: May 30, 2026 at 19:00
Micronaut REST APIs Explained for Spring Boot Developers
Controllers, Validation, Filters, and Testing with Side-by-Side Comparisons
Learn how Spring MVC concepts map to Micronaut's compile-time HTTP framework through practical REST examples. By the end of this guide, you'll understand how to build REST APIs in Micronaut with minimal friction — and why the shift from runtime reflection to compile-time processing changes things in ways that matter.

The mental model shift: compile-time over reflection
The single most important thing to understand about Micronaut before writing any code is this: Micronaut moves the heavy lifting Spring does at runtime to compile time using annotation processing.
Spring Boot relies heavily on reflection and classpath scanning to wire beans, resolve routes, and handle serialisation. Micronaut does all of this at build time instead. Route tables are compiled from your controller annotations before the application ever runs, which means faster startup and no reflection overhead during request dispatch.
This compile-time model also explains why you'll encounter annotations like @Introspected and @Serdeable that have no Spring equivalent — they are instructions to the annotation processor to generate the metadata it needs at build time. Forgetting them is the most common early mistake for Spring developers, and understanding why they exist makes them much easier to remember.
The HTTP layer itself is built on Netty — a non-blocking, event-loop-based server — but for the REST programming model covered here, you won't need to think about that unless you start writing filters (more on that later).
Bean scopes: a quick orientation
Spring developers are used to @Service, @Component, and @Repository. In Micronaut, beans are explicitly scoped with annotations like @Singleton and @Prototype. There is no component scanning that picks up arbitrary classes — you declare the scope you want.
@Controller implies @Singleton automatically, so you don't need to annotate controllers separately.
Your first controller
@Controller + @Get / @Post / @Put / @Delete
A few things to note:
- @Controller is both the route prefix and the bean declaration — no separate @Singleton needed.
- The return value is serialised to JSON automatically using Micronaut Serialization, which is compile-time generated (not Jackson by default). More on this in the serialisation section.
- Path variables are bound by argument name — {id} binds to Long id automatically. You can also use @PathVariable explicitly, which is worth doing early on if you're coming from Spring, as it makes the mapping more visible.
Spring comparison: Spring uses @RestController (which combines @Controller + @ResponseBody) and separate @GetMapping, @PostMapping, etc. Micronaut uses @Controller with @Get, @Post, @Put, @Delete, and @Patch. All controller methods return JSON by default — there is no @ResponseBody equivalent.
Route binding
Path variables, query parameters, request body, and headers
The Spring-to-Micronaut annotation mapping for route binding:
- @PathVariable → parameter name matches {variable} automatically, or use @PathVariable explicitly
- @RequestParam → @QueryValue
- @RequestBody → @Body
- @RequestHeader → @Header
- @CookieValue → @CookieValue (same)
Response status codes
Explicit status codes — no REST semantic defaults
Micronaut does not apply REST semantic defaults. Every HTTP method defaults to 200 OK, including POST. You must set status codes explicitly — this is a deliberate design decision, not an oversight.
Use @Status to annotate the method:
Spring comparison: @Status replaces @ResponseStatus.
HttpResponse<T> for dynamic status codes
When the status code depends on the result of the operation, return HttpResponse<T> — the Micronaut equivalent of Spring's ResponseEntity<T>:
HttpResponse has static factory methods for all common status codes: ok(), created(), noContent(), notFound(), badRequest(), unauthorized(), and serverError().
Input validation
@Valid + Bean Validation — works the same as Spring, with one extra annotation
Add the micronaut-validation dependency, then annotate your request classes with standard JSR-380 constraints. The key difference: you must annotate your DTO with @Introspected (and/or @Serdeable) so Micronaut can generate the compile-time metadata it needs for validation and binding.
Trigger validation by annotating the controller parameter with @Valid:
When validation fails, Micronaut automatically returns a 400 Bad Request with a structured JSON body. The default response looks like this:
Spring comparison: Spring does not require @Introspected. In Micronaut, @Introspected tells the annotation processor to generate the class metadata needed at compile time. Forgetting it is one of the most common early mistakes — you will get a runtime error telling you the class is not introspected.
Validating path variables and query parameters
Add @Validated at the class level to enable constraint annotations directly on method parameters:
Exception handling
@Error — local and global handlers
Micronaut uses @Error to handle exceptions, either locally (within one controller) or globally (across the whole application).
Local error handler — handles exceptions thrown by this controller only:
Global error handler — handles exceptions across the entire application:
Spring comparison: Spring uses @ControllerAdvice + @ExceptionHandler to define global handlers in a separate class. Micronaut has no @ControllerAdvice equivalent — any @Controller can declare global handlers by setting global = true on @Error.
A reusable error response class
Handling validation errors globally
Micronaut fires a ConstraintViolationException when @Valid fails. You can customise the response by handling it explicitly:
Filters
@Filter — the reactive execution model
Filters run before and after every matching request and are useful for logging, auth token extraction, adding response headers, or measuring latency.
@Filter takes an Ant-style path pattern. Multiple filters are ordered using @Order.
Spring comparison: Spring filters implement Filter (servlet) or HandlerInterceptor. Micronaut filters use a reactive execution model internally — they implement HttpServerFilter and return a reactive Publisher. This is true even when your application code is otherwise imperative. Mono.from(chain.proceed(request)) is the standard wrapper you'll use every time.
Content negotiation and serialisation
JSON is the default — @Serdeable makes it compile-time safe
Micronaut serialises return values to JSON automatically using Micronaut Serialization, a compile-time library. Mark your DTOs with @Serdeable so the annotation processor generates serialisers at build time:
Spring comparison: Spring uses Jackson at runtime with no extra annotations needed on DTOs. In Micronaut, @Serdeable tells the compile-time processor to generate serialisation code for this class. Like @Introspected for validation, forgetting @Serdeable on a DTO is a common early mistake.
Jackson is also supported as a drop-in alternative — just swap the serialisation dependency. Existing Jackson annotations (@JsonProperty, @JsonIgnore, etc.) continue to work.
Custom content types
For non-JSON content types, use produces and consumes on the route:
Unlike Spring Boot, where content negotiation is largely implicit, Micronaut is more explicit — you declare what your endpoint produces and consumes directly on the annotation.
Testing
@MicronautTest — real HTTP against an embedded server
Micronaut's test support starts the full embedded HTTP server. Tests make real HTTP calls against a live instance of the application. The server starts in under a second, making this practical even in CI pipelines.
Add the test dependency (build.gradle):
Testing a controller end-to-end:
toBlocking() turns the reactive client into a synchronous one for tests — you don't need reactive test code unless you want it.
By default, @MicronautTest starts an embedded HTTP server. You can also configure context-only tests when you don't need HTTP, using @MicronautTest with startEmbeddedServer = false.
Spring comparison: Spring Boot uses @SpringBootTest + MockMvc (mock servlet layer) or TestRestTemplate (real HTTP). Micronaut always uses real HTTP against an embedded server — there is no mock servlet layer. The @Client injection replaces TestRestTemplate.
Using a typed declarative client in tests
Micronaut can generate a fully functional HTTP client from a plain Java interface annotated with the same @Get, @Post, etc. annotations used on controllers. You define the interface, and Micronaut implements it at compile time — no boilerplate, no manual request construction.
Instead of constructing HttpRequest objects manually, declare a typed client interface that mirrors your controller:
Inject and use it in tests:
This is much cleaner than raw HttpClient calls and gives you compile-time checking on the contract.
Testing error responses
Replacing beans in tests
A complete worked example
Putting everything together — a ProductController with full CRUD, validation, error handling, and tests:
Common pitfalls for Spring developers
These are the mistakes that catch almost everyone in the first week with Micronaut:
Forgetting @Serdeable on DTOs. If your response objects aren't annotated, serialisation will fail at runtime. Every DTO that goes in or out of a controller needs @Serdeable.
Forgetting @Introspected on validated classes. Validation requires compile-time metadata. If you see an error saying a class is not introspected, this is why.
Expecting 201 on POST. Micronaut returns 200 for everything by default. Always add @Status(HttpStatus.CREATED) to POST endpoints explicitly.
Expecting reflection-based behaviour. If something works in Spring without any special annotation, it's probably relying on runtime reflection. In Micronaut, you need to tell the annotation processor explicitly what to generate metadata for.
Writing non-reactive filters. Filters in Micronaut always use a reactive execution model. Even if the rest of your application is imperative, filter code uses Publisher and Mono.from(chain.proceed(request)).
Spring → Micronaut quick reference
Controller declaration: @RestController → @Controller
Route methods: @GetMapping, @PostMapping, etc. → @Get, @Post, @Put, @Delete, @Patch
Path variable: @PathVariable → implicit from {name} in route (or explicit @PathVariable)
Query parameter: @RequestParam → @QueryValue
Request body: @RequestBody → @Body
Request header: @RequestHeader → @Header
Response status: @ResponseStatus → @Status
Dynamic response: ResponseEntity<T> → HttpResponse<T>
Input validation: @Valid + @Validated → same
DTO annotation: none needed → @Introspected + @Serdeable
Global exception handler: @ControllerAdvice + @ExceptionHandler → @Controller + @Error(global = true)
Request filter: OncePerRequestFilter → @Filter implementing HttpServerFilter
Test annotation: @SpringBootTest → @MicronautTest
Test HTTP client: TestRestTemplate / MockMvc → injected @Client
Mock bean in test: @MockBean (Spring Boot) → @MockBean (Micronaut Test)
About N Sharma
Lead Architect at StackAndSystemN Sharma is a technologist with over 28 years of experience in software engineering, system architecture, and technology consulting. He holds a Bachelor’s degree in Engineering, a DBF, and an MBA. His work focuses on research-driven technology education—explaining software architecture, system design, and development practices through structured tutorials designed to help engineers build reliable, scalable systems.
Disclaimer
This article is for educational purposes only. Assistance from AI-powered generative tools was taken to format and improve language flow. While we strive for accuracy, this content may contain errors or omissions and should be independently verified.
