Introduction
Thymeleaf is a templating (server-side rendering) engine used by many Java software engineers within Spring-based web applications. An important feature of any web application is the support for dynamic URLs and path variables within those URLs.
Most REST APIs extensively use path variables to specify the IDs of elements they're performing operations on. For instance, a typical example would be:
https://www.somewebsite.com/viewPost/path-variables-with-spring-boot
# OR
https://www.somewebsite.com/viewProduct/5
In both of these cases, we're trying to find a resource denoted by a certain identifier. In the first case, we're identifying a resource by its title - path-variables-with-spring-boot
, while in the second, we're identifying it through an incremental ID counter - 5
.
Note: When using predictable path variables, such as an incremental counter, beware of security concerns. First of all - these are easily scrapeable, but most important than anything, without proper validation - someone might figure out that /deleteProduct/5
deletes entries from the database, and decide to drop most of the entities stored within it.
In this guide - we'll take a look at how to leverage Thymeleaf to retrieve path variables, and how to use Spring Boot controllers to process them.
Throughout the guide, we'll be using a Post
model, which represents a blog post - which just has an ID and some content
:
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "post_sequence")
private Long id;
private String content;
// Constructors, Getters, Setters, and toString
}
Since we're using Spring Boot - let's also bootstrap a PostRepository
based on the JpaRepository
, which allows us to perform CRUD operations out of the box:
@Repository
public interface PostRepository extends JpaRepository<Post, Long> {}
Thymeleaf Path Variables with Spring Boot
Some URLs are dynamic - namely, given the fact that we can create, read, update and delete our Post
entities, we'll want to have dynamic URLs for the GET
, UPDATE
and DELETE
requests.
Let's initialize our project with a few artificial posts:
@GetMapping("/initialize")
public ResponseEntity<String> initialize() {
Post post1 = new Post("Content of post 1");
Post post2 = new Post("Content of post 2");
Post post3 = new Post("Content of post 3");
postRepository.saveAll(List.of(post1, post2, post3));
return ResponseEntity.ok("Initialized posts");
}
Once we hit our /initialize
endpoint, three posts will be generated and saved into the database. Now, let's define a static URL for the user to retrieve all posts:
@GetMapping("/viewPosts")
public String viewAllPostsAndComments(Model model){
List<Post> postList = postRepository.findAll();
model.addAttribute("postList", postList);
return "all-posts";
}
This is a static URL and handler - there aren't any path variables or parameters that can let the user influence which posts are being retrieved. This is typically what you want if you'd like to let the user choose where they'd like to navigate. Once chosen, they can view the detailed listing for any post, just by clicking on it and navigating to its page.
Since it'd be impractical (and unfeasible) to create a request handler for each post, we can make a dynamic handler that accepts any post ID, finds the post in the database and returns it:
@GetMapping("/viewPost/{postId}")
public String viewPost(@PathVariable("postId") Long postId, Model model) {
Post post = postRepository.findById(postId).get();
model.addAttribute("post", post);
return "view-post";
}
Here, we've defined a @GetMapping
for the URL /viewPost/{postId}
. The postId
wrapped in curly brackets is a dynamic variable that can be set to any value and/or type. In our method's signature, we've used the @PathVariable
annotation, setting the name of the path variable, and assigning it to a type that we can reference - Long postId
.
Note: The name of the path variable in the @GetMapping
annotation has to match the name we've defined in the @PathVariable
annotation. You can use as many path variables as you'd like in a URL, and match them via their names.
Once a GET
request is fired on, say, the /viewPost/5
endpoint - the postId
is converted to a Long
implicitly and we can use it to search for the post via its ID in the database. If we, however, pass in another type, such as a String
- /viewPost/some-post
, this controller will throw an exception:
java.lang.NumberFormatException: For input string: "some-post"
Since we've got the controllers to handle both a request to view all posts as well as to view a single one, let's quickly write up the Thymeleaf pages that allow us to navigate through to these request handlers. Let's first start off with a page that lists all of the posts and allows the user to navigate to the different controller request handlers:
<div th:each="post : ${postList}">
<p th:text="${post.content}"></p>
<a class="btn btn-info" th:href="@{/viewPost/{id}(id = ${post.id})}">View Post</a>
</div>
Check out our hands-on, practical guide to learning Git, with best-practices, industry-accepted standards, and included cheat sheet. Stop Googling Git commands and actually learn it!
Here, we've iterated over each post in the postList
(list of posts we've added to the Model
instance) and for each one, added a button allowing the user to view the post. The href
attribute of the link leads us to:
th:href="@{/viewPost/{id}(id = ${post.id})}
This is the standard URL syntax for adding parameters in Thymeleaf. The standard @{}
expression used for links also accepts path variables. The {id}
is a path variable, which we can externally set. Here, we've pointed it to the post.id
. Once rendered on the server-side, this expression is evaluated as:
<a href="localhost:8080/viewPost/1" class="btn btn-info" >View Post</a>
Clicking this button will trigger the /viewPost/{postId}
endpoint, with the postId
value being converted to 1
and the postRepository
will fetch the post with the id
of 1
, returning it to the view-post
Thymeleaf view:
<div class="container">
<div class="row">
<div class="col-md-12 bg-light">
<p th:text="${post.content}"></p>
</div>
</div>
</div>
Conclusion
Path variables are a common and key-feature of REST APIs. In this guide, we've taken a look at how to use path variables in Spring Boot with Thymeleaf.