Lab: Creating a 'Student Portal' Web Application


Objective:

Develop a basic Spring Boot web application that displays student details dynamically using Thymeleaf.

Prerequisites

  • JDK 17 or higher installed
  • Maven or Gradle installed
  • A text editor or IDE
  • Completed Module 1 lab

Step 1: Set Up the Project

Start by extending the project from Module 1 or creating a new project:

If creating a new project, use Spring Initializr:

  1. Go to start.spring.io
  2. Configure:
  • Project: Maven
  • Language: Java
  • Spring Boot: 3.2.x
  • Group: com.example
  • Artifact: student-portal
  • Dependencies:
    • Spring Web
    • Thymeleaf
    • Spring Boot DevTools (for auto-reload)
    • Validation (for form validation)
  1. Generate and extract the project

If continuing from Module 1:

  1. Add the following dependencies to your pom.xml:
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <scope>runtime</scope>
    <optional>true</optional>
</dependency>

Step 2: Create the Student Model

Create a new package com.example.studentportal.model and add the Student.java class:

package com.example.studentportal.model;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;

public class Student {

    private Long id;

    @NotBlank(message = "Name is required")
    @Size(min = 2, max = 50, message = "Name must be between 2 and 50 characters")
    private String name;

    @NotNull(message = "Age is required")
    @Min(value = 16, message = "Age must be at least 16")
    private Integer age;

    @NotBlank(message = "Email is required")
    @Email(message = "Please provide a valid email address")
    private String email;

    @NotBlank(message = "Course is required")
    private String course;

    @Pattern(regexp = "^[A-Z0-9]{8}$", message = "Student ID must be 8 characters with uppercase letters and numbers only")
    private String studentId;

    // Default constructor
    public Student() {
    }

    // Constructor with fields
    public Student(Long id, String name, Integer age, String email, String course, String studentId) {
        this.id = id;
        this.name = name;
        this.age = age;
        this.email = email;
        this.course = course;
        this.studentId = studentId;
    }

    // Getters and setters
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getCourse() {
        return course;
    }

    public void setCourse(String course) {
        this.course = course;
    }

    public String getStudentId() {
        return studentId;
    }

    public void setStudentId(String studentId) {
        this.studentId = studentId;
    }

    @Override
    public String toString() {
        return "Student{id=" + id + ", name='" + name + "', age=" + age +
               ", email='" + email + "', course='" + course + "', studentId='" + studentId + "'}";
    }
}

Step 3: Create a Service Layer

Create a new package com.example.studentportal.service and add the StudentService.java interface:

package com.example.studentportal.service;

import com.example.studentportal.model.Student;
import java.util.List;
import java.util.Optional;

public interface StudentService {
    List<Student> getAllStudents();
    Optional<Student> getStudentById(Long id);
    Student saveStudent(Student student);
    void deleteStudent(Long id);
}

Now create the implementation StudentServiceImpl.java:

package com.example.studentportal.service;

import com.example.studentportal.model.Student;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicLong;

@Service
public class StudentServiceImpl implements StudentService {

    // In-memory database using a Map
    private final Map<Long, Student> studentDb = new HashMap<>();
    private final AtomicLong idCounter = new AtomicLong();

    public StudentServiceImpl() {
        // Add some sample data
        Student student1 = new Student(idCounter.incrementAndGet(), "John Doe", 21,
                                      "[email protected]", "Computer Science", "CS123456");
        Student student2 = new Student(idCounter.incrementAndGet(), "Jane Smith", 22,
                                      "[email protected]", "Mathematics", "MT789012");
        Student student3 = new Student(idCounter.incrementAndGet(), "Bob Johnson", 20,
                                      "[email protected]", "Physics", "PH345678");

        studentDb.put(student1.getId(), student1);
        studentDb.put(student2.getId(), student2);
        studentDb.put(student3.getId(), student3);
    }

    @Override
    public List<Student> getAllStudents() {
        return new ArrayList<>(studentDb.values());
    }

    @Override
    public Optional<Student> getStudentById(Long id) {
        return Optional.ofNullable(studentDb.get(id));
    }

    @Override
    public Student saveStudent(Student student) {
        if (student.getId() == null) {
            // New student
            student.setId(idCounter.incrementAndGet());
        }
        studentDb.put(student.getId(), student);
        return student;
    }

    @Override
    public void deleteStudent(Long id) {
        studentDb.remove(id);
    }
}

Step 4: Create the Controller

Create a new package com.example.studentportal.controller and add the StudentController.java class:

package com.example.studentportal.controller;

import com.example.studentportal.model.Student;
import com.example.studentportal.service.StudentService;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

@Controller
public class StudentController {

    private final StudentService studentService;

    @Autowired
    public StudentController(StudentService studentService) {
        this.studentService = studentService;
    }

    @GetMapping("/")
    public String home(Model model) {
        model.addAttribute("students", studentService.getAllStudents());
        return "index";
    }

    @GetMapping("/students")
    public String listStudents(Model model) {
        model.addAttribute("students", studentService.getAllStudents());
        return "students/list";
    }

    @GetMapping("/students/{id}")
    public String viewStudent(@PathVariable Long id, Model model) {
        Student student = studentService.getStudentById(id)
                           .orElseThrow(() -> new IllegalArgumentException("Invalid student ID: " + id));
        model.addAttribute("student", student);
        return "students/view";
    }

    @GetMapping("/students/new")
    public String showNewStudentForm(Model model) {
        model.addAttribute("student", new Student());
        return "students/form";
    }

    @PostMapping("/students/save")
    public String saveStudent(@Valid @ModelAttribute("student") Student student,
                             BindingResult result, RedirectAttributes redirectAttributes) {
        if (result.hasErrors()) {
            return "students/form";
        }

        studentService.saveStudent(student);
        redirectAttributes.addFlashAttribute("successMessage", "Student saved successfully!");
        return "redirect:/students";
    }

    @GetMapping("/students/edit/{id}")
    public String showEditForm(@PathVariable Long id, Model model) {
        Student student = studentService.getStudentById(id)
                           .orElseThrow(() -> new IllegalArgumentException("Invalid student ID: " + id));
        model.addAttribute("student", student);
        return "students/form";
    }

    @GetMapping("/students/delete/{id}")
    public String deleteStudent(@PathVariable Long id, RedirectAttributes redirectAttributes) {
        studentService.deleteStudent(id);
        redirectAttributes.addFlashAttribute("successMessage", "Student deleted successfully!");
        return "redirect:/students";
    }
}

Step 5: Create Thymeleaf Templates

Create the following directory structure in src/main/resources/templates/:

templates/
├── fragments/
│   ├── header.html
│   ├── footer.html
│   └── layout.html
├── students/
│   ├── form.html
│   ├── list.html
│   └── view.html
└── index.html
  1. Create fragments/header.html:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
  <head>
    <meta charset="UTF-8" />
  </head>
  <body>
    <div th:fragment="header">
      <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
        <div class="container">
          <a class="navbar-brand" th:href="@{/}">Student Portal</a>
          <button
            class="navbar-toggler"
            type="button"
            data-bs-toggle="collapse"
            data-bs-target="#navbarNav"
          >
            <span class="navbar-toggler-icon"></span>
          </button>
          <div class="collapse navbar-collapse" id="navbarNav">
            <ul class="navbar-nav ms-auto">
              <li class="nav-item">
                <a class="nav-link" th:href="@{/}">Home</a>
              </li>
              <li class="nav-item">
                <a class="nav-link" th:href="@{/students}">Students</a>
              </li>
              <li class="nav-item">
                <a class="nav-link" th:href="@{/students/new}">Add Student</a>
              </li>
            </ul>
          </div>
        </div>
      </nav>
    </div>
  </body>
</html>
  1. Create fragments/footer.html:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
  <head>
    <meta charset="UTF-8" />
  </head>
  <body>
    <div th:fragment="footer">
      <footer class="footer mt-auto py-3 bg-light">
        <div class="container text-center">
          <span class="text-muted">Student Portal © 2025</span>
        </div>
      </footer>
    </div>
  </body>
</html>
  1. Create fragments/layout.html:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" th:fragment="layout (title, content)">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title th:text="${title}">Student Portal</title>
    <link
      href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css"
      rel="stylesheet"
    />
    <link rel="stylesheet" th:href="@{/css/styles.css}" />
  </head>
  <body class="d-flex flex-column min-vh-100">
    <div th:replace="fragments/header :: header"></div>

    <div class="container my-4">
      <div
        th:if="${successMessage}"
        class="alert alert-success alert-dismissible fade show"
        role="alert"
      >
        <span th:text="${successMessage}"></span>
        <button
          type="button"
          class="btn-close"
          data-bs-dismiss="alert"
          aria-label="Close"
        ></button>
      </div>

      <div th:replace="${content}"></div>
    </div>

    <div th:replace="fragments/footer :: footer"></div>

    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
    <script th:src="@{/js/scripts.js}"></script>
  </body>
</html>
  1. Create index.html:
<!DOCTYPE html>
<html
  xmlns:th="http://www.thymeleaf.org"
  th:replace="fragments/layout :: layout(~{::title}, ~{::main})"
>
  <head>
    <title>Home - Student Portal</title>
  </head>
  <body>
    <main>
      <div class="jumbotron">
        <h1 class="display-4">Welcome to Student Portal</h1>
        <p class="lead">A platform to manage student information.</p>
        <hr class="my-4" />
        <p>You can view, add, edit, and delete student records.</p>
        <a class="btn btn-primary btn-lg" th:href="@{/students}" role="button"
          >View Students</a
        >
      </div>

      <div class="row mt-5">
        <div class="col-md-4 mb-4">
          <div class="card">
            <div class="card-body">
              <h5 class="card-title">Total Students</h5>
              <p class="card-text display-4" th:text="${students.size()}">0</p>
              <a th:href="@{/students}" class="card-link">View All</a>
            </div>
          </div>
        </div>
        <div class="col-md-4 mb-4">
          <div class="card">
            <div class="card-body">
              <h5 class="card-title">Quick Actions</h5>
              <a th:href="@{/students/new}" class="btn btn-success"
                >Add New Student</a
              >
            </div>
          </div>
        </div>
        <div class="col-md-4 mb-4">
          <div class="card">
            <div class="card-body">
              <h5 class="card-title">Recent Students</h5>
              <ul class="list-group list-group-flush">
                <li
                  class="list-group-item"
                  th:each="student, stat : ${students}"
                  th:if="${stat.index < 3}"
                >
                  <a
                    th:href="@{/students/{id}(id=${student.id})}"
                    th:text="${student.name}"
                    >Student Name</a
                  >
                </li>
              </ul>
            </div>
          </div>
        </div>
      </div>
    </main>
  </body>
</html>
  1. Create students/list.html:
<!DOCTYPE html>
<html
  xmlns:th="http://www.thymeleaf.org"
  th:replace="fragments/layout :: layout(~{::title}, ~{::main})"
>
  <head>
    <title>Students - Student Portal</title>
  </head>
  <body>
    <main>
      <h2>Students</h2>
      <a th:href="@{/students/new}" class="btn btn-primary mb-3"
        >Add New Student</a
      >

      <div th:if="${#lists.isEmpty(students)}" class="alert alert-info">
        No students found. Please add a student.
      </div>

      <div th:unless="${#lists.isEmpty(students)}">
        <table class="table table-striped table-hover">
          <thead class="table-dark">
            <tr>
              <th>ID</th>
              <th>Name</th>
              <th>Age</th>
              <th>Email</th>
              <th>Course</th>
              <th>Student ID</th>
              <th>Actions</th>
            </tr>
          </thead>
          <tbody>
            <tr th:each="student : ${students}">
              <td th:text="${student.id}">1</td>
              <td th:text="${student.name}">John Doe</td>
              <td th:text="${student.age}">21</td>
              <td th:text="${student.email}">[email protected]</td>
              <td th:text="${student.course}">Computer Science</td>
              <td th:text="${student.studentId}">CS123456</td>
              <td>
                <a
                  th:href="@{/students/{id}(id=${student.id})}"
                  class="btn btn-sm btn-info"
                  >View</a
                >
                <a
                  th:href="@{/students/edit/{id}(id=${student.id})}"
                  class="btn btn-sm btn-warning"
                  >Edit</a
                >
                <a
                  th:href="@{/students/delete/{id}(id=${student.id})}"
                  class="btn btn-sm btn-danger"
                  onclick="return confirm('Are you sure you want to delete this student?');"
                  >Delete</a
                >
              </td>
            </tr>
          </tbody>
        </table>
      </div>
    </main>
  </body>
</html>
  1. Create students/view.html:
<!DOCTYPE html>
<html
  xmlns:th="http://www.thymeleaf.org"
  th:replace="fragments/layout :: layout(~{::title}, ~{::main})"
>
  <head>
    <title th:text="${'Student - ' + student.name}">Student Details</title>
  </head>
  <body>
    <main>
      <div class="d-flex justify-content-between align-items-center mb-4">
        <h2>Student Details</h2>
        <div>
          <a th:href="@{/students}" class="btn btn-secondary">Back to List</a>
          <a
            th:href="@{/students/edit/{id}(id=${student.id})}"
            class="btn btn-warning"
            >Edit</a
          >
        </div>
      </div>

      <div class="card">
        <div class="card-header bg-primary text-white">
          <h4 th:text="${student.name}" class="mb-0">John Doe</h4>
        </div>
        <div class="card-body">
          <div class="row">
            <div class="col-md-6">
              <p><strong>ID:</strong> <span th:text="${student.id}">1</span></p>
              <p>
                <strong>Name:</strong>
                <span th:text="${student.name}">John Doe</span>
              </p>
              <p>
                <strong>Age:</strong> <span th:text="${student.age}">21</span>
              </p>
            </div>
            <div class="col-md-6">
              <p>
                <strong>Email:</strong>
                <span th:text="${student.email}">[email protected]</span>
              </p>
              <p>
                <strong>Course:</strong>
                <span th:text="${student.course}">Computer Science</span>
              </p>
              <p>
                <strong>Student ID:</strong>
                <span th:text="${student.studentId}">CS123456</span>
              </p>
            </div>
          </div>
        </div>
      </div>
    </main>
  </body>
</html>
  1. Create students/form.html:
<!DOCTYPE html>
<html
  xmlns:th="http://www.thymeleaf.org"
  th:replace="fragments/layout :: layout(~{::title}, ~{::main})"
>
  <head>
    <title th:text="${student.id == null ? 'Add Student' : 'Edit Student'}">
      Student Form
    </title>
  </head>
  <body>
    <main>
      <h2 th:text="${student.id == null ? 'Add New Student' : 'Edit Student'}">
        Student Form
      </h2>
      <a th:href="@{/students}" class="btn btn-secondary mb-3">Back to List</a>

      <div class="card">
        <div class="card-body">
          <form
            th:action="@{/students/save}"
            th:object="${student}"
            method="post"
          >
            <input type="hidden" th:field="*{id}" />

            <div class="mb-3">
              <label for="name" class="form-label">Name</label>
              <input
                type="text"
                class="form-control"
                th:field="*{name}"
                id="name"
              />
              <div
                class="text-danger"
                th:if="${#fields.hasErrors('name')}"
                th:errors="*{name}"
              >
                Name Error
              </div>
            </div>

            <div class="mb-3">
              <label for="age" class="form-label">Age</label>
              <input
                type="number"
                class="form-control"
                th:field="*{age}"
                id="age"
              />
              <div
                class="text-danger"
                th:if="${#fields.hasErrors('age')}"
                th:errors="*{age}"
              >
                Age Error
              </div>
            </div>

            <div class="mb-3">
              <label for="email" class="form-label">Email</label>
              <input
                type="email"
                class="form-control"
                th:field="*{email}"
                id="email"
              />
              <div
                class="text-danger"
                th:if="${#fields.hasErrors('email')}"
                th:errors="*{email}"
              >
                Email Error
              </div>
            </div>

            <div class="mb-3">
              <label for="course" class="form-label">Course</label>
              <select class="form-select" th:field="*{course}" id="course">
                <option value="">Select a course</option>
                <option value="Computer Science">Computer Science</option>
                <option value="Mathematics">Mathematics</option>
                <option value="Physics">Physics</option>
                <option value="Chemistry">Chemistry</option>
                <option value="Biology">Biology</option>
                <option value="Engineering">Engineering</option>
              </select>
              <div
                class="text-danger"
                th:if="${#fields.hasErrors('course')}"
                th:errors="*{course}"
              >
                Course Error
              </div>
            </div>

            <div class="mb-3">
              <label for="studentId" class="form-label">Student ID</label>
              <input
                type="text"
                class="form-control"
                th:field="*{studentId}"
                id="studentId"
                placeholder="e.g., CS123456"
              />
              <div class="text-small text-muted">
                Must be 8 characters with uppercase letters and numbers only
              </div>
              <div
                class="text-danger"
                th:if="${#fields.hasErrors('studentId')}"
                th:errors="*{studentId}"
              >
                Student ID Error
              </div>
            </div>

            <button type="submit" class="btn btn-primary">Save</button>
            <a th:href="@{/students}" class="btn btn-outline-secondary"
              >Cancel</a
            >
          </form>
        </div>
      </div>
    </main>
  </body>
</html>

Step 6: Add Static Resources

Create the following directory structure in src/main/resources/static/:

static/
├── css/
│   └── styles.css
└── js/
    └── scripts.js
  1. Create css/styles.css:
/* Custom styles for Student Portal */

body {
  padding-bottom: 60px;
}

.jumbotron {
  padding: 2rem 1rem;
  margin-bottom: 2rem;
  background-color: #e9ecef;
  border-radius: 0.3rem;
}

.card {
  box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
  margin-bottom: 1.5rem;
}

.card-title {
  color: #495057;
  font-weight: 600;
}

.footer {
  margin-top: auto;
}
  1. Create js/scripts.js:
// Custom JavaScript for Student Portal

document.addEventListener("DOMContentLoaded", function () {
  // Auto-hide alerts after 5 seconds
  setTimeout(function () {
    const alerts = document.querySelectorAll(".alert");
    alerts.forEach(function (alert) {
      const bsAlert = new bootstrap.Alert(alert);
      bsAlert.close();
    });
  }, 5000);

  // Confirm delete actions
  const deleteLinks = document.querySelectorAll('a[href*="/delete/"]');
  deleteLinks.forEach(function (link) {
    link.addEventListener("click", function (event) {
      if (!confirm("Are you sure you want to delete this item?")) {
        event.preventDefault();
      }
    });
  });
});

Step 7: Create an Exception Handler

Create a new package com.example.studentportal.exception and add the GlobalExceptionHandler.java class:

package com.example.studentportal.exception;

import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(IllegalArgumentException.class)
    public String handleIllegalArgumentException(IllegalArgumentException ex, RedirectAttributes redirectAttributes) {
        redirectAttributes.addFlashAttribute("errorMessage", ex.getMessage());
        return "redirect:/students";
    }

    @ExceptionHandler(Exception.class)
    public String handleGenericException(Exception ex, Model model) {
        model.addAttribute("errorMessage", "An unexpected error occurred: " + ex.getMessage());
        return "error";
    }
}

Step 8: Create a Custom Error Page

Create error.html in the templates directory:

<!DOCTYPE html>
<html
  xmlns:th="http://www.thymeleaf.org"
  th:replace="fragments/layout :: layout(~{::title}, ~{::main})"
>
  <head>
    <title>Error - Student Portal</title>
  </head>
  <body>
    <main>
      <div class="text-center">
        <h1 class="display-1">Oops!</h1>
        <p class="lead">Something went wrong.</p>
        <div
          class="alert alert-danger"
          th:if="${errorMessage}"
          th:text="${errorMessage}"
        >
          Error message goes here
        </div>
        <div class="my-4">
          <a th:href="@{/}" class="btn btn-primary">Go Home</a>
          <a th:href="@{/students}" class="btn btn-secondary">View Students</a>
        </div>
      </div>
    </main>
  </body>
</html>

Step 9: Configure Spring Boot Application Properties

Update the application.properties file in src/main/resources/:

# Server configuration
server.port=8080

# Thymeleaf configuration
spring.thymeleaf.cache=false
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
spring.thymeleaf.mode=HTML

# Logging configuration
logging.level.org.springframework.web=INFO
logging.level.com.example.studentportal=DEBUG

# Static resources configuration
spring.mvc.static-path-pattern=/static/**

# Server error handling
server.error.include-message=always
server.error.include-binding-errors=always

Step 10: Run the Application

  1. Start the application:
  • From IDE: Run the main application class
  • With Maven: mvn spring-boot:run
  • With Gradle: ./gradlew bootRun
  1. Open a web browser and navigate to:
  • http://localhost:8080/
  1. Test the application by:
  • Viewing the student list
  • Adding a new student
  • Editing an existing student
  • Viewing student details
  • Deleting a student

Step 11: Enhance the Application (To do)

Add these enhancements:

  1. Add student search functionality
  2. Implement sorting in the student list
  3. Add pagination for the student list
  4. Implement a dark mode toggle with JavaScript
  5. Add form validation on the client side using JavaScript

Step 12: Documentation and Reflection

Write down answers to the following questions:

  1. How does Thymeleaf help in creating dynamic web content?
  2. What is the purpose of the @Valid annotation in form handling?
  3. How does the flash attribute mechanism work in Spring MVC?
  4. Explain the layered architecture used in this application.
  5. How would you extend this application to use a real database instead of in-memory storage?

This module has taught you how to build a complete web application using Spring Boot and Thymeleaf. You’ve learned about:

  1. Spring MVC architecture and request flow
  2. Controller annotations and handler methods
  3. Creating and using models
  4. Thymeleaf templating syntax and features
  5. Form handling and validation
  6. Static resource management
  7. Layout and fragment reuse with Thymeleaf
  8. Error handling and exception management

By Wahid Hamdi