Lab: Spring Boot REST API


This lab will guide you through creating a simple yet comprehensive REST API using Spring Boot. By the end, you’ll understand the fundamental concepts of REST architecture and how Spring Boot simplifies API development.

Learning Objectives

  • Understand REST architectural principles
  • Create a Spring Boot application from scratch
  • Implement RESTful endpoints with proper HTTP methods
  • Work with JSON data and object mapping
  • Implement basic error handling
  • Test your API using tools like Postman

Lab Overview

We’ll build a “Student Management System” API that allows operations like adding, updating, retrieving, and removing student records.

Prerequisites

  • Java Development Kit (JDK) 11 or higher
  • Maven or Gradle build tool
  • Your favorite IDE (IntelliJ IDEA, Eclipse, or VS Code)
  • Postman (for testing the API)

Step 1: Setting Up the Spring Boot Project

Let’s start by creating a new Spring Boot project.

  1. Go to Spring Initializer

  2. Configure your project:

  • Project: Maven
  • Language: Java
  • Spring Boot version: 3.0.0+ (or latest stable)
  • Group: com.example
  • Artifact: student-api
  • Dependencies: Spring Web, Spring Data JPA, H2 Database
  1. Click “Generate” to download the project

  2. Extract the ZIP file and open it in your IDE

The generated project structure includes essential files like pom.xml (for Maven) and a main application class.

Step 2: Understanding the Project Structure

Take a moment to explore what Spring Initializer has created for you:

  • src/main/java: Contains Java source code
  • src/main/resources: Contains configuration files
  • src/test: Contains test classes
  • pom.xml: Maven configuration file with dependencies

The main application class (StudentApiApplication.java) should look like this:

package com.example.studentapi;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class StudentApiApplication {
    public static void main(String[] args) {
        SpringApplication.run(StudentApiApplication.class, args);
    }
}

This class serves as the entry point for your application. The @SpringBootApplication annotation combines several annotations that enable auto-configuration, component scanning, and more.

Step 3: Creating the Student Model

Create a new package called model under src/main/java/com/example/studentapi.

Within this package, create a Student.java class:

package com.example.studentapi.model;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;

@Entity
public class Student {

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

    private String firstName;
    private String lastName;
    private String email;
    private String major;

    // Default constructor required by JPA
    public Student() {
    }

    // Constructor with fields
    public Student(String firstName, String lastName, String email, String major) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.email = email;
        this.major = major;
    }

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

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

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public String getEmail() {
        return email;
    }

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

    public String getMajor() {
        return major;
    }

    public void setMajor(String major) {
        this.major = major;
    }

    @Override
    public String toString() {
        return "Student{" +
                "id=" + id +
                ", firstName='" + firstName + '\'' +
                ", lastName='" + lastName + '\'' +
                ", email='" + email + '\'' +
                ", major='" + major + '\'' +
                '}';
    }
}

Here, we’ve defined a simple Student class with some basic attributes. The @Entity annotation marks this class as a JPA entity, which means it’ll be mapped to a database table. The @Id annotation identifies the primary key, and @GeneratedValue specifies how the primary key should be generated.

Step 4: Creating the Repository

Next, create a repository package and add a StudentRepository interface:

package com.example.studentapi.repository;

import com.example.studentapi.model.Student;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface StudentRepository extends JpaRepository<Student, Long> {
    // Spring Data JPA provides basic CRUD operations automatically!
}

Spring Data JPA provides a powerful repository abstraction that eliminates boilerplate code. By extending JpaRepository, we instantly get methods for common operations like saving, finding, and deleting records.

Step 5: Creating the Service Layer

Create a service package with a StudentService interface and implementation:

First, the interface:

package com.example.studentapi.service;

import com.example.studentapi.model.Student;
import java.util.List;

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

Then, the implementation:

package com.example.studentapi.service;

import com.example.studentapi.model.Student;
import com.example.studentapi.repository.StudentRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class StudentServiceImpl implements StudentService {

    @Autowired
    private StudentRepository studentRepository;

    @Override
    public Student saveStudent(Student student) {
        return studentRepository.save(student);
    }

    @Override
    public List<Student> getAllStudents() {
        return studentRepository.findAll();
    }

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

    @Override
    public Student updateStudent(Long id, Student studentDetails) {
        Student student = getStudentById(id);

        student.setFirstName(studentDetails.getFirstName());
        student.setLastName(studentDetails.getLastName());
        student.setEmail(studentDetails.getEmail());
        student.setMajor(studentDetails.getMajor());

        return studentRepository.save(student);
    }

    @Override
    public void deleteStudent(Long id) {
        Student student = getStudentById(id);
        studentRepository.delete(student);
    }
}

The service layer contains business logic and calls repository methods. This separation helps maintain a clean architecture and makes testing easier.

Step 6: Creating the REST Controller

Now, create a controller package and add a StudentController:

package com.example.studentapi.controller;

import com.example.studentapi.model.Student;
import com.example.studentapi.service.StudentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

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

    @Autowired
    private StudentService studentService;

    // CREATE - HTTP POST
    @PostMapping
    public ResponseEntity<Student> createStudent(@RequestBody Student student) {
        Student savedStudent = studentService.saveStudent(student);
        return new ResponseEntity<>(savedStudent, HttpStatus.CREATED);
    }

    // READ ALL - HTTP GET
    @GetMapping
    public ResponseEntity<List<Student>> getAllStudents() {
        List<Student> students = studentService.getAllStudents();
        return new ResponseEntity<>(students, HttpStatus.OK);
    }

    // READ ONE - HTTP GET with path variable
    @GetMapping("/{id}")
    public ResponseEntity<Student> getStudentById(@PathVariable Long id) {
        Student student = studentService.getStudentById(id);
        return new ResponseEntity<>(student, HttpStatus.OK);
    }

    // UPDATE - HTTP PUT
    @PutMapping("/{id}")
    public ResponseEntity<Student> updateStudent(@PathVariable Long id, @RequestBody Student student) {
        Student updatedStudent = studentService.updateStudent(id, student);
        return new ResponseEntity<>(updatedStudent, HttpStatus.OK);
    }

    // DELETE - HTTP DELETE
    @DeleteMapping("/{id}")
    public ResponseEntity<String> deleteStudent(@PathVariable Long id) {
        studentService.deleteStudent(id);
        return new ResponseEntity<>("Student deleted successfully", HttpStatus.OK);
    }
}

This controller establishes the REST endpoints for our API. Let’s understand the key annotations:

  • @RestController: Combines @Controller and @ResponseBody, meaning all methods return data directly (not view names)
  • @RequestMapping: Defines the base URL path for all endpoints in this controller
  • @GetMapping, @PostMapping, @PutMapping, @DeleteMapping: HTTP method-specific shortcuts for @RequestMapping
  • @PathVariable: Extracts values from the URL path
  • @RequestBody: Converts JSON request body to Java objects

Step 7: Configuring the Database

Spring Boot can use various databases. For simplicity, we’ll use H2, an in-memory database.

Add these properties to src/main/resources/application.properties:

# H2 Database Configuration
spring.datasource.url=jdbc:h2:mem:studentdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=

# Enable H2 Console
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console

# JPA Configuration
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true

These settings configure the H2 database connection and enable an H2 web console for easy database inspection.

Step 8: Add Data Validation (Optional)

For better data quality, let’s add validation:

  1. Add the validation dependency to your pom.xml:
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
  1. Update the Student model:
package com.example.studentapi.model;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

@Entity
public class Student {

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

    @NotBlank(message = "First name is required")
    @Size(max = 50, message = "First name cannot exceed 50 characters")
    private String firstName;

    @NotBlank(message = "Last name is required")
    @Size(max = 50, message = "Last name cannot exceed 50 characters")
    private String lastName;

    @NotBlank(message = "Email is required")
    @Email(message = "Email should be valid")
    private String email;

    @NotBlank(message = "Major is required")
    private String major;

    // Constructors, getters, and setters remain the same
}
  1. Update the controller to validate requests:
@PostMapping
public ResponseEntity<Student> createStudent(@Valid @RequestBody Student student) {
    Student savedStudent = studentService.saveStudent(student);
    return new ResponseEntity<>(savedStudent, HttpStatus.CREATED);
}

@PutMapping("/{id}")
public ResponseEntity<Student> updateStudent(@PathVariable Long id, @Valid @RequestBody Student student) {
    Student updatedStudent = studentService.updateStudent(id, student);
    return new ResponseEntity<>(updatedStudent, HttpStatus.OK);
}

Step 9: Exception Handling

Let’s add global exception handling to provide better error responses:

  1. Create an exception package
  2. Add an ErrorDetails class:
package com.example.studentapi.exception;

import java.util.Date;

public class ErrorDetails {
    private Date timestamp;
    private String message;
    private String details;

    public ErrorDetails(Date timestamp, String message, String details) {
        this.timestamp = timestamp;
        this.message = message;
        this.details = details;
    }

    // Getters
    public Date getTimestamp() {
        return timestamp;
    }

    public String getMessage() {
        return message;
    }

    public String getDetails() {
        return details;
    }
}
  1. Add a custom exception:
package com.example.studentapi.exception;

public class ResourceNotFoundException extends RuntimeException {

    private static final long serialVersionUID = 1L;

    public ResourceNotFoundException(String message) {
        super(message);
    }
}
  1. Create a global exception handler:
package com.example.studentapi.exception;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;

import java.util.Date;

@ControllerAdvice
public class GlobalExceptionHandler {

    // Handle specific exceptions
    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<?> resourceNotFoundException(ResourceNotFoundException ex, WebRequest request) {
        ErrorDetails errorDetails = new ErrorDetails(new Date(), ex.getMessage(), request.getDescription(false));
        return new ResponseEntity<>(errorDetails, HttpStatus.NOT_FOUND);
    }

    // Handle validation exceptions
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<?> validationErrorHandler(MethodArgumentNotValidException ex, WebRequest request) {
        StringBuilder messages = new StringBuilder();
        ex.getBindingResult().getAllErrors().forEach(error -> {
            messages.append(error.getDefaultMessage()).append(". ");
        });

        ErrorDetails errorDetails = new ErrorDetails(new Date(), messages.toString(), request.getDescription(false));
        return new ResponseEntity<>(errorDetails, HttpStatus.BAD_REQUEST);
    }

    // Handle global exceptions
    @ExceptionHandler(Exception.class)
    public ResponseEntity<?> globalExceptionHandler(Exception ex, WebRequest request) {
        ErrorDetails errorDetails = new ErrorDetails(new Date(), ex.getMessage(), request.getDescription(false));
        return new ResponseEntity<>(errorDetails, HttpStatus.INTERNAL_SERVER_ERROR);
    }
}
  1. Update the service implementation to use the custom exception:
@Override
public Student getStudentById(Long id) {
    return studentRepository.findById(id)
            .orElseThrow(() -> new ResourceNotFoundException("Student not found with id: " + id));
}

Step 10: Run and Test Your API

  1. Run your application by executing the main method in StudentApiApplication.java or using Maven:
mvn spring-boot:run
  1. Once started, your API will be available at http://localhost:8080/api/students

  2. Use Postman to test the endpoints:

a. Create a student (POST to http://localhost:8080/api/students):

{
  "firstName": "John",
  "lastName": "Doe",
  "email": "[email protected]",
  "major": "Computer Science"
}

b. Get all students (GET http://localhost:8080/api/students)

c. Get a specific student (GET http://localhost:8080/api/students/{id})

d. Update a student (PUT http://localhost:8080/api/students/{id})

e. Delete a student (DELETE http://localhost:8080/api/students/{id})

  1. Check the H2 database console at http://localhost:8080/h2-console (use the connection details from application.properties)

Discussion Questions

  1. What makes an API “RESTful”? How does our implementation adhere to REST principles?
  2. Why did we use separate layers (controller, service, repository) instead of putting all logic in the controller?
  3. How does Spring Boot simplify the creation of a REST API compared to traditional Java web applications?
  4. What would you need to change to make this application use a production database like MySQL or PostgreSQL?
  5. How would you implement authentication and authorization for this API?

Conclusion

Congratulations! You’ve created a complete REST API with Spring Boot. This application demonstrates:

  • Creating RESTful endpoints with appropriate HTTP methods
  • Using Spring Data JPA for database operations
  • Implementing proper exception handling
  • Working with JSON data through automatic object mapping
  • Organizing code into separate layers for better maintainability

This foundational understanding will help you build more complex applications and better understand how modern web services work.


By Wahid Hamdi