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.
-
Go to Spring Initializer
-
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
-
Click “Generate” to download the project
-
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 codesrc/main/resources
: Contains configuration filessrc/test
: Contains test classespom.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:
- Add the validation dependency to your
pom.xml
:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
- 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
}
- 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:
- Create an
exception
package - 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;
}
}
- 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);
}
}
- 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);
}
}
- 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
- Run your application by executing the main method in
StudentApiApplication.java
or using Maven:
mvn spring-boot:run
-
Once started, your API will be available at
http://localhost:8080/api/students
-
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}
)
- Check the H2 database console at
http://localhost:8080/h2-console
(use the connection details from application.properties)
Discussion Questions
- What makes an API “RESTful”? How does our implementation adhere to REST principles?
- Why did we use separate layers (controller, service, repository) instead of putting all logic in the controller?
- How does Spring Boot simplify the creation of a REST API compared to traditional Java web applications?
- What would you need to change to make this application use a production database like MySQL or PostgreSQL?
- 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