Serving static resources in a Micronaut Framework application

Learn how to expose static resources such as CSS or images in a Micronaut Framework application.

Authors: Tim Yates

Micronaut Version: 4.4.2

1. Getting Started

In this guide, we will create a Micronaut application written in Java.

2. What you will need

To complete this guide, you will need the following:

3. Solution

We recommend that you follow the instructions in the next sections and create the application step by step. However, you can go right to the completed example.

4. Writing the Application

Create an application using the Micronaut Command Line Interface or with Micronaut Launch.

mn create-app example.micronaut.micronautguide \
    --features=validation,views-thymeleaf \
    --build=gradle \
    --lang=java \
    --test=junit
If you don’t specify the --build argument, Gradle is used as the build tool.
If you don’t specify the --lang argument, Java is used as the language.
If you don’t specify the --test argument, JUnit is used for Java and Kotlin, and Spock is used for Groovy.

The previous command creates a Micronaut application with the default package example.micronaut in a directory named micronautguide.

If you use Micronaut Launch, select Micronaut Application as application type and add validation, and views-thymeleaf features.

If you have an existing Micronaut application and want to add the functionality described here, you can view the dependency and configuration changes from the specified features and apply those changes to your application.

5. Views

To use the Thymeleaf Java template engine to render views in a Micronaut application, add the following dependency on your classpath.

build.gradle
implementation("io.micronaut.views:micronaut-views-thymeleaf")

5.1. Thymeleaf template

Create a Thymeleaf template in src/main/resources/views/index.html:

src/main/resources/views/index.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <title>A hearty welcome!</title>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <link rel="stylesheet" href="/css/style.css"> (1)
    </head>
    <body>
        <section>
            <img width="253" height="200" src="/images/micronaut_stacked_black.png" alt="Micronaut Framework logo"> (2)
            <h1 th:text="${message}"></h1> (3)
        </section>
    </body>
</html>
1 Include the CSS file from our static assets
2 Include a PNG from our static assets
3 Use the th:text attribute to set the element’s text

6. Message service

Create an interface which describes our message service.

src/main/java/example/micronaut/MessageService.java
package example.micronaut;

import io.micronaut.core.annotation.NonNull;
import jakarta.validation.constraints.NotBlank;

interface MessageService {

    String sayHello(@NonNull @NotBlank String name);
}

And then create a default implementation of the service.

src/main/java/example/micronaut/MyMessageService.java
package example.micronaut;

import io.micronaut.core.annotation.NonNull;
import jakarta.inject.Singleton;
import jakarta.validation.constraints.NotBlank;

@Singleton (1)
class MyMessageService implements MessageService {

    @Override
    public String sayHello(@NonNull @NotBlank String name) {
        return "Hello %s!".formatted(name);
    }
}
1 Use jakarta.inject.Singleton to designate a class as a singleton.

7. Controller

Create a controller that takes a name and generates our templated HTML page.

src/main/java/example/micronaut/MainController.java
package example.micronaut;

import io.micronaut.core.annotation.NonNull;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.views.View;
import jakarta.validation.constraints.NotBlank;

import java.util.Map;

import static io.micronaut.http.MediaType.TEXT_HTML;

@Controller (1)
class MainController {

    private final MessageService messageService;

    public MainController(MessageService messageService) { (2)
        this.messageService = messageService;
    }

    @View("index.html") (3)
    @Get(value = "/hello/{name}", produces = TEXT_HTML) (4)
    Map<String, String> index(@NonNull @NotBlank String name) { (5)
        return Map.of("message", messageService.sayHello(name));
    }
}
1 The class is defined as a controller with the @Controller annotation mapped to the path /.
2 Injection for MessageService.
3 Use View annotation to specify which template to use to render the response.
4 The @Get annotation maps the sayHello method to an HTTP GET request on /hello/{name}.
5 Use jakarta.validation.constraints Constraints to ensure the data matches your expectations.

8. Assets

Then create a static CSS file in src/main/resources/static/css/style.css:

src/main/resources/static/css/style.css
html, body {
    margin: 0;
    padding: 0.5em;
    background: #eee;
    font-family: sans-serif;
    color: #333;
}

section {
    display: flex;
    flex-direction: column;
    text-align: center;
    justify-content: left;
    width: fit-content;
}

section h1 {
    margin: 0;
    padding: 0.5em;
    font-size: 1.5em;
    font-variant: small-caps;
}

And a static PNG file in src/main/resources/static/images/micronaut_stacked_black.png:

micronaut stacked black

8.1. Configuration

We can then add a static-mapping to our application.properties so that any missing requests are attempted to be resolved from the static assets.

First configure a route /css/*.css to map any routes matching that pattern to be mapped to the static/css resources.

src/main/resources/application.properties
micronaut.router.static-resources.css.mapping=/css/*.css
micronaut.router.static-resources.css.paths=classpath\:static/css
  • The first line matches any request to the server that does not match a controller route.

  • The second line defines that the classpath inside a root static directory should be searched for the resource.

This means that a request to /css/style.css will be resolved to src/main/resources/static/css/style.css (if that resource exists and there is no matching controller path).

And then configure a route /images/** to be mapped to the static/images resources. We use a wildcard here as images may have different file extensions.

src/main/resources/application.properties
micronaut.router.static-resources.images.mapping=/images/**
micronaut.router.static-resources.images.paths=classpath\:static/images

This means that a request to /images/some/image.png will be resolved to src/main/resources/static/images/some/image.png (if that resource exists and there is no matching controller path).

8.2. A note on wildcard mappings

The framework searches for every request a controller route; if not found, it searches for a static resource. In the above configuration, we could have used a single wildcard mapping for all assets similar to:

A wasteful wildcard example
micronaut.router.static-resources.assets.mapping=/**
micronaut.router.static-resources.assets.paths=classpath\:static

However, searching for a static resource in every request is a waste. We know specific paths (e.g., /bogus) will not match any resource.

Therefore, as we only want to search for static resources for paths starting with /css or /images, we can limit our static resource mappings to these paths.

9. Testing

Create a test that verifies the MessageService is working as expected.

src/test/java/example/micronaut/MessageServiceTest.java
package example.micronaut;

import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import jakarta.inject.Inject;
import jakarta.validation.ConstraintViolationException;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

@MicronautTest(startApplication = false) (1)
class MessageServiceTest {

    @Inject
    MessageService service; (2)

    @Test
    void testItWorks() {
        assertEquals("Hello Tim!", service.sayHello("Tim"));
    }

    @Test
    void testValidationWithNull() {
        ConstraintViolationException exception = assertThrows(ConstraintViolationException.class, () -> service.sayHello(null));
        assertEquals(1, exception.getConstraintViolations().size());
        assertEquals("sayHello.name: must not be blank", exception.getLocalizedMessage());
    }

    @Test
    void testValidationWithBlank() {
        ConstraintViolationException exception = assertThrows(ConstraintViolationException.class, () -> service.sayHello("   "));
        assertEquals(1, exception.getConstraintViolations().size());
        assertEquals("sayHello.name: must not be blank", exception.getMessage());
    }
}
1 Annotate the class with @MicronautTest so the Micronaut framework will initialize the application context and the embedded server. More info.
2 Inject the MessageService bean.

And another that verifies the generated HTML contains the expected text.

src/test/java/example/micronaut/MainControllerTest.java
package example.micronaut;

import io.micronaut.http.client.HttpClient;
import io.micronaut.http.client.annotation.Client;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertTrue;

@MicronautTest (1)
class MainControllerTest {

    @Inject
    @Client("/hello") (2)
    HttpClient client;

    @Test
    void testItWorks() {
        String html = client.toBlocking().retrieve("/Tim", String.class);
        assertTrue(html.contains("""
                <h1>Hello Tim!</h1>"""));
    }
}
1 Annotate the class with @MicronautTest so the Micronaut framework will initialize the application context and the embedded server. More info.
2 Inject the HttpClient bean and point it to the embedded server.

And another that verifies the static resources are returned on the expected URLs.

src/test/java/example/micronaut/StaticResourceTest.java
package example.micronaut;

import io.micronaut.http.client.HttpClient;
import io.micronaut.http.client.annotation.Client;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import org.junit.jupiter.api.Test;

import java.io.IOException;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

@MicronautTest (1)
class StaticResourceTest {

    @Test
    void stylesheetExists(@Client("/css") HttpClient client) { (2)
        String css = client.toBlocking().retrieve("/style.css", String.class);
        assertTrue(css.contains("""
                html, body {"""));
    }

    @Test
    void imageExists(@Client("/images") HttpClient client) throws IOException { (2)
        byte[] image = client.toBlocking().retrieve("/micronaut_stacked_black.png", byte[].class);
        int expectedLength = StaticResourceTest.class
                .getResourceAsStream("/static/images/micronaut_stacked_black.png")
                .readAllBytes().length;
        assertEquals(expectedLength, image.length);
    }
}
1 Annotate the class with @MicronautTest so the Micronaut framework will initialize the application context and the embedded server. More info.
2 Inject the HttpClient bean and point it to the embedded server.

10. Testing the Application

To run the tests:

./gradlew test

Then open build/reports/tests/test/index.html in a browser to see the results.

11. Running the Application

To run the application, use the ./gradlew run command, which starts the application on port 8080.

Visit http://localhost:8080/hello/Micronaut and the browser displays our templated HTML page with styling and an image.

12. Next steps

13. Help with the Micronaut Framework

The Micronaut Foundation sponsored the creation of this Guide. A variety of consulting and support services are available.

14. License

All guides are released with an Apache license 2.0 license for the code and a Creative Commons Attribution 4.0 license for the writing and media (images…​).