Thymeleaf Path Variables with Spring Boot

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>
Free eBook: Git Essentials

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.

Last Updated: October 14th, 2023
Was this article helpful?

© 2013-2024 Stack Abuse. All rights reserved.

AboutDisclosurePrivacyTerms