This project is meant to teach Spring B oot fundamentals by creating a bookshop, step by step.
It uses the Spring Boot framework, a Bootstrap 5 frontend and stores its data in CSV files.
This is how the final shop looks like:
Follow these steps to implement the bookshop:
-
On the next screen, select the following dependencies:
- DevTools (for instant refresh)
- Mustache (as templating engine for interactive HTML pages)
- Spring Web (to add the Tomcat webserver)
-
Wait until all files and folders are created.
-
In your application.properties, set the Mustache file suffix to
html:spring.application.name=Book Shop spring.mustache.suffix=.html
- Go to a template portal, e.g. TemplatesJungle, and select a proper HTML template for your shop.
- Klick the download button and continue to checkout.
- Open the ZIP file in your "Downloads" folder and extract it to your resources/static directory, including subdirectories.
-
Your static folder structure should look similar to this, now:

-
In the Shop class, start the Spring Boot server.
@SpringBootApplication public class Shop { public static void main(String[] args) { SpringApplication.run(Shop.class, args); } }
-
Open this URL in your Browser: http://localhost:8080
Your static start page should appear.
- Create a Controller class named ShopController.java
- Give it a
@Controllerannotation. - Add a method to handle the root URL: "/"
- Add a method to handle
.htmlURLs. The result should look like this:@Controller public class ShopController { @GetMapping(value = {"/"}) public String root(Model model) { return "redirect:/index.html"; } @GetMapping(value = {"/{name}.html"}) public String htmlMapping(@PathVariable String name, HttpSession session) { return name; } }
-
Restart the web server (http://localhost:8080) and test whether the HTML page links still work properly.
-
From the index.html file, cut out part of the HTML head section and paste it into a new partials/htmlHead.html file:
<meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="icon" href="/images/favicon.ico" sizes="48x48"> <link rel="stylesheet" type="text/css" href="/css/bootstrap.min.css"> <link rel="stylesheet" type="text/css" href="/css/style.css"> <link rel="stylesheet" type="text/css" href="/css/swiper-bundle.min.css"/> <link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
-
In the index file, create a reference to the freshly created htmlHead file.
-
Repeat these steps for header, footer and other page sections.
-
Your HTML file should look like this:
-
Repeat these steps for all other HTML files: replace their partials code by Mustache templates.
- In the merchandise folder, create an Article.java class.
- It should contain the basic article fields:
public class Article { /** Unique article number */ protected int articleNo; /** Display-title of this Article */ protected String title; /** Manufacturer of this Article */ protected String manufacturer; /** Shop price */ protected double price; /** URL to the image */ protected String image; ... }
- Also, create a Book class containing following fields:
public class Book extends Article { protected int pages; protected String author; protected Format format; protected Genre genre; ... }
-
In the resources directory, create a CSV file containing a list of articles or books. It could look like this:
Title;Author;Genre;Pages;Publisher;Price;Image God Created the Integers;Hawking, Stephen;mathematics;197;Penguin;24,50;https://m.media-amazon.com/images/I/71HKbmRoVmL._AC_UY218_.jpg -
To facilitate the import of CSV files, add “commons-csv” to the dependencies in the pom.xml:
<dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-csv</artifactId> <version>1.10.0</version> </dependency>
-
In the Shop class, create a
readArticles()method that will read a CSV file and fill a Book list from it. Pass thefileNameandListas parameters:private static void readArticles(String fileName, List<Book> books) { ... }
-
Create a getter method so that we can access the article list from outside.
public static List<Book> getArticles() { return books; }
-
In ShopController.homePage(), load the article list and pass it to the view model.
@GetMapping(value = {"/index.html"}) public String homePage(Model model) { model.addAttribute("articles", Shop.getArticles()); return "index"; }
-
In the index.html template, iterate over the
articles. Integrate the article fields in the proper HTML code section. -
Check the result in your browser. The dynamic articles from the CSV file should appear on your homepage: http://localhost:8080. The result should look like this:

- To be able to add multiple copies of an item to the shopping cart, we need to extend the article or book class. To do this, we create a new CartItem class in the merchandise package, which inherits from Book.
- Add a
quantityfield and generate a getter and setter for it. - Create a
getTotalPrice()method which calculates the total price, taking into account the quantity. The code should look like this:public class CartItem extends Book { private int quantity = 0; public int getQuantity() { return quantity; } public void setQuantity(int quantity) { this.quantity = quantity; } public double getTotalPrice() { return quantity * price; } }
- As we have changed the cart items from Article to CartItem, we must refactor and correct all corresponding code lines.
- We want to use the shopping cart in the web server session, so we have to use the proper Spring Annotations. The resulting code should look like this:
@Component @SessionScope public class Cart { private List<CartItem> items = new ArrayList<>(); public List<CartItem> getItems() { return items; } }
- Create a method that finds a cart item by its article number:
private CartItem findItem(int articleNo) { ... }
- Create a method to add an article to the shopping cart. If the article already exists, its quantity should be increased.
public void addArticle(Book book) { ... }
- Create a method to remove an article from the shopping cart.
public boolean removeArticle(int articleNo) { ... }
- Create a method to decrease the quantity of an existing article.
public boolean decreaseQuantity(int articleNo) { ... }
- To use the Shop sortiment and Cart items, add those Beans to the ShopController using the
@Autowiredannotation:@Controller public class ShopController { @Autowired Shop shop; @Autowired Cart cart; }
- In the ShopController, create a method
getCartItems()that adds the current cart items to the model.private void getCartItems(Model model) { ... }
- Call the
getCartItems()method at the end of thehomePage()andhtmlMapping()methods. - As this is the second controller and there are more to come, we should move the controllers to their own package. To do so, create a new controllers package under the onlineshop folder. Move the ShopController to that package.
-
We need a new Controller to handle cart actions. In the controllers package, create a new CartController. It should handle every request starting with
/cart.@Controller @RequestMapping(value = "/cart") public class CartController { ... }
-
We need methods to add and remove cart items, as well as to increase and decrease their quantity. First, create the
addToCart()method.@GetMapping(value = {"/add/{articleNo}"}) public String addToCart( @PathVariable(name = "articleNo") Integer articleNo, RedirectAttributes atts) { ... }
-
Next, implement the
removeFromCart()method.@GetMapping(value = {"/remove/{articleNo}"}) public String removeFromCart( @PathVariable(name = "articleNo") Integer articleNo, RedirectAttributes atts) { ... }
-
Don't forget to add the
increaseQuantity()anddecreaseQuantity()methods.@GetMapping(value = {"/increase/{articleNo}"}) public String increaseQuantity( @PathVariable(name = "articleNo") Integer articleNo) { ... } @GetMapping(value = {"/decrease/{articleNo}"}) public String decreaseQuantity (@PathVariable(name = "articleNo") Integer articleNo, RedirectAttributes atts) { ... }
-
After most of the actions, we want to give feedback to the user, so we will be using the FlashAttribute in RedirectAttributes.
public String addToCart(..., RedirectAttributes atts) { ... atts.addFlashAttribute(MESSAGE, message); atts.addFlashAttribute(SHOW_MESSAGE, true); }
-
We will use the
showMessageflag to make themessagevisible in the HTML pages.
- Next, we want to trigger the controller's actions from the HTML pages. To do so, edit the index.html file, find the cart button and insert the proper
href.<a href="/cart/add/{{articleNo}}" role="button" class="btn btn-dark" data-bs-toggle="tooltip" data-bs-placement="top"> <svg class="cart"><use xlink:href="#cart"></use></svg> </a>
- In the header.html, find the cart section and show the number of cart items.
- Furthermore, list the cart items. If the quantity is > 1, then it is shown, separately, with the help of the
showQuantitygetter.
-
Finally, we implement the cart page. To do so, we first want to display any messages from the server.
-
Then, we iterate over the cartItems and display them.
{{#cartItems}} <div class="cart-item border-bottom padding-small"> <div class="row align-items-center"> <div class="col-lg-4 col-md-3">...</div> <div class="col-lg-6 col-md-7">...</div> <div class="col-lg-1 col-md-2">...</div> </div> </div> {{/cartItems}} -
In the first column, we display the article image, title and price.
-
In the second column, we place a quantity input field, as well as plus and minus buttons. We add hyperlinks to trigger the corresponding actions.
-
Finally, in the third column, place a remove icon. Add a hyperlink to remove this article from the cart, regardless of the quantity.
-
Don't forget to show the grand total in the
Cart Totalssection. -
The card page should be fully functional, now, and look similar to this:

Now, we can see all 209 books from the CSV file but these are too many entries at once. To handle this, we introduce a pagination, i.e. show only 15 books at once and offer a navigation to the next / previous page.
- We want to control the pagination via the URL parameter
page. So we use a request parameter in ourhomePagemethod.@GetMapping(value = {"/index.html"}) public String homePage(Model model, @RequestParam(name = "page", required = false) Integer page)
- We want to memorize the selected page when the user returns from another page, so we store it in the session. If there is a
pagerequest parameter at the same time, then it should be used. If none of both is present, use a default value. We handle this logic in thegetSessionParam()method:Integer getSessionParam(HttpSession session, String paramName, Integer paramValue, Integer defaultValue)
- Next, we create a method that handles the pagination of the articles array:
handlePagination(Model model, Integer page)
- We need to calculate the
fromandtoindex to be able to return the proper sublist of articles.int numOfArticles = shop.getNumOfArticles(); int from = Math.max((page - 1) * PAGE_SIZE, 0); int to = Math.min(numOfArticles, from + PAGE_SIZE); List<Book> articles = shop.getArticles().subList(from, to);
- We add all this attributes to the view model:
model.addAttribute("from", ++from); model.addAttribute("to", to); model.addAttribute("numOfArticles", numOfArticles); model.addAttribute("articles", articles);
- In index.html, scroll to the
showing-productsection and insert this Mustache code: - Test it by using different
pageparameters in your browser, e.g. http://localhost:8080/index.html?page=2. It should display the proper from/to values:

As Mustache doesn't handle logic, we have to implement it in the controller.
- To do so, we will use a Map with the
pageNumberas key and the current page'sactivestate as value.int pageCount = (numOfArticles / PAGE_SIZE) + 1; Map<Integer, String> pages = new HashMap<>(); for(int pageNumber = 1; pageNumber <= pageCount; pageNumber++) { String active = (pageNumber == page) ? "active" : ""; pages.put(pageNumber, active); }
- We also need page values for the
previousandnextpage links. We add all this attributes to the view model:model.addAttribute("pageCount", pageCount); model.addAttribute("pages", pages.entrySet()); model.addAttribute("prevPage", Math.max(page - 1, 1)); model.addAttribute("nextPage", Math.min(page + 1, pageCount));
- To show the pagination links, scroll to the
Page navigationsection in pagination.html and insert this code: - The result should look like this:

- Test the proper pagination by clicking the links!
It is very sensible to use an Enum for the sorting selection list. This allows us to display it in the frontend and react to the transmitted parameters in the backend. We have already used named enums for genres, book format and gender.
- So we create a new
Enumclass called Sorting.java in the enums directory.public enum Sorting { ALPHA_UP ("Name A-Z"), ALPHA_DOWN("Name Z-A"); private final String label; Sorting(String label) { this.label = label; } }
- We add a sort parameter to the
getArticles()method and call a separatesortArticles()method for it.public List<Book> getArticles(Sorting sorting, int from, int to) { sortArticles(sorting); ... }
- In
sortArticles(), we program a switch statement that takes the Sorting as argument. To perform the sorting, we use theComparator.comparing()method. This takes the getter reference of the desired field as an argument.private void sortArticles(Sorting sorting) { switch (sorting) { case ALPHA_UP: books.sort(Comparator.comparing(Book::getTitle)); break; } }
- To sort the books in descending order, we use the
reversed()method.case ALPHA_DOWN: books.sort(Comparator.comparing(Book::getTitle).reversed()); break;
The next step is to react to possible URL sorting parameters in the ShopController.
-
First we need to add the new request parameter in the homePage method. If there is such a request parameter, we remember it in the session, otherwise we use the standard sorting.
@GetMapping(value = {"/index.html"}) public String homePage(Model view, @RequestParam(name = "page", required = false) Integer page, @RequestParam(name = "sort", required = false) Sorting sort, HttpSession session) { sort = (Sorting) getSessionParam(session, "sort", sort, Sorting.ALPHA_UP); ... }
-
Next, we add a Sorting parameter to the
handlePagination()method and pass it to the shop'sgetArticles()method.void handlePagination(Model view, Sorting sorting, Integer page) { ... List<Book> articles = shop.getArticles(sorting, from, to); ... handleSorting(view, sorting); }
-
Last, we need to pass a view model to the Mustache page, so it can render the proper
<select>sort options. They need three parameters:It's pretty tricky to pass more than a key-value-pair to Mustache, so I decided to enhance the Sorting class to provide the needed data.
-
To do so, we add Getters for
valueandlabel.public String getValue() { return name(); } public String getLabel() { return label; }
-
Next, we create a Getter/Setter for
selected.private String selected; public String getSelected() { return selected; } public void setSelected(String selected) { this.selected = selected; }
-
Now we can use the enhanced
Enummodel in ourhandleSorting()method. We iterate over the sorting values, check whether the entry is the current sorting and set theselectedfield, accordingly.handleSorting(Model view, Sorting currentSort) { List<Sorting> sortings = new ArrayList<>(); for (Sorting entry : Sorting.values()) { String isCurrentSort = (entry == currentSort) ? "selected" : ""; entry.setSelected(isCurrentSort); sortings.add(entry); } view.addAttribute("sortings", sortings); view.addAttribute("sort", currentSort.getValue()); }
Now it's finally time to display the sortings on the homepage.
- Find the sorting section in index.html and replace it with this Mustache code:
- Create a sorting.html partial. In it, we iterate over the sortings that were passed by the controller and display the proper field values.
- The result should look like this:

The last step is to trigger the change the search parameters in the URL as soon as the user changes the sorting dropbox. This cannot be achieved with standart HTML means, so we need a little JavaScript magic here.
- Create a
<script>tag in the<head>section of index.html. Let's add atriggerSortingfunction that reacts to the change event of the sorting dropdown box.<script> function triggerSorting(element) { ... } </script>
- Let's add the selected option's (=
event.target) value to the browsers search parameters and reset the paging to page 1:const searchParams = new URLSearchParams(location.search) searchParams.set('sort', element.value) searchParams.set('page', "1")
- Last step - change the browsers URL:
location.search = searchParams.toString()
- Congratulations! Now it's time to thoroughly test the sorting in your browser!

- Add a new
checkout()method that prepares the view model for the checkout page. Calculate all necessary fields.@GetMapping(value = {"/checkout.html"}) public String checkout(Model view) { loadCartItems(view); double subTotal = Double.parseDouble(cart.getGrandTotal()); double discount = subTotal * 0.05; double shippingCosts = 3.99; double taxRate = 0.07; double taxes = subTotal * taxRate; double grandTotal = subTotal - discount + shippingCosts + taxes; view.addAttribute("subTotal", cart.getGrandTotal()); view.addAttribute("discount", Shop.df.format(discount)); view.addAttribute("shippingCosts", shippingCosts); view.addAttribute("taxes", Shop.df.format(taxes)); view.addAttribute("grandTotal", Shop.df.format(grandTotal)); return "checkout"; }
- Integrate the cart items in the checkout.html page.
- The result should look like this:

- Display the order's subtotal.
Repeat this for discount, shipping, taxes and grand total.
<tr class="subtotal border-bottom"> <th>Subtotal</th> <td data-title="Subtotal"> <span class="price-amount amount text-primary"> <bdi><span class="price-currency-symbol">$</span>{{subTotal}}</bdi> </span> </td> </tr>
- The result should look like this:




