Student Management System in Spring Boot

Build a full-stack student management app with Spring Security and Tailwind CSS.

Introduction

30 Second Summary

Every school, university, and training program needs a way to keep track of who is enrolled in what. Without a proper system, managing students, courses, and enrollments becomes a mess of spreadsheets and manual updates.

In this project, you will build a full-stack Student Management System with a Spring Boot REST API backend and an HTML/JavaScript frontend styled with Tailwind CSS. You will wire up role-based security so that admins, teachers, and students each see only what they are allowed to access.

What You'll Build

A Spring Boot REST API with JPA entities, DTOs, and role-based access control, paired with a Tailwind CSS frontend that authenticates users and displays data based on their role.

By the end of this project, you'll have:

  • A working REST API with Student, Course, and Enrollment entities connected to MySQL.
  • Role-based access control (ADMIN, TEACHER, STUDENT) protecting your API endpoints with Spring Security.
  • A responsive frontend dashboard that calls the API with Basic Authentication and shows different views depending on the logged-in user's role.
  • Secret Mission: Add a search and filter feature to find students by name or department.

Are there any prerequisites?

This project assumes you have experience with Java and Spring Boot basics. You also need JDK 17, Maven, IntelliJ IDEA Community Edition, and a local MySQL Server already installed and running on your machine.

Before We Start

Before We Start...

Set Up the Spring Boot Project

Every Spring Boot application starts from a project skeleton. You need the right dependencies, a database connection, and a clean package structure before you can write any application logic.

In this step, you will generate a Spring Boot project using Spring Initializr, connect it to a local MySQL database, and organize your code into packages that follow a layered architecture pattern.

In this step, get ready to:
  • Create a Spring Boot project with all required dependencies.
  • Create the MySQL database and verify the application starts.
  • Set up the project package structure.
Create the Spring Boot project with Spring Initializr

Spring Initializr is a web tool that generates a ready-to-use Spring Boot project with your chosen dependencies already wired into the build file.

  • Open your browser and navigate to start.spring.io.
  • Set **Project** to Maven.
  • Set Language to Java.
  • Under Project Metadata, set Group to com.studentms.
  • Set Artifact to student-management.
  • Set Java version to 17.
  • Click Add Dependencies and add the following five dependencies: Spring Web, Spring Data JPA, Spring Security, Lombok, and MySQL Driver.
  • Click Generate to download a ZIP file.
  • Extract the ZIP to a folder on your computer.

What is Spring Initializr?

Spring Initializr at start.spring.io generates a complete Maven or Gradle project with your chosen dependencies already declared in the build file. It saves you from manually writing a pom.xml from scratch.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type IntelliJ and press Enter to open it.
  • In IntelliJ, click Open on the welcome screen.
  • Navigate to the extracted student-management folder and click OK.
  • IntelliJ will detect the pom.xml and import the project as a Maven project automatically.

Now configure the database connection. Spring Boot reads its configuration from application.properties.

  • In the IntelliJ Project panel on the left, expand src/main/resources.
  • Open application.properties and replace its contents with the following configuration:
spring.datasource.url=jdbc:mysql://localhost:3306/student_management_db
spring.datasource.username=root
spring.datasource.password=your-mysql-password-here
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
  • Replace your-mysql-password-here with your actual MySQL root password.

What does this configuration do?

The spring.datasource.url property tells Spring Boot where your MySQL database lives. The JDBC URL format is jdbc:mysql://host:port/database_name.

Setting spring.jpa.hibernate.ddl-auto=update tells Hibernate to automatically create or modify database tables to match your Java entities. The spring.jpa.show-sql=true property prints all SQL queries to the console so you can see what Hibernate is doing.

  • Save application.properties.

Not sure what your MySQL password is?

This is the password you set when you installed MySQL Server. If you never set one, try leaving the value empty (just spring.datasource.password= with nothing after the equals sign).

Help me figure out my MySQL root password.

Create the MySQL database and verify the application starts

Your Spring Boot application needs the database to exist before it can connect. You will create it using the MySQL command line client.

  • Open a terminal and connect to MySQL by running:
mysql -u root -p

You will be prompted to enter your MySQL root password.

  • Create the database by running this SQL statement:
CREATE DATABASE student_management_db;
  • Exit the MySQL client by typing:
exit;

What is this database for?

The student_management_db database will store all your application data. Spring Boot's JPA integration will automatically create tables inside this database based on the Java entity classes you define in the next step.

Now verify the Spring Boot application starts successfully.

  • In your terminal, navigate to the student-management project folder.
  • Start the application by running:
mvn spring-boot:run

What should I see?

Spring Security auto-generates a password on startup. You should see a line in the console that says "Using generated security password:" followed by a random string. You should also see "Started StudentManagementApplication" near the bottom of the output. This confirms the application connected to MySQL and started successfully.

  • Press Ctrl+C in the terminal to stop the application.

Seeing a connection refused error?

Make sure your MySQL Server is running. On macOS, check System Preferences > MySQL. On Windows, open Services and look for the MySQL service.

Also verify the database name in application.properties matches exactly what you created: student_management_db.

Help me fix my Spring Boot MySQL connection error.

Set up the project package structure

A layered architecture keeps your code organized by responsibility. Each layer has a clear job: entities define data, DTOs shape what crosses the API boundary, repositories handle database access, services contain business logic, controllers expose endpoints, and config holds infrastructure settings.

  • In IntelliJ's Project panel, expand src/main/java/com/studentms.
  • Right-click the studentms package, select New > Package.
  • Type entity and press Enter.
  • Repeat the same process to create five more packages: dto, repository, service, controller, and config.

Why separate packages?

Each package represents a layer in the application. This pattern makes it easy to find code by its purpose. For example, all database-related interfaces live in repository, and all request/response shapes live in dto.

As your application grows, this structure prevents the codebase from becoming a tangled mess of files in a single directory.

You should now see six sub-packages under com.studentms in the Project panel.

✔️ Awesome, I've got everything!

Great. Double check you've saved application.properties and all six packages are visible in the project tree.

ⓧ I'd like to double check the full code

Here is the complete application.properties file:

spring.datasource.url=jdbc:mysql://localhost:3306/student_management_db
spring.datasource.username=root
spring.datasource.password=your-mysql-password-here
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true

Your project is fully set up with a working database connection and a clean package structure. Next up, you will build the JPA entities and DTOs that define how your data is stored and transferred.

Build Entities and DTOs

Your Spring Boot project is up and running with a MySQL connection ready to go. Now it is time to define the data model that powers the entire Student Management System.

Every REST API needs a clear data layer. In this step, you will create JPA entities that map directly to MySQL tables, DTOs that control what data flows in and out of the API, and repository interfaces that handle database queries.

In this step, get ready to:
  • Create three JPA entities (Student, Course, Enrollment) with Lombok annotations.
  • Create seven DTO classes for request and response patterns.
  • Create three JPA repository interfaces for database access.
Create the JPA entities

JPA entities are Java classes that map directly to database tables. Each field becomes a column, and Lombok annotations eliminate the boilerplate getters, setters, and constructors.

  • In IntelliJ's Project tool window on the left, right-click the entity package under src/main/java/com/studentms.
  • Select New then Java Class.
  • Name it Student and press Enter.
  • Replace the entire file content with the following code:
package com.studentms.entity;

import jakarta.persistence.*;
import lombok.*;

@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Student {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String name;

    @Column(nullable = false, unique = true)
    private String email;

    private String department;
}

What does this code do?

The @Entity annotation tells JPA to create a student table in MySQL based on this class.

  • @Data (Lombok) generates getters, setters, toString(), equals(), and hashCode() automatically.
  • @NoArgsConstructor and @AllArgsConstructor generate empty and full constructors.
  • @Builder gives you a fluent builder pattern for creating Student objects.
  • @Id with @GeneratedValue(strategy = GenerationType.IDENTITY) makes MySQL auto-increment the primary key.
  • @Column(nullable = false, unique = true) on email prevents duplicate or null email entries at the database level.
  • Save Student.java.
  • Create another Java class in the entity package named Course by right-clicking the package, selecting New then Java Class.
  • Replace the entire file content with:
package com.studentms.entity;

import jakarta.persistence.*;
import lombok.*;

@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Course {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String courseName;

    @Column(nullable = false, unique = true)
    private String courseCode;

    private Integer credits;
}

What does this code do?

The Course entity follows the same pattern as Student. The courseCode field is marked unique = true so no two courses can share the same code (like CS101).

  • Save Course.java.
  • Create one more Java class in the entity package named Enrollment.
  • Replace the entire file content with:
package com.studentms.entity;

import jakarta.persistence.*;
import lombok.*;
import java.time.LocalDate;

@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Enrollment {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne
    @JoinColumn(name = "student_id", nullable = false)
    private Student student;

    @ManyToOne
    @JoinColumn(name = "course_id", nullable = false)
    private Course course;

    private LocalDate enrollmentDate;
}

What is @ManyToOne?

The @ManyToOne annotation defines a relationship where many enrollments can belong to one student, and many enrollments can belong to one course. JPA creates foreign key columns (student_id and course_id) in the enrollment table automatically.

  • Save Enrollment.java.
  • Run the application using mvn spring-boot:run in the IntelliJ terminal. You should see Hibernate SQL output creating three tables (student, course, enrollment) in your console log.

Application won't start?

If you see errors about missing imports, make sure your import jakarta.persistence.* and import lombok.* lines are present. IntelliJ sometimes needs a moment to resolve dependencies. Try clicking File then Invalidate Caches / Restart if imports stay red.

help me fix my Spring Boot entity errors

✔️ Awesome, I've got everything!

Great. Make sure all three entity files are saved before moving on.

ⓧ I'd like to double check the full code

package com.studentms.entity;

import jakarta.persistence.*;
import lombok.*;

@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Student {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String name;

    @Column(nullable = false, unique = true)
    private String email;

    private String department;
}
package com.studentms.entity;

import jakarta.persistence.*;
import lombok.*;

@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Course {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String courseName;

    @Column(nullable = false, unique = true)
    private String courseCode;

    private Integer credits;
}
package com.studentms.entity;

import jakarta.persistence.*;
import lombok.*;
import java.time.LocalDate;

@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Enrollment {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne
    @JoinColumn(name = "student_id", nullable = false)
    private Student student;

    @ManyToOne
    @JoinColumn(name = "course_id", nullable = false)
    private Course course;

    private LocalDate enrollmentDate;
}
Create the DTO classes

DTOs (Data Transfer Objects) separate your internal entity structure from what the API exposes to clients. You will create request DTOs for incoming data and response DTOs for outgoing data.

Why use DTOs instead of entities directly?

Exposing entities directly means clients could send unexpected fields (like overriding the id) or see internal relationship details they should not. DTOs give you precise control over the shape of request and response data.

  • Right-click the dto package under src/main/java/com/studentms, select New then Java Class, and name it CreateStudentRequest.
  • Replace the file content with:
package com.studentms.dto;

import lombok.Data;

@Data
public class CreateStudentRequest {
    private String name;
    private String email;
    private String department;
}
  • Create another class in the dto package named UpdateStudentRequest with this content:
package com.studentms.dto;

import lombok.Data;

@Data
public class UpdateStudentRequest {
    private String name;
    private String email;
    private String department;
}
  • Create StudentResponse in the dto package with this content:
package com.studentms.dto;

import lombok.Builder;
import lombok.Data;

@Data
@Builder
public class StudentResponse {
    private Long id;
    private String name;
    private String email;
    private String department;
}

Why does StudentResponse have @Builder?

Response DTOs use @Builder because your service layer will construct them from entity data. The builder pattern makes this conversion readable and less error-prone. Request DTOs skip @Builder because Spring deserializes JSON into them automatically.

  • Create CreateCourseRequest in the dto package:
package com.studentms.dto;

import lombok.Data;

@Data
public class CreateCourseRequest {
    private String courseName;
    private String courseCode;
    private Integer credits;
}
  • Create CourseResponse in the dto package:
package com.studentms.dto;

import lombok.Builder;
import lombok.Data;

@Data
@Builder
public class CourseResponse {
    private Long id;
    private String courseName;
    private String courseCode;
    private Integer credits;
}
  • Create EnrollmentRequest in the dto package:
package com.studentms.dto;

import lombok.Data;

@Data
public class EnrollmentRequest {
    private Long studentId;
    private Long courseId;
}
  • Create EnrollmentResponse in the dto package:
package com.studentms.dto;

import lombok.Builder;
import lombok.Data;

@Data
@Builder
public class EnrollmentResponse {
    private Long id;
    private String studentName;
    private String courseName;
    private String enrollmentDate;
}

What does EnrollmentResponse look like?

Notice that EnrollmentResponse uses studentName and courseName as strings instead of full Student and Course objects. This keeps the API response flat and avoids circular references or exposing unnecessary data.

  • Save all seven DTO files.
  • Verify in the Project tool window that the dto package now contains: CreateStudentRequest, UpdateStudentRequest, StudentResponse, CreateCourseRequest, CourseResponse, EnrollmentRequest, and EnrollmentResponse.

Seeing red squiggles on Lombok annotations?

IntelliJ requires the Lombok plugin to recognize annotations like @Data and @Builder. Go to File then Settings (or IntelliJ IDEA then Preferences on macOS), search for Plugins, and make sure Lombok is installed and enabled.

help me fix Lombok not being recognized in IntelliJ

✔️ Awesome, I've got everything!

Great. Make sure all seven DTO files are saved.

ⓧ I'd like to double check the full code

package com.studentms.dto;

import lombok.Data;

@Data
public class CreateStudentRequest {
    private String name;
    private String email;
    private String department;
}
package com.studentms.dto;

import lombok.Data;

@Data
public class UpdateStudentRequest {
    private String name;
    private String email;
    private String department;
}
package com.studentms.dto;

import lombok.Builder;
import lombok.Data;

@Data
@Builder
public class StudentResponse {
    private Long id;
    private String name;
    private String email;
    private String department;
}
package com.studentms.dto;

import lombok.Data;

@Data
public class CreateCourseRequest {
    private String courseName;
    private String courseCode;
    private Integer credits;
}
package com.studentms.dto;

import lombok.Builder;
import lombok.Data;

@Data
@Builder
public class CourseResponse {
    private Long id;
    private String courseName;
    private String courseCode;
    private Integer credits;
}
package com.studentms.dto;

import lombok.Data;

@Data
public class EnrollmentRequest {
    private Long studentId;
    private Long courseId;
}
package com.studentms.dto;

import lombok.Builder;
import lombok.Data;

@Data
@Builder
public class EnrollmentResponse {
    private Long id;
    private String studentName;
    private String courseName;
    private String enrollmentDate;
}
Create the JPA repositories

Spring Data JPA repositories give you full CRUD operations without writing any implementation code. You just declare an interface that extends JpaRepository and Spring generates the implementation at runtime.

  • Right-click the repository package, select New then Java Class.
  • Change the type dropdown from Class to Interface, name it StudentRepository, and press Enter.
  • Replace the file content with:
package com.studentms.repository;

import com.studentms.entity.Student;
import org.springframework.data.jpa.repository.JpaRepository;

public interface StudentRepository extends JpaRepository<Student, Long> {
}
  • Create another interface in the repository package named CourseRepository:
package com.studentms.repository;

import com.studentms.entity.Course;
import org.springframework.data.jpa.repository.JpaRepository;

public interface CourseRepository extends JpaRepository<Course, Long> {
}
  • Create one more interface named EnrollmentRepository:
package com.studentms.repository;

import com.studentms.entity.Enrollment;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;

public interface EnrollmentRepository extends JpaRepository<Enrollment, Long> {
    List<Enrollment> findByStudentId(Long studentId);
}

What is findByStudentId?

Spring Data JPA derives query methods from method names. The method findByStudentId(Long studentId) automatically generates a SQL query that filters enrollments by the student_id foreign key column. No implementation code needed.

  • Save all three repository files.
  • Run mvn spring-boot:run again in the terminal. The application should start successfully without errors.

Seeing 'cannot find symbol' errors?

Make sure your import statements match the entity package exactly: import com.studentms.entity.Student, import com.studentms.entity.Course, and import com.studentms.entity.Enrollment. A mismatch in the package path causes compile errors.

help me fix my repository import errors

✔️ Awesome, I've got everything!

Great. Make sure all three repository files are saved.

ⓧ I'd like to double check the full code

package com.studentms.repository;

import com.studentms.entity.Student;
import org.springframework.data.jpa.repository.JpaRepository;

public interface StudentRepository extends JpaRepository<Student, Long> {
}
package com.studentms.repository;

import com.studentms.entity.Course;
import org.springframework.data.jpa.repository.JpaRepository;

public interface CourseRepository extends JpaRepository<Course, Long> {
}
package com.studentms.repository;

import com.studentms.entity.Enrollment;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;

public interface EnrollmentRepository extends JpaRepository<Enrollment, Long> {
    List<Enrollment> findByStudentId(Long studentId);
}

Your data layer is now complete with entities, DTOs, and repositories. Next up, you will build the service classes and REST controllers that wire everything together into a working API.

Build Service Classes and REST Controllers

You have your data layer set up with entities, DTOs, and repositories. Now you need the business logic that ties everything together.

In a layered Spring Boot application, service classes handle the "how" of your operations. Controllers handle the "where" by mapping HTTP requests to those service methods. In this step, you will build both layers and expose your student management system as a REST API.

In this step, get ready to:
  • Create three service classes with CRUD logic and DTO mapping.
  • Create three REST controllers that expose HTTP endpoints.
  • Test the endpoints to confirm they work before adding security.
Create the service classes

Service classes sit between your controllers and your repositories. They contain the business logic that transforms raw entity data into the DTOs your API returns.

Why concrete classes instead of interfaces?

Many Spring tutorials use an interface plus an implementation class (e.g. StudentService interface + StudentServiceImpl). That pattern shines when you have multiple implementations. For this project, each service has exactly one implementation, so a concrete class annotated with @Service is simpler and equally valid.

  • In IntelliJ, right-click the com.studentms.service package in the Project panel.
  • Select New, then Java Class.
  • Name it StudentService and paste the following code:
package com.studentms.service;

import com.studentms.dto.*;
import com.studentms.entity.Student;
import com.studentms.repository.StudentRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
public class StudentService {

    private final StudentRepository studentRepository;

    public List<StudentResponse> getAllStudents() {
        return studentRepository.findAll().stream()
                .map(this::toResponse)
                .collect(Collectors.toList());
    }

    public StudentResponse getStudentById(Long id) {
        Student student = studentRepository.findById(id)
                .orElseThrow(() -> new RuntimeException("Student not found with id: " + id));
        return toResponse(student);
    }

    public StudentResponse createStudent(CreateStudentRequest request) {
        // Build a Student entity from the incoming DTO
        Student student = Student.builder()
                .name(request.getName())
                .email(request.getEmail())
                .department(request.getDepartment())
                .build();
        Student saved = studentRepository.save(student);
        return toResponse(saved);
    }

    public StudentResponse updateStudent(Long id, UpdateStudentRequest request) {
        // Find the existing student or throw an error
        Student student = studentRepository.findById(id)
                .orElseThrow(() -> new RuntimeException("Student not found with id: " + id));
        student.setName(request.getName());
        student.setEmail(request.getEmail());
        student.setDepartment(request.getDepartment());
        Student updated = studentRepository.save(student);
        return toResponse(updated);
    }

    public void deleteStudent(Long id) {
        studentRepository.deleteById(id);
    }

    // Convert a Student entity to a StudentResponse DTO
    private StudentResponse toResponse(Student student) {
        return StudentResponse.builder()
                .id(student.getId())
                .name(student.getName())
                .email(student.getEmail())
                .department(student.getDepartment())
                .build();
    }
}

What does this code do?

@RequiredArgsConstructor is a Lombok annotation that generates a constructor for all final fields. Spring uses this constructor to inject the StudentRepository automatically.

Each public method accepts or returns a DTO (never the raw entity). The private toResponse() method handles the entity-to-DTO conversion so you don't repeat mapping logic across methods.

  • Save StudentService.java.
  • Create another class in the same service package named CourseService and paste the following code:
package com.studentms.service;

import com.studentms.dto.*;
import com.studentms.entity.Course;
import com.studentms.repository.CourseRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
public class CourseService {

    private final CourseRepository courseRepository;

    public List<CourseResponse> getAllCourses() {
        return courseRepository.findAll().stream()
                .map(this::toResponse)
                .collect(Collectors.toList());
    }

    public CourseResponse createCourse(CreateCourseRequest request) {
        // Build a Course entity from the incoming DTO
        Course course = Course.builder()
                .courseName(request.getCourseName())
                .courseCode(request.getCourseCode())
                .credits(request.getCredits())
                .build();
        Course saved = courseRepository.save(course);
        return toResponse(saved);
    }

    // Convert a Course entity to a CourseResponse DTO
    private CourseResponse toResponse(Course course) {
        return CourseResponse.builder()
                .id(course.getId())
                .courseName(course.getCourseName())
                .courseCode(course.getCourseCode())
                .credits(course.getCredits())
                .build();
    }
}

What does this code do?

CourseService follows the same pattern as StudentService but with fewer methods. It only needs getAllCourses() and createCourse() because the project scope doesn't require updating or deleting individual courses.

  • Save CourseService.java.
  • Create one more class in the service package named EnrollmentService and paste the following code:
package com.studentms.service;

import com.studentms.dto.*;
import com.studentms.entity.Course;
import com.studentms.entity.Enrollment;
import com.studentms.entity.Student;
import com.studentms.repository.CourseRepository;
import com.studentms.repository.EnrollmentRepository;
import com.studentms.repository.StudentRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.time.LocalDate;
import java.util.List;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
public class EnrollmentService {

    private final EnrollmentRepository enrollmentRepository;
    private final StudentRepository studentRepository;
    private final CourseRepository courseRepository;

    public List<EnrollmentResponse> getAllEnrollments() {
        return enrollmentRepository.findAll().stream()
                .map(this::toResponse)
                .collect(Collectors.toList());
    }

    public List<EnrollmentResponse> getEnrollmentsByStudentId(Long studentId) {
        return enrollmentRepository.findByStudentId(studentId).stream()
                .map(this::toResponse)
                .collect(Collectors.toList());
    }

    public EnrollmentResponse createEnrollment(EnrollmentRequest request) {
        // Look up the student and course, throw if not found
        Student student = studentRepository.findById(request.getStudentId())
                .orElseThrow(() -> new RuntimeException("Student not found with id: " + request.getStudentId()));
        Course course = courseRepository.findById(request.getCourseId())
                .orElseThrow(() -> new RuntimeException("Course not found with id: " + request.getCourseId()));

        // Build the enrollment with today's date
        Enrollment enrollment = Enrollment.builder()
                .student(student)
                .course(course)
                .enrollmentDate(LocalDate.now())
                .build();
        Enrollment saved = enrollmentRepository.save(enrollment);
        return toResponse(saved);
    }

    // Convert an Enrollment entity to an EnrollmentResponse DTO
    private EnrollmentResponse toResponse(Enrollment enrollment) {
        return EnrollmentResponse.builder()
                .id(enrollment.getId())
                .studentName(enrollment.getStudent().getName())
                .courseName(enrollment.getCourse().getCourseName())
                .enrollmentDate(enrollment.getEnrollmentDate().toString())
                .build();
    }
}

What does this code do?

EnrollmentService injects three repositories because creating an enrollment requires validating that both the student and the course exist first.

The toResponse() method pulls studentName and courseName from the related entities. This means the API response shows human-readable names instead of raw foreign key IDs.

  • Save EnrollmentService.java.

Seeing red underlines in IntelliJ?

If IntelliJ shows errors on the Lombok annotations, make sure annotation processing is enabled. Go to File > Settings > Build, Execution, Deployment > Compiler > Annotation Processors and check Enable annotation processing.

help me fix Lombok annotation errors in IntelliJ

✔️ Awesome, I've got everything!

Make sure all three service files are saved before moving on to the controllers.

ⓧ I'd like to double check the full code

Here is StudentService.java:

package com.studentms.service;

import com.studentms.dto.*;
import com.studentms.entity.Student;
import com.studentms.repository.StudentRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
public class StudentService {

    private final StudentRepository studentRepository;

    public List<StudentResponse> getAllStudents() {
        return studentRepository.findAll().stream()
                .map(this::toResponse)
                .collect(Collectors.toList());
    }

    public StudentResponse getStudentById(Long id) {
        Student student = studentRepository.findById(id)
                .orElseThrow(() -> new RuntimeException("Student not found with id: " + id));
        return toResponse(student);
    }

    public StudentResponse createStudent(CreateStudentRequest request) {
        Student student = Student.builder()
                .name(request.getName())
                .email(request.getEmail())
                .department(request.getDepartment())
                .build();
        Student saved = studentRepository.save(student);
        return toResponse(saved);
    }

    public StudentResponse updateStudent(Long id, UpdateStudentRequest request) {
        Student student = studentRepository.findById(id)
                .orElseThrow(() -> new RuntimeException("Student not found with id: " + id));
        student.setName(request.getName());
        student.setEmail(request.getEmail());
        student.setDepartment(request.getDepartment());
        Student updated = studentRepository.save(student);
        return toResponse(updated);
    }

    public void deleteStudent(Long id) {
        studentRepository.deleteById(id);
    }

    private StudentResponse toResponse(Student student) {
        return StudentResponse.builder()
                .id(student.getId())
                .name(student.getName())
                .email(student.getEmail())
                .department(student.getDepartment())
                .build();
    }
}

Here is CourseService.java:

package com.studentms.service;

import com.studentms.dto.*;
import com.studentms.entity.Course;
import com.studentms.repository.CourseRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
public class CourseService {

    private final CourseRepository courseRepository;

    public List<CourseResponse> getAllCourses() {
        return courseRepository.findAll().stream()
                .map(this::toResponse)
                .collect(Collectors.toList());
    }

    public CourseResponse createCourse(CreateCourseRequest request) {
        Course course = Course.builder()
                .courseName(request.getCourseName())
                .courseCode(request.getCourseCode())
                .credits(request.getCredits())
                .build();
        Course saved = courseRepository.save(course);
        return toResponse(saved);
    }

    private CourseResponse toResponse(Course course) {
        return CourseResponse.builder()
                .id(course.getId())
                .courseName(course.getCourseName())
                .courseCode(course.getCourseCode())
                .credits(course.getCredits())
                .build();
    }
}

Here is EnrollmentService.java:

package com.studentms.service;

import com.studentms.dto.*;
import com.studentms.entity.Course;
import com.studentms.entity.Enrollment;
import com.studentms.entity.Student;
import com.studentms.repository.CourseRepository;
import com.studentms.repository.EnrollmentRepository;
import com.studentms.repository.StudentRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.time.LocalDate;
import java.util.List;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
public class EnrollmentService {

    private final EnrollmentRepository enrollmentRepository;
    private final StudentRepository studentRepository;
    private final CourseRepository courseRepository;

    public List<EnrollmentResponse> getAllEnrollments() {
        return enrollmentRepository.findAll().stream()
                .map(this::toResponse)
                .collect(Collectors.toList());
    }

    public List<EnrollmentResponse> getEnrollmentsByStudentId(Long studentId) {
        return enrollmentRepository.findByStudentId(studentId).stream()
                .map(this::toResponse)
                .collect(Collectors.toList());
    }

    public EnrollmentResponse createEnrollment(EnrollmentRequest request) {
        Student student = studentRepository.findById(request.getStudentId())
                .orElseThrow(() -> new RuntimeException("Student not found with id: " + request.getStudentId()));
        Course course = courseRepository.findById(request.getCourseId())
                .orElseThrow(() -> new RuntimeException("Course not found with id: " + request.getCourseId()));

        Enrollment enrollment = Enrollment.builder()
                .student(student)
                .course(course)
                .enrollmentDate(LocalDate.now())
                .build();
        Enrollment saved = enrollmentRepository.save(enrollment);
        return toResponse(saved);
    }

    private EnrollmentResponse toResponse(Enrollment enrollment) {
        return EnrollmentResponse.builder()
                .id(enrollment.getId())
                .studentName(enrollment.getStudent().getName())
                .courseName(enrollment.getCourse().getCourseName())
                .enrollmentDate(enrollment.getEnrollmentDate().toString())
                .build();
    }
}
Create the REST controllers

Controllers are the entry point for HTTP requests. Each controller maps URL paths to service methods and returns JSON responses wrapped in ResponseEntity.

  • Right-click the com.studentms.controller package in the Project panel.
  • Select New, then Java Class.
  • Name it StudentController and paste the following code:
package com.studentms.controller;

import com.studentms.dto.*;
import com.studentms.service.StudentService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/students")
@RequiredArgsConstructor
public class StudentController {

    private final StudentService studentService;

    // GET /api/students - list all students
    @GetMapping
    public ResponseEntity<List<StudentResponse>> getAllStudents() {
        return ResponseEntity.ok(studentService.getAllStudents());
    }

    // GET /api/students/{id} - get one student by ID
    @GetMapping("/{id}")
    public ResponseEntity<StudentResponse> getStudentById(@PathVariable Long id) {
        return ResponseEntity.ok(studentService.getStudentById(id));
    }

    // POST /api/students - create a new student
    @PostMapping
    public ResponseEntity<StudentResponse> createStudent(@RequestBody CreateStudentRequest request) {
        return ResponseEntity.ok(studentService.createStudent(request));
    }

    // PUT /api/students/{id} - update an existing student
    @PutMapping("/{id}")
    public ResponseEntity<StudentResponse> updateStudent(@PathVariable Long id, @RequestBody UpdateStudentRequest request) {
        return ResponseEntity.ok(studentService.updateStudent(id, request));
    }

    // DELETE /api/students/{id} - delete a student
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteStudent(@PathVariable Long id) {
        studentService.deleteStudent(id);
        return ResponseEntity.noContent().build();
    }
}

What does this code do?

@RestController tells Spring this class handles HTTP requests and automatically serializes return values to JSON.

@RequestMapping("/api/students") sets the base URL path. Each method annotation (@GetMapping, @PostMapping, etc.) adds to that base path. For example, @GetMapping("/{id}") maps to GET /api/students/1.

  • Save StudentController.java.
  • Create another class in the controller package named CourseController and paste the following code:
package com.studentms.controller;

import com.studentms.dto.*;
import com.studentms.service.CourseService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/courses")
@RequiredArgsConstructor
public class CourseController {

    private final CourseService courseService;

    // GET /api/courses - list all courses
    @GetMapping
    public ResponseEntity<List<CourseResponse>> getAllCourses() {
        return ResponseEntity.ok(courseService.getAllCourses());
    }

    // POST /api/courses - create a new course
    @PostMapping
    public ResponseEntity<CourseResponse> createCourse(@RequestBody CreateCourseRequest request) {
        return ResponseEntity.ok(courseService.createCourse(request));
    }
}

What does this code do?

CourseController exposes just two endpoints: one to list all courses and one to create a new course. The pattern is identical to StudentController but scoped to the /api/courses path.

  • Save CourseController.java.
  • Create one more class in the controller package named EnrollmentController and paste the following code:
package com.studentms.controller;

import com.studentms.dto.*;
import com.studentms.service.EnrollmentService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/enrollments")
@RequiredArgsConstructor
public class EnrollmentController {

    private final EnrollmentService enrollmentService;

    // GET /api/enrollments - list all enrollments
    @GetMapping
    public ResponseEntity<List<EnrollmentResponse>> getAllEnrollments() {
        return ResponseEntity.ok(enrollmentService.getAllEnrollments());
    }

    // GET /api/enrollments/student/{studentId} - get enrollments for one student
    @GetMapping("/student/{studentId}")
    public ResponseEntity<List<EnrollmentResponse>> getEnrollmentsByStudent(@PathVariable Long studentId) {
        return ResponseEntity.ok(enrollmentService.getEnrollmentsByStudentId(studentId));
    }

    // POST /api/enrollments - enroll a student in a course
    @PostMapping
    public ResponseEntity<EnrollmentResponse> createEnrollment(@RequestBody EnrollmentRequest request) {
        return ResponseEntity.ok(enrollmentService.createEnrollment(request));
    }
}

What does this code do?

EnrollmentController has a special endpoint at /student/{studentId} that filters enrollments by student. This lets the frontend show a specific student's enrolled courses without loading every enrollment in the system.

  • Save EnrollmentController.java.

Seeing compile errors on the imports?

Make sure your service classes are in the com.studentms.service package and your controllers are in com.studentms.controller. If the import for a service class is red, the package declaration in that service file may be wrong.

help me fix import errors in my Spring Boot controller classes

✔️ Awesome, I've got everything!

Make sure all three controller files are saved before testing.

ⓧ I'd like to double check the full code

Here is StudentController.java:

package com.studentms.controller;

import com.studentms.dto.*;
import com.studentms.service.StudentService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/students")
@RequiredArgsConstructor
public class StudentController {

    private final StudentService studentService;

    @GetMapping
    public ResponseEntity<List<StudentResponse>> getAllStudents() {
        return ResponseEntity.ok(studentService.getAllStudents());
    }

    @GetMapping("/{id}")
    public ResponseEntity<StudentResponse> getStudentById(@PathVariable Long id) {
        return ResponseEntity.ok(studentService.getStudentById(id));
    }

    @PostMapping
    public ResponseEntity<StudentResponse> createStudent(@RequestBody CreateStudentRequest request) {
        return ResponseEntity.ok(studentService.createStudent(request));
    }

    @PutMapping("/{id}")
    public ResponseEntity<StudentResponse> updateStudent(@PathVariable Long id, @RequestBody UpdateStudentRequest request) {
        return ResponseEntity.ok(studentService.updateStudent(id, request));
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteStudent(@PathVariable Long id) {
        studentService.deleteStudent(id);
        return ResponseEntity.noContent().build();
    }
}

Here is CourseController.java:

package com.studentms.controller;

import com.studentms.dto.*;
import com.studentms.service.CourseService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/courses")
@RequiredArgsConstructor
public class CourseController {

    private final CourseService courseService;

    @GetMapping
    public ResponseEntity<List<CourseResponse>> getAllCourses() {
        return ResponseEntity.ok(courseService.getAllCourses());
    }

    @PostMapping
    public ResponseEntity<CourseResponse> createCourse(@RequestBody CreateCourseRequest request) {
        return ResponseEntity.ok(courseService.createCourse(request));
    }
}

Here is EnrollmentController.java:

package com.studentms.controller;

import com.studentms.dto.*;
import com.studentms.service.EnrollmentService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/enrollments")
@RequiredArgsConstructor
public class EnrollmentController {

    private final EnrollmentService enrollmentService;

    @GetMapping
    public ResponseEntity<List<EnrollmentResponse>> getAllEnrollments() {
        return ResponseEntity.ok(enrollmentService.getAllEnrollments());
    }

    @GetMapping("/student/{studentId}")
    public ResponseEntity<List<EnrollmentResponse>> getEnrollmentsByStudent(@PathVariable Long studentId) {
        return ResponseEntity.ok(enrollmentService.getEnrollmentsByStudentId(studentId));
    }

    @PostMapping
    public ResponseEntity<EnrollmentResponse> createEnrollment(@RequestBody EnrollmentRequest request) {
        return ResponseEntity.ok(enrollmentService.createEnrollment(request));
    }
}
Test the endpoints

Before adding security in the next step, you should confirm that your API works correctly with no authentication required.

  • Start your application by running this command in your terminal (from the project root):
mvn spring-boot:run

Why does the app ask for a login?

Because you added the spring-boot-starter-security dependency, Spring Security automatically protects all endpoints with a generated password. Look in your terminal output for a line starting with Using generated security password: and copy that password.

The default username is user. You will replace this auto-generated security in the next step.

  • Open your browser and navigate to http://localhost:8080/api/students.
  • When prompted to log in, enter user as the username and the generated password from your terminal output.

You should see an empty JSON array [] in the browser because there are no students yet.

  • Test creating a student by running this curl command in a new terminal window:
curl -X POST http://localhost:8080/api/students -H "Content-Type: application/json" -u user:[[GENERATED_PASSWORD="your generated security password"]] -d '{"name": "Alice Smith", "email": "alice@example.com", "department": "Computer Science"}'

You should see a JSON response with the created student including an auto-generated id field.

  • Refresh http://localhost:8080/api/students in your browser to confirm the student appears in the list.
  • Test the courses endpoint by navigating to http://localhost:8080/api/courses in your browser.

Getting a 403 Forbidden or connection refused?

If you see a 403 error, make sure you are including the -u user:password flag in your curl command with the correct generated password. If you see connection refused, make sure your app started successfully without errors in the terminal.

help me troubleshoot my Spring Boot API returning 403 errors

Your REST API is fully functional. Next up, you will lock it down with Spring Security so that different user roles have different levels of access.

Add Spring Security with Role-Based Access

Your REST API is working, but anyone can access any endpoint. In a real application, you need to control who can do what.

In this step, you will add Spring Security to protect your endpoints with role-based access control. Three roles (ADMIN, TEACHER, STUDENT) will each have different permissions.

In this step, get ready to:
  • Create a SecurityConfig class with a SecurityFilterChain bean.
  • Define in-memory users with three different roles.
  • Test that unauthenticated and unauthorized requests are rejected.
Create the SecurityFilterChain with endpoint rules

A SecurityFilterChain is a bean that tells Spring Security how to handle incoming requests. You define which URLs are public, which require authentication, and which require specific roles.

  • In IntelliJ, right-click the com.studentms.config package in the Project sidebar.
  • Select New then Java Class.
  • Name it SecurityConfig and press Enter.
  • Replace the entire file contents with this code:
package com.studentms.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            // Disable CSRF since this is a stateless REST API
            .csrf(csrf -> csrf.disable())
            // Define authorization rules for each endpoint
            .authorizeHttpRequests(authorize -> authorize
                .requestMatchers("/", "/index.html", "/favicon.ico").permitAll()
                .requestMatchers(HttpMethod.DELETE, "/api/**").hasRole("ADMIN")
                .requestMatchers(HttpMethod.POST, "/api/students").hasRole("ADMIN")
                .requestMatchers(HttpMethod.POST, "/api/courses").hasRole("ADMIN")
                .requestMatchers(HttpMethod.POST, "/api/enrollments").hasAnyRole("ADMIN", "TEACHER")
                .requestMatchers(HttpMethod.PUT, "/api/students/**").hasAnyRole("ADMIN", "TEACHER")
                .requestMatchers(HttpMethod.GET, "/api/**").hasAnyRole("ADMIN", "TEACHER", "STUDENT")
                .anyRequest().authenticated()
            )
            // Enable HTTP Basic authentication
            .httpBasic(Customizer.withDefaults());
        return http.build();
    }
}

What does this code do?

This SecurityFilterChain bean defines the security rules for every HTTP request that hits your API.

  • .csrf(csrf -> csrf.disable()) turns off CSRF protection. CSRF protection is designed for browser form submissions, not for REST APIs that use programmatic requests.
  • .requestMatchers("/", "/index.html", "/favicon.ico").permitAll() allows anyone to access the frontend page without logging in.
  • The hasRole and hasAnyRole methods restrict endpoints to specific user roles. For example, only ADMIN can delete resources or create students.
  • .httpBasic(Customizer.withDefaults()) enables HTTP Basic authentication, where the client sends credentials encoded in Base64 with each request.

Why disable CSRF for this project?

CSRF attacks exploit browser cookies to make unwanted requests on behalf of a logged-in user. Since your API uses HTTP Basic auth (credentials sent explicitly per request, not stored in cookies), CSRF protection adds no value here.

Additionally, the frontend and the API are served from the same Spring Boot application. This means they share the same origin, so you do not need explicit CORS configuration either.

Define in-memory users and a password encoder

Your SecurityFilterChain references roles like ADMIN and TEACHER, but Spring Security needs to know which users exist and what roles they have. You will define three test users stored in memory.

  • In SecurityConfig.java, add the following two beans below the securityFilterChain method but still inside the class:
    @Bean
    public InMemoryUserDetailsManager userDetailsManager(PasswordEncoder passwordEncoder) {
        // Create an admin user with full access
        UserDetails admin = User.builder()
                .username("admin")
                .password(passwordEncoder.encode("admin123"))
                .roles("ADMIN")
                .build();
        // Create a teacher user with limited write access
        UserDetails teacher = User.builder()
                .username("teacher")
                .password(passwordEncoder.encode("teacher123"))
                .roles("TEACHER")
                .build();
        // Create a student user with read-only access
        UserDetails student = User.builder()
                .username("student")
                .password(passwordEncoder.encode("student123"))
                .roles("STUDENT")
                .build();
        return new InMemoryUserDetailsManager(admin, teacher, student);
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

What does this code do?

These two beans work together to manage user authentication.

  • InMemoryUserDetailsManager stores users in memory (no database needed for this demo). Each user has a username, an encoded password, and one or more roles.
  • BCryptPasswordEncoder hashes passwords so they are never stored in plain text. Spring Security requires a PasswordEncoder bean to verify login attempts.
  • The three users match the three roles from your SecurityFilterChain: admin has full access, teacher can read and enroll students, and student can only read data.
  • Save SecurityConfig.java.

Seeing red underlines in IntelliJ?

Make sure all imports at the top of the file match exactly. If IntelliJ shows unresolved symbols, press Cmd+Shift+O (macOS) or Ctrl+Shift+O (Windows) to optimize imports.

If BCryptPasswordEncoder or InMemoryUserDetailsManager cannot be resolved, confirm that spring-boot-starter-security is in your pom.xml dependencies.

Help me fix unresolved imports in my SecurityConfig class.

✔️ Awesome, I've got everything!

Great. Make sure you have saved SecurityConfig.java with no errors before moving on.

ⓧ I'd like to double check the full code

package com.studentms.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())
            .authorizeHttpRequests(authorize -> authorize
                .requestMatchers("/", "/index.html", "/favicon.ico").permitAll()
                .requestMatchers(HttpMethod.DELETE, "/api/**").hasRole("ADMIN")
                .requestMatchers(HttpMethod.POST, "/api/students").hasRole("ADMIN")
                .requestMatchers(HttpMethod.POST, "/api/courses").hasRole("ADMIN")
                .requestMatchers(HttpMethod.POST, "/api/enrollments").hasAnyRole("ADMIN", "TEACHER")
                .requestMatchers(HttpMethod.PUT, "/api/students/**").hasAnyRole("ADMIN", "TEACHER")
                .requestMatchers(HttpMethod.GET, "/api/**").hasAnyRole("ADMIN", "TEACHER", "STUDENT")
                .anyRequest().authenticated()
            )
            .httpBasic(Customizer.withDefaults());
        return http.build();
    }

    @Bean
    public InMemoryUserDetailsManager userDetailsManager(PasswordEncoder passwordEncoder) {
        UserDetails admin = User.builder()
                .username("admin")
                .password(passwordEncoder.encode("admin123"))
                .roles("ADMIN")
                .build();
        UserDetails teacher = User.builder()
                .username("teacher")
                .password(passwordEncoder.encode("teacher123"))
                .roles("TEACHER")
                .build();
        UserDetails student = User.builder()
                .username("student")
                .password(passwordEncoder.encode("student123"))
                .roles("STUDENT")
                .build();
        return new InMemoryUserDetailsManager(admin, teacher, student);
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
Test that security rejects unauthorized requests

Your security configuration is in place. Now you need to verify that unauthenticated requests are rejected and that users can only access the endpoints their role allows.

  • Stop the running application if it is still active, then restart it by running:
mvn spring-boot:run
  • Open your browser and navigate to http://localhost:8080/api/students.

Your browser should prompt you for a username and password. This confirms that unauthenticated requests are now blocked with a 401 status.

  • Enter admin as the username and admin123 as the password.

You should see the JSON list of students (or an empty array if you have not created any yet). This confirms the ADMIN role has GET access.

  • To test role restrictions, open a new terminal and run this curl command to attempt a POST as the student user:
curl -u student:student123 -X POST http://localhost:8080/api/students -H "Content-Type: application/json" -d '{"name":"Test","email":"test@test.com","department":"CS"}'

You should see a 403 Forbidden response. The student role does not have permission to create new students.

  • Now test the same POST as the admin user:
curl -u admin:admin123 -X POST http://localhost:8080/api/students -H "Content-Type: application/json" -d '{"name":"Test","email":"test@test.com","department":"CS"}'

You should see a JSON response with the newly created student. The ADMIN role has full access to all endpoints.

What should I see?

Three distinct behaviors confirm your security is working correctly:

  • No credentials: browser prompts for login (HTTP 401 Unauthorized).
  • Wrong role: request is rejected (HTTP 403 Forbidden).
  • Correct role: request succeeds and returns data.

Still getting the Spring Security default login page?

If you see a styled HTML login form instead of a browser credential popup, your SecurityConfig class may not be detected. Confirm the class is in the com.studentms.config package and has both @Configuration and @EnableWebSecurity annotations.

Also check that there are no compilation errors preventing the class from loading. Review the terminal output for any stack traces on startup.

Help me troubleshoot why my SecurityConfig is not being picked up by Spring Boot.

Your API is now locked down with role-based security. Next up, you will build a frontend that logs in and displays different views depending on who is signed in.

Build the Frontend with Tailwind CSS and JavaScript

Your REST API is secured and tested. Now it is time to build the user interface that brings it all together.

In this step, you will create a single HTML page styled with Tailwind CSS that calls your API with JavaScript fetch requests. The page adapts its UI based on the logged-in user's role, so ADMIN, TEACHER, and STUDENT each see only the controls they are authorized to use.

In this step, get ready to:
  • Create the HTML page with a login form and dashboard layout.
  • Write JavaScript fetch() calls with Basic Authentication headers to load data.
  • Add forms for creating students, courses, and enrollments with role-based visibility.
Create the HTML file with login form and dashboard layout

Spring Boot automatically serves static files from the src/main/resources/static/ directory. You will create a single index.html file there that contains the entire frontend.

  • In IntelliJ, right-click the src/main/resources/static folder.
  • Select New, then File.
  • Name the file index.html and press Enter.
  • Paste the following HTML structure into the file:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Student Management System</title>
    <!-- Tailwind CSS v4 CDN for utility-class styling -->
    <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
</head>
<body class="bg-gray-100 min-h-screen">
    <!-- Login section: visible by default -->
    <div id="login-section" class="flex items-center justify-center min-h-screen">
        <div class="bg-white p-8 rounded-lg shadow-md w-96">
            <h1 class="text-2xl font-bold text-center mb-6 text-gray-800">Student Management System</h1>
            <div class="space-y-4">
                <input type="text" id="username" placeholder="Username" class="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
                <input type="password" id="password" placeholder="Password" class="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
                <button onclick="login()" class="w-full bg-blue-600 text-white py-2 rounded-lg hover:bg-blue-700 transition">Login</button>
                <p id="login-error" class="text-red-500 text-sm text-center hidden">Invalid credentials</p>
            </div>
        </div>
    </div>

    <!-- Dashboard: hidden until login succeeds -->
    <div id="dashboard" class="hidden">
        <nav class="bg-blue-600 text-white p-4 shadow-md">
            <div class="container mx-auto flex justify-between items-center">
                <h1 class="text-xl font-bold">Student Management System</h1>
                <div class="flex items-center gap-4">
                    <span id="user-info" class="text-sm"></span>
                    <button onclick="logout()" class="bg-blue-800 px-4 py-1 rounded hover:bg-blue-900 transition">Logout</button>
                </div>
            </div>
        </nav>

        <div class="container mx-auto p-6">
            <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
                <!-- Students panel -->
                <div class="bg-white rounded-lg shadow-md p-6">
                    <div class="flex justify-between items-center mb-4">
                        <h2 class="text-lg font-bold text-gray-800">Students</h2>
                        <button id="add-student-btn" onclick="showAddStudentForm()" class="hidden bg-green-500 text-white px-3 py-1 rounded text-sm hover:bg-green-600 transition">Add Student</button>
                    </div>
                    <div id="add-student-form" class="hidden mb-4 p-4 bg-gray-50 rounded">
                        <input type="text" id="student-name" placeholder="Name" class="w-full mb-2 px-3 py-1 border rounded">
                        <input type="email" id="student-email" placeholder="Email" class="w-full mb-2 px-3 py-1 border rounded">
                        <input type="text" id="student-department" placeholder="Department" class="w-full mb-2 px-3 py-1 border rounded">
                        <button onclick="createStudent()" class="bg-blue-600 text-white px-4 py-1 rounded text-sm hover:bg-blue-700">Save</button>
                    </div>
                    <div id="students-table" class="overflow-x-auto">
                        <table class="w-full text-sm">
                            <thead class="bg-gray-50">
                                <tr>
                                    <th class="px-4 py-2 text-left">Name</th>
                                    <th class="px-4 py-2 text-left">Email</th>
                                    <th class="px-4 py-2 text-left">Dept</th>
                                    <th id="actions-header" class="px-4 py-2 text-left hidden">Actions</th>
                                </tr>
                            </thead>
                            <tbody id="students-body"></tbody>
                        </table>
                    </div>
                </div>

                <!-- Courses panel -->
                <div class="bg-white rounded-lg shadow-md p-6">
                    <div class="flex justify-between items-center mb-4">
                        <h2 class="text-lg font-bold text-gray-800">Courses</h2>
                        <button id="add-course-btn" onclick="showAddCourseForm()" class="hidden bg-green-500 text-white px-3 py-1 rounded text-sm hover:bg-green-600 transition">Add Course</button>
                    </div>
                    <div id="add-course-form" class="hidden mb-4 p-4 bg-gray-50 rounded">
                        <input type="text" id="course-name" placeholder="Course Name" class="w-full mb-2 px-3 py-1 border rounded">
                        <input type="text" id="course-code" placeholder="Course Code" class="w-full mb-2 px-3 py-1 border rounded">
                        <input type="number" id="course-credits" placeholder="Credits" class="w-full mb-2 px-3 py-1 border rounded">
                        <button onclick="createCourse()" class="bg-blue-600 text-white px-4 py-1 rounded text-sm hover:bg-blue-700">Save</button>
                    </div>
                    <ul id="courses-list" class="space-y-2"></ul>
                </div>
            </div>

            <!-- Enrollments panel -->
            <div class="mt-6 bg-white rounded-lg shadow-md p-6">
                <div class="flex justify-between items-center mb-4">
                    <h2 class="text-lg font-bold text-gray-800">Enrollments</h2>
                    <button id="add-enrollment-btn" onclick="showAddEnrollmentForm()" class="hidden bg-green-500 text-white px-3 py-1 rounded text-sm hover:bg-green-600 transition">Enroll Student</button>
                </div>
                <div id="add-enrollment-form" class="hidden mb-4 p-4 bg-gray-50 rounded">
                    <select id="enroll-student" class="w-full mb-2 px-3 py-1 border rounded"></select>
                    <select id="enroll-course" class="w-full mb-2 px-3 py-1 border rounded"></select>
                    <button onclick="createEnrollment()" class="bg-blue-600 text-white px-4 py-1 rounded text-sm hover:bg-blue-700">Enroll</button>
                </div>
                <table class="w-full text-sm">
                    <thead class="bg-gray-50">
                        <tr>
                            <th class="px-4 py-2 text-left">Student</th>
                            <th class="px-4 py-2 text-left">Course</th>
                            <th class="px-4 py-2 text-left">Date</th>
                        </tr>
                    </thead>
                    <tbody id="enrollments-body"></tbody>
                </table>
            </div>
        </div>
    </div>

What does this HTML do?

The page has two main sections that toggle visibility with the hidden class.

  • The login-section div is visible by default. It shows username and password inputs plus a login button.
  • The dashboard div starts hidden. After a successful login, JavaScript swaps their visibility.
  • The Tailwind CSS CDN script loads utility classes like bg-gray-100 and rounded-lg directly in the browser.
  • Buttons like add-student-btn start with class hidden and are revealed only for the appropriate role.

You won't see a change in the browser yet. The next section adds the JavaScript that makes this page functional.

Write JavaScript for authentication and data loading

The frontend uses HTTP Basic Authentication to talk to your secured API. Each request includes a Base64-encoded username:password header that Spring Security validates.

  • Below the closing </div> of the dashboard section, add this <script> block with authentication and data-loading functions:
    <script>
        // Store credentials and role for the session
        let credentials = '';
        let currentRole = '';

        // Authenticate against the API using Basic Auth
        function login() {
            const username = document.getElementById('username').value;
            const password = document.getElementById('password').value;
            credentials = btoa(username + ':' + password);

            fetch('/api/students', {
                headers: { 'Authorization': 'Basic ' + credentials }
            }).then(response => {
                if (response.ok) {
                    currentRole = username === 'admin' ? 'ADMIN' : username === 'teacher' ? 'TEACHER' : 'STUDENT';
                    document.getElementById('login-section').classList.add('hidden');
                    document.getElementById('dashboard').classList.remove('hidden');
                    document.getElementById('user-info').textContent = username + ' (' + currentRole + ')';
                    setupRoleBasedUI();
                    loadData();
                } else {
                    document.getElementById('login-error').classList.remove('hidden');
                }
            }).catch(() => {
                document.getElementById('login-error').classList.remove('hidden');
            });
        }

        // Clear session and return to login screen
        function logout() {
            credentials = '';
            currentRole = '';
            document.getElementById('dashboard').classList.add('hidden');
            document.getElementById('login-section').classList.remove('hidden');
            document.getElementById('username').value = '';
            document.getElementById('password').value = '';
        }

        // Show or hide buttons depending on the user's role
        function setupRoleBasedUI() {
            if (currentRole === 'ADMIN') {
                document.getElementById('add-student-btn').classList.remove('hidden');
                document.getElementById('add-course-btn').classList.remove('hidden');
                document.getElementById('add-enrollment-btn').classList.remove('hidden');
                document.getElementById('actions-header').classList.remove('hidden');
            } else if (currentRole === 'TEACHER') {
                document.getElementById('add-enrollment-btn').classList.remove('hidden');
            }
        }

        // Reusable helper for authenticated API calls
        function apiCall(url, method = 'GET', body = null) {
            const options = {
                method: method,
                headers: {
                    'Authorization': 'Basic ' + credentials,
                    'Content-Type': 'application/json'
                }
            };
            if (body) options.body = JSON.stringify(body);
            return fetch(url, options).then(r => r.json());
        }

        // Fetch all data and populate the dashboard tables
        function loadData() {
            apiCall('/api/students').then(students => {
                const tbody = document.getElementById('students-body');
                tbody.innerHTML = students.map(s => `
                    <tr class="border-t">
                        <td class="px-4 py-2">${s.name}</td>
                        <td class="px-4 py-2">${s.email}</td>
                        <td class="px-4 py-2">${s.department}</td>
                        ${currentRole === 'ADMIN' ? `<td class="px-4 py-2"><button onclick="deleteStudent(${s.id})" class="text-red-500 hover:text-red-700 text-xs">Delete</button></td>` : ''}
                    </tr>
                `).join('');
            });

            apiCall('/api/courses').then(courses => {
                const list = document.getElementById('courses-list');
                list.innerHTML = courses.map(c => `
                    <li class="p-3 bg-gray-50 rounded flex justify-between">
                        <span class="font-medium">${c.courseName}</span>
                        <span class="text-gray-500 text-sm">${c.courseCode} | ${c.credits} credits</span>
                    </li>
                `).join('');
            });

            apiCall('/api/enrollments').then(enrollments => {
                const tbody = document.getElementById('enrollments-body');
                tbody.innerHTML = enrollments.map(e => `
                    <tr class="border-t">
                        <td class="px-4 py-2">${e.studentName}</td>
                        <td class="px-4 py-2">${e.courseName}</td>
                        <td class="px-4 py-2">${e.enrollmentDate}</td>
                    </tr>
                `).join('');
            });
        }
    </script>
</body>
</html>

What does this JavaScript do?

  • The login() function encodes the username and password with btoa() (Base64) and sends a test request to /api/students. If the server returns 200, the credentials are valid.
  • The setupRoleBasedUI() function unhides the Add/Delete buttons only for roles that have permission to use them.
  • The apiCall() helper attaches the Authorization header to every request, so you avoid repeating that logic in each function.
  • The loadData() function fetches students, courses, and enrollments from the API and renders them into the HTML tables using template literals.
  • Save index.html.
  • Make sure your Spring Boot application is running. If not, restart it with mvn spring-boot:run from your project root.
  • Open your browser and go to http://localhost:8080.
  • Enter admin as the username and admin123 as the password, then click Login.

You should see the dashboard with the nav bar showing admin (ADMIN) in the top right corner. The Students, Courses, and Enrollments panels are visible. The tables are empty because no data exists yet.

Login shows Invalid credentials?

  • Make sure you typed admin (all lowercase) as the username. The passwords are case-sensitive.
  • Confirm your Spring Boot app is running. Check IntelliJ's Run panel for errors.
  • Verify your SecurityConfig.java has the .requestMatchers("/", "/index.html", "/favicon.ico").permitAll() line so the HTML page loads without authentication.

Help me debug why my login is failing in the Student Management System.

Add form interaction and CRUD functions

The dashboard shows data, but clicking the Add Student or Add Course buttons throws an error because their handler functions do not exist yet. You will add the form toggle and create functions now.

  • In index.html, find the closing </script> tag at the bottom of the file.
  • Add this code above </script>:
        // Toggle form visibility for adding students
        function showAddStudentForm() {
            document.getElementById('add-student-form').classList.toggle('hidden');
        }

        // Toggle form visibility for adding courses
        function showAddCourseForm() {
            document.getElementById('add-course-form').classList.toggle('hidden');
        }

        // Toggle enrollment form and populate dropdowns
        function showAddEnrollmentForm() {
            const form = document.getElementById('add-enrollment-form');
            form.classList.toggle('hidden');
            if (!form.classList.contains('hidden')) {
                apiCall('/api/students').then(students => {
                    document.getElementById('enroll-student').innerHTML = students.map(s => `<option value="${s.id}">${s.name}</option>`).join('');
                });
                apiCall('/api/courses').then(courses => {
                    document.getElementById('enroll-course').innerHTML = courses.map(c => `<option value="${c.id}">${c.courseName}</option>`).join('');
                });
            }
        }

        // POST a new student to the API
        function createStudent() {
            const body = {
                name: document.getElementById('student-name').value,
                email: document.getElementById('student-email').value,
                department: document.getElementById('student-department').value
            };
            apiCall('/api/students', 'POST', body).then(() => {
                showAddStudentForm();
                loadData();
            });
        }

        // POST a new course to the API
        function createCourse() {
            const body = {
                courseName: document.getElementById('course-name').value,
                courseCode: document.getElementById('course-code').value,
                credits: parseInt(document.getElementById('course-credits').value)
            };
            apiCall('/api/courses', 'POST', body).then(() => {
                showAddCourseForm();
                loadData();
            });
        }

        // POST a new enrollment to the API
        function createEnrollment() {
            const body = {
                studentId: parseInt(document.getElementById('enroll-student').value),
                courseId: parseInt(document.getElementById('enroll-course').value)
            };
            apiCall('/api/enrollments', 'POST', body).then(() => {
                showAddEnrollmentForm();
                loadData();
            });
        }

        // DELETE a student by ID (ADMIN only)
        function deleteStudent(id) {
            fetch('/api/students/' + id, {
                method: 'DELETE',
                headers: { 'Authorization': 'Basic ' + credentials }
            }).then(() => loadData());
        }

What do these functions do?

  • The showAdd*Form() functions toggle the hidden class on the form containers. For enrollments, the toggle also populates the student and course dropdowns with fresh data from the API.
  • The createStudent(), createCourse(), and createEnrollment() functions read form input values, send a POST request using apiCall(), then hide the form and reload the data.
  • The deleteStudent() function sends a DELETE request with the student's ID and refreshes the table on success.
  • Save index.html.
  • Refresh your browser at http://localhost:8080.
  • Log in as admin / admin123.
  • Click Add Student, enter a name, email, and department, then click Save.

The new student appears in the Students table immediately. You can also test adding a course and creating an enrollment using the same flow.

How does role-based visibility work here?

Try logging out and back in as teacher / teacher123. You will see the Enroll Student button but NOT the Add Student or Add Course buttons. The backend enforces this too. Even if someone unhides the button with DevTools, the POST request returns 403 Forbidden.

Student not appearing after clicking Save?

  • Open your browser's DevTools console (F12) and look for network errors. A 403 means you are logged in as a role without POST permission.
  • Make sure you added the CRUD functions code ABOVE the </script> closing tag, not below it.
  • Confirm the email field contains a valid address. Duplicate emails cause a database constraint error because the Student entity has unique = true on the email column.

Help me debug why creating a student is failing in my frontend.

✔️ Awesome, I've got everything!

Great. Double check you have saved index.html and your app is running.

ⓧ I'd like to double check the full code

Here is the complete index.html file. Compare yours against it:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Student Management System</title>
    <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
</head>
<body class="bg-gray-100 min-h-screen">
    <div id="login-section" class="flex items-center justify-center min-h-screen">
        <div class="bg-white p-8 rounded-lg shadow-md w-96">
            <h1 class="text-2xl font-bold text-center mb-6 text-gray-800">Student Management System</h1>
            <div class="space-y-4">
                <input type="text" id="username" placeholder="Username" class="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
                <input type="password" id="password" placeholder="Password" class="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
                <button onclick="login()" class="w-full bg-blue-600 text-white py-2 rounded-lg hover:bg-blue-700 transition">Login</button>
                <p id="login-error" class="text-red-500 text-sm text-center hidden">Invalid credentials</p>
            </div>
        </div>
    </div>

    <div id="dashboard" class="hidden">
        <nav class="bg-blue-600 text-white p-4 shadow-md">
            <div class="container mx-auto flex justify-between items-center">
                <h1 class="text-xl font-bold">Student Management System</h1>
                <div class="flex items-center gap-4">
                    <span id="user-info" class="text-sm"></span>
                    <button onclick="logout()" class="bg-blue-800 px-4 py-1 rounded hover:bg-blue-900 transition">Logout</button>
                </div>
            </div>
        </nav>

        <div class="container mx-auto p-6">
            <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
                <div class="bg-white rounded-lg shadow-md p-6">
                    <div class="flex justify-between items-center mb-4">
                        <h2 class="text-lg font-bold text-gray-800">Students</h2>
                        <button id="add-student-btn" onclick="showAddStudentForm()" class="hidden bg-green-500 text-white px-3 py-1 rounded text-sm hover:bg-green-600 transition">Add Student</button>
                    </div>
                    <div id="add-student-form" class="hidden mb-4 p-4 bg-gray-50 rounded">
                        <input type="text" id="student-name" placeholder="Name" class="w-full mb-2 px-3 py-1 border rounded">
                        <input type="email" id="student-email" placeholder="Email" class="w-full mb-2 px-3 py-1 border rounded">
                        <input type="text" id="student-department" placeholder="Department" class="w-full mb-2 px-3 py-1 border rounded">
                        <button onclick="createStudent()" class="bg-blue-600 text-white px-4 py-1 rounded text-sm hover:bg-blue-700">Save</button>
                    </div>
                    <div id="students-table" class="overflow-x-auto">
                        <table class="w-full text-sm">
                            <thead class="bg-gray-50">
                                <tr>
                                    <th class="px-4 py-2 text-left">Name</th>
                                    <th class="px-4 py-2 text-left">Email</th>
                                    <th class="px-4 py-2 text-left">Dept</th>
                                    <th id="actions-header" class="px-4 py-2 text-left hidden">Actions</th>
                                </tr>
                            </thead>
                            <tbody id="students-body"></tbody>
                        </table>
                    </div>
                </div>

                <div class="bg-white rounded-lg shadow-md p-6">
                    <div class="flex justify-between items-center mb-4">
                        <h2 class="text-lg font-bold text-gray-800">Courses</h2>
                        <button id="add-course-btn" onclick="showAddCourseForm()" class="hidden bg-green-500 text-white px-3 py-1 rounded text-sm hover:bg-green-600 transition">Add Course</button>
                    </div>
                    <div id="add-course-form" class="hidden mb-4 p-4 bg-gray-50 rounded">
                        <input type="text" id="course-name" placeholder="Course Name" class="w-full mb-2 px-3 py-1 border rounded">
                        <input type="text" id="course-code" placeholder="Course Code" class="w-full mb-2 px-3 py-1 border rounded">
                        <input type="number" id="course-credits" placeholder="Credits" class="w-full mb-2 px-3 py-1 border rounded">
                        <button onclick="createCourse()" class="bg-blue-600 text-white px-4 py-1 rounded text-sm hover:bg-blue-700">Save</button>
                    </div>
                    <ul id="courses-list" class="space-y-2"></ul>
                </div>
            </div>

            <div class="mt-6 bg-white rounded-lg shadow-md p-6">
                <div class="flex justify-between items-center mb-4">
                    <h2 class="text-lg font-bold text-gray-800">Enrollments</h2>
                    <button id="add-enrollment-btn" onclick="showAddEnrollmentForm()" class="hidden bg-green-500 text-white px-3 py-1 rounded text-sm hover:bg-green-600 transition">Enroll Student</button>
                </div>
                <div id="add-enrollment-form" class="hidden mb-4 p-4 bg-gray-50 rounded">
                    <select id="enroll-student" class="w-full mb-2 px-3 py-1 border rounded"></select>
                    <select id="enroll-course" class="w-full mb-2 px-3 py-1 border rounded"></select>
                    <button onclick="createEnrollment()" class="bg-blue-600 text-white px-4 py-1 rounded text-sm hover:bg-blue-700">Enroll</button>
                </div>
                <table class="w-full text-sm">
                    <thead class="bg-gray-50">
                        <tr>
                            <th class="px-4 py-2 text-left">Student</th>
                            <th class="px-4 py-2 text-left">Course</th>
                            <th class="px-4 py-2 text-left">Date</th>
                        </tr>
                    </thead>
                    <tbody id="enrollments-body"></tbody>
                </table>
            </div>
        </div>
    </div>

    <script>
        let credentials = '';
        let currentRole = '';

        function login() {
            const username = document.getElementById('username').value;
            const password = document.getElementById('password').value;
            credentials = btoa(username + ':' + password);

            fetch('/api/students', {
                headers: { 'Authorization': 'Basic ' + credentials }
            }).then(response => {
                if (response.ok) {
                    currentRole = username === 'admin' ? 'ADMIN' : username === 'teacher' ? 'TEACHER' : 'STUDENT';
                    document.getElementById('login-section').classList.add('hidden');
                    document.getElementById('dashboard').classList.remove('hidden');
                    document.getElementById('user-info').textContent = username + ' (' + currentRole + ')';
                    setupRoleBasedUI();
                    loadData();
                } else {
                    document.getElementById('login-error').classList.remove('hidden');
                }
            }).catch(() => {
                document.getElementById('login-error').classList.remove('hidden');
            });
        }

        function logout() {
            credentials = '';
            currentRole = '';
            document.getElementById('dashboard').classList.add('hidden');
            document.getElementById('login-section').classList.remove('hidden');
            document.getElementById('username').value = '';
            document.getElementById('password').value = '';
        }

        function setupRoleBasedUI() {
            if (currentRole === 'ADMIN') {
                document.getElementById('add-student-btn').classList.remove('hidden');
                document.getElementById('add-course-btn').classList.remove('hidden');
                document.getElementById('add-enrollment-btn').classList.remove('hidden');
                document.getElementById('actions-header').classList.remove('hidden');
            } else if (currentRole === 'TEACHER') {
                document.getElementById('add-enrollment-btn').classList.remove('hidden');
            }
        }

        function apiCall(url, method = 'GET', body = null) {
            const options = {
                method: method,
                headers: {
                    'Authorization': 'Basic ' + credentials,
                    'Content-Type': 'application/json'
                }
            };
            if (body) options.body = JSON.stringify(body);
            return fetch(url, options).then(r => r.json());
        }

        function loadData() {
            apiCall('/api/students').then(students => {
                const tbody = document.getElementById('students-body');
                tbody.innerHTML = students.map(s => `
                    <tr class="border-t">
                        <td class="px-4 py-2">${s.name}</td>
                        <td class="px-4 py-2">${s.email}</td>
                        <td class="px-4 py-2">${s.department}</td>
                        ${currentRole === 'ADMIN' ? `<td class="px-4 py-2"><button onclick="deleteStudent(${s.id})" class="text-red-500 hover:text-red-700 text-xs">Delete</button></td>` : ''}
                    </tr>
                `).join('');
            });

            apiCall('/api/courses').then(courses => {
                const list = document.getElementById('courses-list');
                list.innerHTML = courses.map(c => `
                    <li class="p-3 bg-gray-50 rounded flex justify-between">
                        <span class="font-medium">${c.courseName}</span>
                        <span class="text-gray-500 text-sm">${c.courseCode} | ${c.credits} credits</span>
                    </li>
                `).join('');
            });

            apiCall('/api/enrollments').then(enrollments => {
                const tbody = document.getElementById('enrollments-body');
                tbody.innerHTML = enrollments.map(e => `
                    <tr class="border-t">
                        <td class="px-4 py-2">${e.studentName}</td>
                        <td class="px-4 py-2">${e.courseName}</td>
                        <td class="px-4 py-2">${e.enrollmentDate}</td>
                    </tr>
                `).join('');
            });
        }

        function showAddStudentForm() {
            document.getElementById('add-student-form').classList.toggle('hidden');
        }

        function showAddCourseForm() {
            document.getElementById('add-course-form').classList.toggle('hidden');
        }

        function showAddEnrollmentForm() {
            const form = document.getElementById('add-enrollment-form');
            form.classList.toggle('hidden');
            if (!form.classList.contains('hidden')) {
                apiCall('/api/students').then(students => {
                    document.getElementById('enroll-student').innerHTML = students.map(s => `<option value="${s.id}">${s.name}</option>`).join('');
                });
                apiCall('/api/courses').then(courses => {
                    document.getElementById('enroll-course').innerHTML = courses.map(c => `<option value="${c.id}">${c.courseName}</option>`).join('');
                });
            }
        }

        function createStudent() {
            const body = {
                name: document.getElementById('student-name').value,
                email: document.getElementById('student-email').value,
                department: document.getElementById('student-department').value
            };
            apiCall('/api/students', 'POST', body).then(() => {
                showAddStudentForm();
                loadData();
            });
        }

        function createCourse() {
            const body = {
                courseName: document.getElementById('course-name').value,
                courseCode: document.getElementById('course-code').value,
                credits: parseInt(document.getElementById('course-credits').value)
            };
            apiCall('/api/courses', 'POST', body).then(() => {
                showAddCourseForm();
                loadData();
            });
        }

        function createEnrollment() {
            const body = {
                studentId: parseInt(document.getElementById('enroll-student').value),
                courseId: parseInt(document.getElementById('enroll-course').value)
            };
            apiCall('/api/enrollments', 'POST', body).then(() => {
                showAddEnrollmentForm();
                loadData();
            });
        }

        function deleteStudent(id) {
            fetch('/api/students/' + id, {
                method: 'DELETE',
                headers: { 'Authorization': 'Basic ' + credentials }
            }).then(() => loadData());
        }
    </script>
</body>
</html>

Your Student Management System is fully functional. You have a secured REST API with proper DTOs, role-based access control, and a responsive frontend that adapts to each user's permissions.

Secret mission

Add Student Search and Filter

Your student management system works great for small lists. But what happens when you have hundreds of students? In this mission, you'll add a real-time search feature that filters students by name or department as the user types.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally, so there are no ongoing cloud costs.

Resources you used:

  • The student_management_db MySQL database.
  • The student-management Spring Boot project folder.
  • The running Spring Boot application process.

Keep everything running

No action needed. Choose this if you are still actively building on your Student Management System or want to experiment with additional features.

  • Your student_management_db database stays in MySQL with all your test data intact.
  • Your project files remain on disk, ready to open again in IntelliJ IDEA.
  • If the Spring Boot app is still running, it continues to serve requests on localhost:8080.

Pause - I'll come back to this later

Shut down the running application to free up memory and your terminal. Your files and database stay intact for when you return.

  • Switch to the terminal where Spring Boot is running and press Ctrl+C to stop the application.
  • Your student_management_db database remains in MySQL. Your project folder stays on disk.
  • To resume later, open the project in IntelliJ IDEA and run mvn spring-boot:run again.

Delete - I don't want to use this again

Remove all project resources and start fresh. This deletes your database and project files permanently.

  • Stop the Spring Boot application by pressing Ctrl+C in the terminal where it is running.
  • Drop the MySQL database by running this command in your MySQL client:
mysql -u root -p -e "DROP DATABASE student_management_db;"
  • Delete the project folder. On macOS/Linux:
rm -rf ~/Desktop/student-management

On Windows:

rmdir /s /q %USERPROFILE%\Desktop\student-management
  • Close the project in IntelliJ IDEA by clicking File, then Close Project.

Nice Work!

Nice Work!

You just built a full-stack Student Management System with a Spring Boot REST API, role-based security, and a clean frontend that ties it all together.

Here's what you accomplished:

  • Built a Spring Boot REST API with 3 JPA entities (Student, Course, Enrollment), proper DTOs with Lombok, and concrete service classes connected to MySQL.
  • Configured role-based security with Spring Security using SecurityFilterChain and in-memory users (ADMIN, TEACHER, STUDENT) to protect API endpoints.
  • Built a responsive frontend with Tailwind CSS and JavaScript that calls the REST API with Basic Authentication headers.
  • Secret Mission: Added student search with a custom JPA query method and debounced frontend filtering.

Ready to quiz yourself?