pyronaut create example.micronaut.micronautguide --features=views-thymeleaf,http-client
Server-side HTML with Thymeleaf
Learn how to render an HTML page with Thymeleaf and Micronaut Views
1. Getting Started
In this guide, we will create a Python application built with Pyronaut.
2. What you will need
To complete this guide, you will need the following:
-
Some time on your hands
-
GraalPy installed and the Pyronaut CLI available locally
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.
-
Download and unzip the source
4. Writing the Application
Create an application using the Pyronaut CLI (Command Line Interface) or Pyronaut Launch
5. Views
To use the Thymeleaf Java template engine to render views in a Pyronaut application, add the following dependency on your classpath.
5.1. API Response
For this guide, we will use a publicly available REST API jsonplaceholder.typicode.com as a 3rd party Photo API.
Create a Photo type to map the API response:
from dataclasses import dataclass
from micronaut.serde.annotation import Serdeable
@Serdeable (1)
@dataclass
class Photo:
albumId: int
title: str
url: str
thumbnailUrl: str
| 1 | Declare the @Serdeable annotation at the type level in your source code to allow the type to be serialized or deserialized. |
Thymeleaf accesses Photo via reflection. By adding the @ReflectiveAccess annotation, the application enables reflective access to the Photo type.
5.2. HTTP Client
from abc import ABC, abstractmethod
from typing import Annotated
from micronaut.http.annotation import Get, PathVariable
from micronaut.http.client.annotation import Client
from .photo import Photo
@Client(id="photos") (1)
class PhotosClient(ABC):
@Get("/photos/{id}") (2)
@abstractmethod
def find_by_id(self, id: Annotated[int, PathVariable]) -> Photo: (3)
...
| 1 | Use @Client to use declarative HTTP Clients. You can annotate interfaces or abstract classes. You can use the id member to provide a service identifier or specify the URL directly as the annotation’s value. |
| 2 | The @Get annotation maps the findById method to an HTTP GET request on /photos/{id}. |
| 3 | You can define path variables with a RFC-6570 URI template in the HTTP Method annotation value. The method argument can optionally be annotated with @PathVariable. |
5.3. HTTP Client Configuration
We used photos as the HTTP Client identifier.
Configure the micronaut.http.services.photos.url to point the client identifier to jsonplaceholder.typicode.com
[micronaut.http.services.photos]
url = "https://jsonplaceholder.typicode.com"
5.4. Controller
Create a controller, which invokes the declarative HTTP client:
from typing import Annotated
from micronaut.http import MediaType
from micronaut.http.annotation import Controller, Get, PathVariable
from micronaut.scheduling import TaskExecutors
from micronaut.scheduling.annotation import ExecuteOn
from micronaut.views import View
from .photo import Photo
from .photos_client import PhotosClient
@Controller("/photos") (1)
class PhotosController:
def __init__(self, photos_client: PhotosClient): (2)
self.photos_client = photos_client
@View("photos/show.html") (5)
@Get(value="/{id}", produces=MediaType.TEXT_HTML) (3) (6)
@ExecuteOn(TaskExecutors.BLOCKING) (4)
def find_by_id(self, id: Annotated[int, PathVariable]) -> dict[str, Photo]: (7)
return {"photo": self.photos_client.find_by_id(id)}
| 1 | The class is defined as a controller with the @Controller annotation mapped to the path /photos. |
| 2 | Use constructor injection to inject a bean of type PhotosClient. |
| 3 | Set the response content-type to HTML with the @Produces annotation. |
| 4 | It is critical that any blocking I/O operations (such as fetching the data from the database) are offloaded to a separate thread pool that does not block the Event loop. |
| 5 | Use View annotation to specify which template to use to render the response. |
| 6 | The @Get annotation maps the findById method to an HTTP GET request on /photos/{id}. |
| 7 | You can define path variables with a RFC-6570 URI template in the HTTP Method annotation value. The method argument can optionally be annotated with @PathVariable. |
5.5. Thymeleaf template
Create a Thymeleaf template in config/views/photos/show.html:
<!DOCTYPE html>
<html lang="en" xmlns:th="https://www.thymeleaf.org">
<head>
<title>Photo</title>
</head>
<body>
<h1 th:text="${photo.title}"></h1>
<a th:href="${photo.url}"><img th:src="${photo.thumbnailUrl}" th:alt="${photo.title}"/></a>
</body>
</html>
5.6. Tests
Write a test that verifies the application renders the HTML page.
from typing import Annotated
import pytest
import requests
import java
from micronaut.context.annotation import Requires
from micronaut.http.annotation import Controller, Get, PathVariable
from pyronaut.test import MicronautTest, micronaut_test_fixture
from example.micronaut.photo import Photo
EmbeddedServer = java.type("io.micronaut.runtime.server.EmbeddedServer")
@Requires(property="spec.name", value="PhotosControllerTest")
@Controller
class PhotosApi:
@Get("/photos/{id}")
def find_by_id(self, id: Annotated[int, PathVariable]) -> Photo:
return Photo(
albumId=1,
title="accusamus beatae ad facilis cum similique qui sunt",
url="https://via.placeholder.com/600/92c952",
thumbnailUrl="https://via.placeholder.com/150/92c952",
)
@pytest.fixture
def photos_context(request):
fixture = micronaut_test_fixture(
request,
MicronautTest(
environments=["test"],
transactional=False,
properties={
"spec.name": "PhotosControllerTest",
},
),
)
yield fixture
fixture.stop()
@pytest.fixture
def my_context(request, photos_context):
photos_server = photos_context[EmbeddedServer]
fixture = micronaut_test_fixture(
request,
MicronautTest(
environments=["test"],
transactional=False,
properties={
"micronaut.http.services.photos.url": f"http://localhost:{photos_server.getPort()}",
},
),
) (1)
yield fixture
fixture.stop()
@pytest.fixture
def client(my_context):
return requests.with_context(my_context) (2)
def test_photo(client):
response = client.get("/photos/1", headers={"Accept": "text/html"})
assert response.status_code == 200
html = response.text
assert "<!DOCTYPE html>" in html
expected_url = "https://via.placeholder.com/600/92c952"
expected_title = "accusamus beatae ad facilis cum similique qui sunt"
expected_thumbnail_url = "https://via.placeholder.com/150/92c952"
assert f"<h1>{expected_title}</h1>" in html
assert (
f'<a href="{expected_url}"><img src="{expected_thumbnail_url}" alt="{expected_title}"/></a>'
in html
)
| 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. |
6. Testing the Application
To run the tests:
pyronaut install
pyronaut validate-config
pyronaut test
7. Next Steps
Explore more features with Micronaut Guides.
Read more about Micronaut Views.
8. License
| All guides are released with an Apache License 2.0 for the code and a Creative Commons Attribution 4.0 license for the writing and media (images). |