Making nice pages with Sass and Spring

Jump to Sass Jump to Spring Link to demo page Screenshot of generic code.

Sass

Intro

Sass looks very similar to CSS, but it has a few tricks up its sleeve which can help you organize your CSS and make it more modular than it was before. A few simple features of Sass that will be covered are:

There are other features of Sass that you can use like mathematical operators, extend/inheritance and modules which can be found through the official website:

https://sass-lang.com/guide

Variables

Variables are declared by using the $ operator. They can store any CSS value you want to reuse. An example of a variable can be:


$bg-colour: #E9EBF1
            

You can also use them for fonts as well:


$text: 'Work Sans', sans-serif;
            

You typically declare them near the top of the SCSS file so you can use throughout the file like so:


body {
    background-color: $bg-colour;
    font-family: $text;
}
            

Mixins

Mixins can be used to avoid repetitive code by compacting them into a single statement. They act like functions and you can even chose to pass in variables. However this example does not pass any variables.


// mixins, reuse this in each element I want to become a "card"
@mixin card-theme() {
    background-color: $win-colour;
    box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
    border-radius: 5px;
    padding: 1em;
}
            

Now whenever the line


@include card-theme(); 
            

is placed in an element, the CSS rules from the card theme will be applied to the respectful element.

Nesting

You can edit elements nested elements using Sass's nesting elements using Sass's nesting features which can make your code more readable and easier to understand with the visual hierarchy.


form {
    padding: 2em 0;
    p {
        font-size: 1em;
        padding: 0.25em 0;
    }
}
            

This will only style p elements that are within a form element.

Conclusion

This covers the very basics of using Sass, you can learn more by going to the Sass website and finding the other cool features that Sass offers. All the code snippets here were also used in the demo website.

You can find the demo website here

The SCSS file used in the demo site can be found here

Spring

Our Spring Demo And How We Built it

What is the Project

For our demo project, we built a simple full-stack CRUD todo application. Here, we will use Spring to implement all the business logic, database connections, REST endpoints, and web pages.

We intentionally omitted some features so this guide doesn’t become overly long. As an added benefit, these omitted features leave the users with an exercise to try out themselves! Despite this, the demo should give a really good overview of the major features needed to make a full web application with the Spring framework. We also won’t be styling the pages since that will be left for the Sass guide.

Initializing the Project

To start, go to the spring initializer to initialize the Spring Boot application. To build the project like how we did, you will need to select the following:

along with the appropriate project metadata of your choice. For the dependencies, we used the following:

From there, you can download the project by selecting the generate button.

Setting up the database

Spring gives you powerful functionality to manage a database with little to no SQL code involved. For instance, we can leverage a tool called Hibernate to automatically generate a database using simple Java classes. The classes we create will represent database entities that Hibernate will use to create the tables for us. Throughout this guide, I will refer to these classes as entities.

To start, let’s create what’s called a base entity which is just a superclass our entities will inherit from:

package us.john.hanna.cps530assignment.entities;

import lombok.Data;

import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;

// MappedSuperclass signifies this will be a superclass for our entities
@MappedSuperclass
/*
Data is a lombok annotation to implicitly generate toString, hashCode, equals, getters, setters, and a constructor to set the fields in this class!
*/
@Data
public class BaseEntity {
   // @Id specifies that this field will be the id for any entity inheriting this
   @Id
   // @GeneratedValue specifies that this id will be autogenerated by hibernate
   @GeneratedValue(strategy = GenerationType.AUTO)
   private Long id;

}

Next, let’s create a Todo entity to represent items in the to-do list:

package us.john.hanna.cps530assignment.entities;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import us.john.hanna.cps530assignment.domain.TodoDto;

import javax.persistence.Entity;
import javax.persistence.ManyToOne;
import java.sql.Timestamp;

@Entity
@AllArgsConstructor
@NoArgsConstructor
public class Todo extends BaseEntity {

   // @Getter creates a getter and @Setter creates a setter
   @Getter
   @Setter
   private String subject;
   @Getter
   @Setter
   private Timestamp dueDate;

   // Many todos will be owned by one user
   @ManyToOne
   private User owner;

   public Todo(Long id, String subject, Timestamp dueDate, User owner) {
       this.subject = subject;
       this.dueDate = dueDate;
       this.owner = owner;
       this.setId(id);
   }

   /*
   Create an Instance of TodoDto using this class. TodoDto is just a POJO that will later be used to represent HTTP responses. The TodoDto class simply contains an id, due date (long epoch time), and the subject of the to-do.
    */
   public TodoDto toDto(){

       return new TodoDto(getId(), dueDate.getTime(), subject);

   }

}

As our last entity, we need to create a User class:

package us.john.hanna.cps530assignment.entities;

import lombok.*;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.OneToMany;
import java.util.Set;

@Entity
@AllArgsConstructor
@NoArgsConstructor
public class User extends BaseEntity{

   @Getter
   @Setter
   // No user can have the same username
   @Column(unique = true)
   private String username;
   @Getter
   @Setter
   private String password;

   /*
    One user will own many todos. Map the entity-relationship based on the *owner* field in the Todo entity.
    This adds the id of the user as a field on the Todo table.
    */
   @OneToMany(mappedBy = "owner")
   @Getter
   @Setter
   private Set<Todo> todos;

}

Finally, to set up our database properly, we need to add some configurations. In the src/main/resources folder, create a file called application-dev.properties with the following configurations:

spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.h2.console.enabled=true

Note, since the file we made is called application-dev.properties we need to tell Spring we are in a dev environment for these properties to be active. To do this, set the command line argument Dspring.profiles.active to dev. Doing so will make the properties in that file active, and override any conflicting settings in application.properties. From there, to run the application, go to http://localhost:8080/h2-console, log in to the database administration page with the above username and password, and verify that the tables were created.

Note: you may want to temporarily disable spring-security when doing this as it may interfere with you accessing the h2 console. Simply comment out the spring-security dependency in the pom.xml file

Querying the Database

Now that we created code to generate our database, we need a way to query it. Lucky for us, Spring Data JPA makes it easy. To query users in our database, we just need to make a simple interface:

package us.john.hanna.cps530assignment.data;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import us.john.hanna.cps530assignment.entities.User;

import java.util.Optional;

@Repository
// The type arguments must be the entity you want to query followed by the type of its ID
public interface UserRepo extends JpaRepository<User, Long> {

   // The Optional class is a wrapper class to provide null safety. It gives options for if the returned value is null.
   Optional<User> findByUsername(String username);

}

Notice how we annotate it with @Repositiory. This marks the class as a Spring-managed component (bean). This indicates that Spring will make an instance of this interface at runtime and provide it to any other bean that needs to use it. This is called dependency injection and is a huge part of the Spring framework. Any classes marked with @Component, @Service, @Repository, @Controller, @Configuration, or @RestController are marked as beans. This means that Spring will create a singleton instance of it at runtime (you can change the scope if you don’t want it to be a singleton but that is usually not necessary). Any of these beans then can make use of other beans by requiring an instance of it in their constructor.

In the above interface, we have a special instance of dependency injection where Spring will generate the implementation of the interface for us and use that as the bean. Behind the scenes, the findByUsername method will invoke an SQL query to search for users based on their username. Any methods here must be precisely named for Spring to create the implementation with the correct query. If there is an error in the naming convention, the application will fail to run. For more complex queries, use the @Query annotation to specify what query to use for that method. Aside from that, there are many other methods inherited from JpaRepository that you may use to query the database. With that newly created interface, we can then inject it into any other bean to suit our needs.

Now we can also make a TodoRepo interface to query for to-do list items:

package us.john.hanna.cps530assignment.data;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import us.john.hanna.cps530assignment.entities.Todo;
import us.john.hanna.cps530assignment.entities.User;

import java.util.Optional;
import java.util.Set;

@Repository
public interface TodoRepo extends JpaRepository<Todo, Long> {

   Set<Todo> findAllByOwner(User owner);

   // The Optional class is a wrapper class to provide null safety. It gives options for if the returned value is null.
   Optional<Todo> findByOwnerAndId(User owner, Long id);

   Boolean existsByOwnerAndId(User user, Long id);

}

Implementing Login And Sign REST endpoints

Doing this gets rather involved and I can’t fit all the source code and concepts in an easily digestible format. However, we will go over the gist of how our security systems are implemented and you can check out my full guide on Spring Security here. Alongside that, you can always check out our repository for the full implementation. I will be sure to add detailed comments to it where needed. In a nutshell, we add the following to our project:

  1. Auth0’s JWT maven dependency:
<dependency>
       <groupId>com.auth0</groupId>
       <artifactId>java-jwt</artifactId>
       <version>3.10.3</version>
</dependency>

This is for us to create security tokens to authenticate and/or authorize a user.

  1. An app.secret property in our application-dev.properties file which we will use to help generate our security tokens. The value can be any string but for a production environment, I like to use a secure password generator to create the value.
  2. A JWTAuthService class to save new users, generate security tokens, log in the user, and get the currently signed-in user. This is an implementation of a generic AuthService interface we created as part of the project.
  3. An implementation of Spring Security’s UserDetails interface which we named UserDetailsImpl. This is just a wrapper class for our User entity which provides info like the account activation status.
  4. An implementation of Spring Security’s UserDetailsService interface which we named UserDetailsServiceImpl. This is for Spring to be able to find a user’s corresponding UserDetails based on their username.
  5. An implementation of Spring Security’s AuthenticationManager interface which we named AuthManagerImpl. This is to validate the credentials of a user.
  6. A JWTFilter class that extends Spring Security’s BasicAuthenticationFilter class. This class will be used to intercept any HTTP requests and use our AuthManagerImpl to verify the security token. The security token needs to be placed in the Authorization header of each request like so: Bearer .
  7. A SecurityBeans class. This contains a method with the @Bean annotation that returns a BCryptPasswordEncoder object. The @Bean annotation specifies that the password encoder returned is a Spring-managed instance. The SecurityBeans class must also be a bean for this to work. We make it a bean by adding the @Configuration annotation to it.
  8. A SecurityConfig class that extends the WebSecurityConfigurerAdapter class from Spring Security. This class will set the UserDetailsService implementation Spring will use (the implementation we created) and the password encoder (the bean we created). As well, we use this class to tell Spring that we want it to apply our JWTFilter to intercept incoming requests. Lastly, this class configures specific endpoints for Spring Security to ignore securing. These include the login and signup endpoints and endpoints needed for the autogenerated documentation (we won’t be including how to generate the documentation in the guide).
  9. An AuthController class configuring REST endpoints a frontend would use to login and signup.

Again, be sure to look through the implementation of each of the classes mentioned here for a full understanding of our security framework.

Creating a TodoService

Now, let’s create a TodoService interface to manage a user’s to-do list:

package us.john.hanna.cps530assignment.services;

import us.john.hanna.cps530assignment.domain.TodoDto;
import us.john.hanna.cps530assignment.entities.Todo;
import us.john.hanna.cps530assignment.exceptions.BadAuthRequest;
import us.john.hanna.cps530assignment.exceptions.TodoNotFoundException;

import java.util.Set;

public interface TodoService {

   Set<Todo> getAllTodos() throws BadAuthRequest;

   Todo getTodoById(Long id) throws TodoNotFoundException, BadAuthRequest;

   void deleteTodo(Long id) throws TodoNotFoundException, BadAuthRequest;

   void updateTodo(Long id, TodoDto dto) throws TodoNotFoundException, BadAuthRequest;

   Long createTodo(TodoDto dto) throws BadAuthRequest;

}

We want to make an interface first as best practice and inject a generic TodoService to our components to allow for flexibility. This way, we can, if needed, create multiple beans that implement TodoService and inject them into our components depending on the environment to suit our needs. This could also be used if we need to depreciate an implementation of TodoService without having to change much code. Anyways, here is the implementation:

package us.john.hanna.cps530assignment.services;

import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
import us.john.hanna.cps530assignment.data.TodoRepo;
import us.john.hanna.cps530assignment.domain.TodoDto;
import us.john.hanna.cps530assignment.entities.Todo;
import us.john.hanna.cps530assignment.exceptions.BadAuthRequest;
import us.john.hanna.cps530assignment.exceptions.TodoNotFoundException;

import java.sql.Timestamp;
import java.util.Set;

@Service
@AllArgsConstructor
public class TodoServiceImpl implements TodoService {

  // These two fields are injected via the all args constructor generated by the above annotation
  private TodoRepo todoRepo;
  private AuthService authService;

  // BadAuthRequest is a simple custom exception
  @Override
  public Set<Todo> getAllTodos() throws BadAuthRequest {
      return todoRepo.findAllByOwner(authService.getCurrentlySignedInUser());
  }

  // TodoNotFoundException is a simple custom exception
  @Override
  public Todo getTodoById(Long id) throws TodoNotFoundException, BadAuthRequest {
      return todoRepo.findByOwnerAndId(authService.getCurrentlySignedInUser(), id).orElseThrow(TodoNotFoundException::new);
  }

  @Override
  public void deleteTodo(Long id) throws TodoNotFoundException, BadAuthRequest {
      if(!todoRepo.existsByOwnerAndId(authService.getCurrentlySignedInUser(), id)){
          throw new TodoNotFoundException();
      }
      todoRepo.deleteById(id);
  }

  @Override
  public void updateTodo(Long id, TodoDto dto) throws TodoNotFoundException, BadAuthRequest {

      Todo original = todoRepo.findByOwnerAndId(authService.getCurrentlySignedInUser(), id)
              .orElseThrow(TodoNotFoundException::new);
      Timestamp due = dto.getDueDate() == null ? original.getDueDate() : new Timestamp(dto.getDueDate());
      String subject = dto.getSubject() == null ? original.getSubject() : dto.getSubject();
      todoRepo.save(new Todo(id, subject, due,
              authService.getCurrentlySignedInUser()));
  }

  @Override
  public Long createTodo(TodoDto dto) throws BadAuthRequest {

      Todo todo = todoRepo.save(new Todo(dto.getSubject(), new Timestamp(dto.getDueDate()),
              authService.getCurrentlySignedInUser()));
      return todo.getId();
  }
}

Creating Endpoints to Expose our Service

With that service created, let’s create REST endpoints that will make use of it:

package us.john.hanna.cps530assignment.controllers;

import lombok.AllArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import us.john.hanna.cps530assignment.domain.TodoDto;
import us.john.hanna.cps530assignment.entities.Todo;
import us.john.hanna.cps530assignment.exceptions.BadAuthRequest;
import us.john.hanna.cps530assignment.services.TodoService;

import java.net.URI;
import java.util.Set;
import java.util.stream.Collectors;

// @RestController specifies that this bean will be for managing REST endpoints
@RestController
// Prefix all the endpoints here with /api/todo
@RequestMapping("/api/todo")
@AllArgsConstructor
public class TodoController {

   // This field is injected via the all args constructor generated by the annotation above
   private final TodoService todoService;

   // Map this method to the endpoint: GET /api/todo/allTodos
   @GetMapping("/allTodos")
   public ResponseEntity<Set<TodoDto>> myTodos() throws BadAuthRequest {
      
           return ResponseEntity.ok(todoService.getAllTodos().stream()
                   // convert all the Todos to instances of TodoDto
                   .map(Todo::toDto)
                   .collect(Collectors.toSet()));

   }

   // Map this method to the endpoint: POST /api/todo
   // @RequestBody specifies that the following argument represents the request body 
   @PostMapping
   public ResponseEntity<Void> postTodo(@RequestBody TodoDto dto) throws BadAuthRequest {

           Long todoId = todoService.createTodo(dto);
           return ResponseEntity.created(URI.create("/todo/" + todoId)).build();

   }

   // Map this method to the endpoint: GET /api/todo/{id} where id is the id of the TODO
   // @PathVariable specifies that the following argument is the path variable of {id} in the url
   @GetMapping("/{id}")
   public ResponseEntity<TodoDto> getTodoById(@PathVariable("id") Long id) throws BadAuthRequest {

           return ResponseEntity.ok(todoService.getTodoById(id).toDto());

   }

   // Map this method to the endpoint: PATCH /api/todo/{id} where id is the id of the TODO
   @PatchMapping("/{id}")
   public ResponseEntity<Void> updateTodo(@PathVariable("id") Long id, @RequestBody TodoDto dto) throws BadAuthRequest {

           todoService.updateTodo(id, dto);
           return ResponseEntity.created(URI.create("/todo/" + id)).build();

   }

   // Map this method to the endpoint: DELETE /api/todo/{id} where id is the id of the TODO
   @DeleteMapping("/{id}")
   public ResponseEntity<Void> deleteTodo(@PathVariable("id") Long id) throws BadAuthRequest {

           todoService.deleteTodo(id);
           return ResponseEntity.noContent().build();

   }

}

Creating a Frontend

Now that we’ve completed our business logic and created an API, we can build a frontend that consumes it. To do that, we’ve added thymeleaf as a maven dependency. With thymeleaf, we can pass data to our pages and display it on the elements using custom HTML attributes.

Note: we will only be adding login and signup pages along with a page to display all the to-do list items. For adding, deleting, and updating to-do list items you’ll need to use a tool like curl or postman to send the requests to the API. As an exercise, you can even create pages to do this with a GUI!

To start, let’s create a simple controller:

@Controller
@RequestMapping("views")
public class ViewsController {

   // Inject the value of app.origin from our application-dev.properties. This should be set to http://localhost:8080
   @Value("${app.origin}")
   private String origin;

}

Then let’s add endpoints to redirect to HTML pages in our src/main/resources/templates folder:

@GetMapping("login")
// The Model object is for adding data to the page we will direct the user to.
public String login(Model model){

  // Send the base URL of our app to the page. This data will be accessed via an id of "origin".
  model.addAttribute("origin", origin);
  // Return any HTML page in the folder src/main/resources/templates named login.html
  return "login";

}

@GetMapping("signup")
public String signup(Model model){

  model.addAttribute("origin", origin);
  return "signup";

}

Note that in this case, the controller returns pages based on the returned string. This is because we annotated it with @Controller instead of @RestController. If we used @RestController, it would give the user the raw string.

From there, let’s build the login page our first endpoint points to:

<!DOCTYPE html>
<!-- xmlns:th="http://www.thymeleaf.org" -> add Thymeleaf to the page -->
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="UTF-8">
  <title>Login</title>
</head>
<body>

  <!-- th:action="${origin} + '/views/login'" -> set the action attribute of this form to http://localhost:8080/views/login
    ${origin} is the data we sent to this page ("http://localhost:8080")
    -->
  <form method="post" th:action="${origin} + '/views/login'">
      <label for="username">
          Username
      </label>
      <input id="username" name="username" type="text">
      <label for="password">
          Password
      </label>
      <input id="password" name="password" type="password">
      <button type="submit">Login</button>
  </form>
</body>
</html>

Notice how this form sends a POST request to http://localhost:8080/views/login, let’s create that endpoint in our ViewController:

@PostMapping(path = "login")
// @RequestParam Map<String, String> body -> the key value pairs corresponding to form data
public String getTodosPage(Model model, @RequestParam Map<String, String> body){

        try{
    
            // LoginRequest is a class we created to represent a request body for the login endpoint of our API. It is just a POJO.
            LoginRequest loginRequest = new LoginRequest(body.get("username"), body.get("password"));
            // The org.springframework.web.client.RestTemplate class is for sending HTTP requests.
            RestTemplate rest = new RestTemplate();
            // Send a post request to our API to login using the loginRequest object as the request body
            ResponseEntity<String> response = rest.postForEntity(origin + "/api/auth/login", new HttpEntity<>(loginRequest), String.class);
    
            try{
    
                // Retrieve the security token (if successful) from the response
                String token = response.getBody();
                // Add the security token to the Authorization header
                HttpHeaders headers = new HttpHeaders();
                headers.set("Authorization", "Bearer " + token);
                // Send an HTTP request to get all of this user's todos
                ResponseEntity<TodoDto[]> todosResponse = rest.exchange(origin + "/api/todo/allTodos",
                HttpMethod.GET, new HttpEntity(headers), TodoDto[].class);
                // Add the todos from this user on the todos page
                model.addAttribute("todos", todosResponse.getBody());
                // Send the security token to the todos-page (just in case)
                model.addAttribute("token", token);
                return "todos-page";
    
            }catch(HttpClientErrorException ex){
    
                model.addAttribute("error", "Error fetching your todos");
                return "error";
    
            }

        }catch(HttpClientErrorException ex){

            model.addAttribute("error", "Incorrect username or password. Please try again.");
            return "error";

        }

}

From there, let’s add a simple page to show all the user’s to-dos:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="UTF-8">
  <title>Your todos</title>
</head>
<body>
  <h1>Here are your to-dos</h1>
  <ul>
      <!-- th:each="todo: ${todos}" -> for each of the todo list items, create an li element like so -->
      <!-- ${#dates.format(new java.util.Date(todo.dueDate))} -> change the long the date was stored as to a human-readable date -->
      <li th:each="todo: ${todos}" th:text="'TODO: ' + ${todo.subject} + ' DUE: ' + ${#dates.format(new java.util.Date(todo.dueDate))}"></li>
  </ul>
</body>
</html>

Lastly, let’s create some GUIs for the signup login. To start, let’s build that signup.html page:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">

<head>
    <meta charset="UTF-8">
    <title>Sign Up</title>
    <link rel="stylesheet" th:href="@{/css/reset.css}">
    <link rel="stylesheet" th:href="@{/css/styles.css}">
</head>

<body class="content">
<section class="login">
    <section class="card">
        <p>Don't have an account yet?</p>
        <h2>Sign up</h2>

        <form method="post" th:action="${origin} + '/views/signup'">
            <p><label for="username">
                Username
            </label></p>
            <input id="username" name="username" type="text">
            <p><label for="password">
                Password
            </label></p>
            <input id="password" name="password" type="password">
            <p><label for="confirm-password">
                Confirm password
            </label></p>
            <input id="confirm-password" name="confirmPassword" type="password">
            <p><button class="submit-button" type="submit">Signup</button></p>
            <a th:href="${origin} + '/views/login'">Login</a>
        </form>

    </section>
</section>


</body>

</html>

Notice how it sends a POST request to http://localhost:8080/views/signup, let’s implement that endpoint in our ViewController:

@PostMapping("signup")
// @RequestParam Map<String, String> body -> the key-value pairs corresponding to form data
public String processSignUpForm(Model model, @RequestParam Map<String, String> body){

        // SignupRequest is just a POJO with the username, password, and confirm password
        SignupRequest signupRequest = new SignupRequest(body.get("username"), body.get("password"), body.get("confirmPassword"));
        RestTemplate rest = new RestTemplate();

        try {
            ResponseEntity<String> response = rest.postForEntity(origin + "/api/auth/signup",
            new HttpEntity<>(signupRequest), String.class);
            model.addAttribute("origin", origin);
            return "successful-signup";
        }catch (HttpClientErrorException ex){
            model.addAttribute("error", ex.getMessage());
            return "error";
        }

}

From there, let’s build a simple page for when the user successfully signs up:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
      <meta charset="UTF-8">
      <title>Welcome!</title>
    </head>
    <body>
      <h1>Thanks for signing up for this app!</h1>
      <p>Your account has successfully been created. You may login <a th:href="${origin} + '/views/login'">here</a>.</p>
    </body>
</html>

Finally, let’s build a simple controller to redirect the user to the signup page from the root url of our app:

package us.john.hanna.cps530assignment.controllers;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.servlet.view.RedirectView;

@Controller
public class HomeController {
    // This method is mapped to the root url of our app since we didn't pass any arguments to the GetMapping annotation
    @GetMapping
    public RedirectView getHomePage(){

        return new RedirectView("/views/signup");

    }

}

Conclusion

With that, you should have an idea of how to build a full-stack to-do app with Spring as the frontend and the backend. This was probably a ton to pick up so definitely look over the code and re-read the guide if you need to. The Spring community is fairly large so there are plenty of resources out there to learn it more fully. As an exercise, you can try adding GUIs for adding, updating, or removing items in a user’s to-do list. You can even try something more advanced and configure email functionality to the signup process!

Further Reading