TP3.1 - Layered Architecture and Complete CRUD API
Student Management Application
Objectives
This practical work will allow you to:
- Understand the layered architecture (Controller → Service → Repository)
- Create a complete REST CRUD API
- Implement Create, Read, Update, Delete operations
- Use Spring Data JPA with H2 (in-memory database)
- Test a REST API with Postman or a browser
- Handle errors and validations
Table of Contents
- Layered Architecture
- Project Creation
- Creating the Student Entity
- Creating the Repository
- Creating the Service
- Creating the Controller
- H2 Configuration
- Testing the Endpoints
- Additional Exercises
Resources
Documentation with detailed screenshots:
📸 TP3.1 References - Google Drive
1. Layered Architecture
Overview
The layered architecture separates responsibilities into 4 distinct levels:
+----------------+ +----------------+ +----------------+ +----------------+
| | | | | | | |
| Controller +------>+ Service +------>+ Repository +------>+ Database |
| | | | | | | |
+----------------+ +----------------+ +----------------+ +----------------+
(HTTP) (Business Logic) (Data Access) (Storage)
Mermaid Diagram
Responsibilities of Each Layer
1. Controller (Presentation Layer)
Role:
- Entry point of the application
- Receives HTTP requests (GET, POST, PUT, DELETE)
- Parses request parameters
- Calls the appropriate Service
- Returns the response to the client (JSON)
Annotations:
@RestController@RequestMapping@GetMapping,@PostMapping,@PutMapping,@DeleteMapping
Example:
@RestController
@RequestMapping("/api/students")
public class StudentController {
@Autowired
private StudentService studentService;
@GetMapping
public List<Student> getAllStudents() {
return studentService.getAllStudents();
}
}
2. Service (Business Layer)
Role:
- Contains the business logic of the application
- Intermediary between Controller and Repository
- Applies validation rules
- Performs calculations and transformations
- Manages transactions
Annotations:
@Service@Transactional(for transactions)
Example:
@Service
public class StudentService {
@Autowired
private StudentRepository studentRepository;
public List<Student> getAllStudents() {
return studentRepository.findAll();
}
}
3. Repository (Data Access Layer)
Role:
- Directly interacts with the database
- Performs CRUD operations
- Uses Spring Data JPA
- Automatic generation of SQL queries
Annotations:
@Repository- Interface extending
JpaRepository
Example:
@Repository
public interface StudentRepository extends JpaRepository<Student, Long> {
// Methods automatically generated by Spring Data JPA
}
4. Database (Persistence Layer)
Role:
- Physical storage of data
- Management of ACID transactions
- Indexing and optimization
Technologies:
- H2 (in-memory database for development/tests)
- PostgreSQL, MySQL (production)
Request Flow
2. Project Creation
2.1 Spring Initializr
Go to https://start.spring.io/
Configuration:
| Parameter | Value |
|---|---|
| Project | Maven |
| Language | Java |
| Spring Boot | 3.2.0 (or latest stable version) |
| Group | com.example |
| Artifact | student-management |
| Name | student-management |
| Package name | com.example.student |
| Packaging | Jar |
| Java | 17 |
Dependencies to add:
- Spring Web - To create the REST API
- Spring Data JPA - For persistence
- H2 Database - In-memory database
- Validation - To validate data
- Spring Boot DevTools - For automatic reload
2.2 Import into VS Code
- Download the project (ZIP)
- Unzip into your workspace
- Open VS Code
- File → Open Folder → Select
student-management - Wait for Maven to download dependencies
2.3 Project Structure
Create the following package structure:
src/main/java/com/example/student/
├── StudentManagementApplication.java
├── controller/
│ └── StudentController.java
├── service/
│ └── StudentService.java
├── repository/
│ └── StudentRepository.java
├── model/
│ └── Student.java
└── exception/
└── ResourceNotFoundException.java
3. Creating the Student Entity
3.1 Student Class
Create src/main/java/com/example/student/model/Student.java:
package com.example.student.model;
import jakarta.persistence.*;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
@Entity
@Table(name = "students")
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank(message = "First name is required")
@Size(min = 2, max = 50, message = "First name must be between 2 and 50 characters")
@Column(name = "first_name", nullable = false)
private String firstName;
@NotBlank(message = "Last name is required")
@Size(min = 2, max = 50, message = "Last name must be between 2 and 50 characters")
@Column(name = "last_name", nullable = false)
private String lastName;
@NotBlank(message = "Email is required")
@Email(message = "Invalid email format")
@Column(name = "email", nullable = false, unique = true)
private String email;
@Column(name = "age")
private Integer age;
// Default constructor (required by JPA)
public Student() {
}
// Constructor with parameters
public Student(String firstName, String lastName, String email, Integer age) {
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
this.age = age;
}
// 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 Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"id=" + id +
", firstName='" + firstName + '\'' +
", lastName='" + lastName + '\'' +
", email='" + email + '\'' +
", age=" + age +
'}';
}
}
3.2 Explanation of Annotations
| Annotation | Role |
|---|---|
@Entity | Marks the class as a JPA entity |
@Table(name = "students") | SQL table name |
@Id | Primary key |
@GeneratedValue(strategy = GenerationType.IDENTITY) | Auto-increment |
@Column | Column configuration |
@NotBlank | Validation: non-empty field |
@Size | Validation: min/max size |
@Email | Validation: email format |
4. Creating the Repository
4.1 StudentRepository Interface
Create src/main/java/com/example/student/repository/StudentRepository.java:
package com.example.student.repository;
import com.example.student.model.Student;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface StudentRepository extends JpaRepository<Student, Long> {
// Spring Data JPA automatically generates the implementation
// Custom methods (optional)
Optional<Student> findByEmail(String email);
boolean existsByEmail(String email);
}
4.2 Available Methods
Methods inherited from JpaRepository:
| Method | Description |
|---|---|
save(Student s) | Save or update |
findAll() | Retrieve all students |
findById(Long id) | Retrieve by ID |
deleteById(Long id) | Delete by ID |
count() | Count records |
existsById(Long id) | Check existence |
Custom methods:
findByEmail(String email)- Find by emailexistsByEmail(String email)- Check if email exists
Note: Spring Data JPA automatically generates SQL queries based on the method name!
5. Creating the Service
5.1 StudentService Class
Create src/main/java/com/example/student/service/StudentService.java:
package com.example.student.service;
import com.example.student.model.Student;
import com.example.student.repository.StudentRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
@Service
public class StudentService {
@Autowired
private StudentRepository studentRepository;
// CREATE - Create a student
public Student createStudent(Student student) {
// Check if email already exists
if (studentRepository.existsByEmail(student.getEmail())) {
throw new RuntimeException("A student with this email already exists");
}
return studentRepository.save(student);
}
// READ - Retrieve all students
public List<Student> getAllStudents() {
return studentRepository.findAll();
}
// READ - Retrieve a student by ID
public Optional<Student> getStudentById(Long id) {
return studentRepository.findById(id);
}
// UPDATE - Update a student
public Student updateStudent(Long id, Student studentDetails) {
Student student = studentRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Student not found with id: " + id));
student.setFirstName(studentDetails.getFirstName());
student.setLastName(studentDetails.getLastName());
student.setEmail(studentDetails.getEmail());
student.setAge(studentDetails.getAge());
return studentRepository.save(student);
}
// DELETE - Delete a student
public void deleteStudent(Long id) {
Student student = studentRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Student not found with id: " + id));
studentRepository.delete(student);
}
// Utility method - Find by email
public Optional<Student> getStudentByEmail(String email) {
return studentRepository.findByEmail(email);
}
}
5.2 Business Logic
The Service contains:
- Validation: Check if email exists before creation
- Error Handling: Throw exceptions if student not found
- Orchestration: Coordinate calls to the Repository
- Transformation: Map data if necessary
6. Creating the Controller
6.1 StudentController Class
Create src/main/java/com/example/student/controller/StudentController.java:
package com.example.student.controller;
import com.example.student.model.Student;
import com.example.student.service.StudentService;
import jakarta.validation.Valid;
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 - POST /api/students
@PostMapping
public ResponseEntity<Student> createStudent(@Valid @RequestBody Student student) {
try {
Student createdStudent = studentService.createStudent(student);
return new ResponseEntity<>(createdStudent, HttpStatus.CREATED);
} catch (RuntimeException e) {
return new ResponseEntity<>(null, HttpStatus.CONFLICT);
}
}
// READ ALL - GET /api/students
@GetMapping
public ResponseEntity<List<Student>> getAllStudents() {
List<Student> students = studentService.getAllStudents();
return new ResponseEntity<>(students, HttpStatus.OK);
}
// READ ONE - GET /api/students/{id}
@GetMapping("/{id}")
public ResponseEntity<Student> getStudentById(@PathVariable Long id) {
return studentService.getStudentById(id)
.map(student -> new ResponseEntity<>(student, HttpStatus.OK))
.orElse(new ResponseEntity<>(HttpStatus.NOT_FOUND));
}
// UPDATE - PUT /api/students/{id}
@PutMapping("/{id}")
public ResponseEntity<Student> updateStudent(
@PathVariable Long id,
@Valid @RequestBody Student studentDetails) {
try {
Student updatedStudent = studentService.updateStudent(id, studentDetails);
return new ResponseEntity<>(updatedStudent, HttpStatus.OK);
} catch (RuntimeException e) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
}
// DELETE - DELETE /api/students/{id}
@DeleteMapping("/{id}")
public ResponseEntity<HttpStatus> deleteStudent(@PathVariable Long id) {
try {
studentService.deleteStudent(id);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
} catch (RuntimeException e) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
}
// SEARCH BY EMAIL - GET /api/students/email/{email}
@GetMapping("/email/{email}")
public ResponseEntity<Student> getStudentByEmail(@PathVariable String email) {
return studentService.getStudentByEmail(email)
.map(student -> new ResponseEntity<>(student, HttpStatus.OK))
.orElse(new ResponseEntity<>(HttpStatus.NOT_FOUND));
}
}
6.2 REST Endpoints
| HTTP Method | Endpoint | Action | Success Code |
|---|---|---|---|
| POST | /api/students | Create a student | 201 CREATED |
| GET | /api/students | List all students | 200 OK |
| GET | /api/students/\{id\} | Retrieve a student | 200 OK |
| PUT | /api/students/\{id\} | Update a student | 200 OK |
| DELETE | /api/students/\{id\} | Delete a student | 204 NO CONTENT |
| GET | /api/students/email/\{email\} | Search by email | 200 OK |
6.3 HTTP Status Codes
7. H2 Configuration
7.1 application.properties File
Create/modify src/main/resources/application.properties:
# Server port
server.port=8080
# H2 Database Configuration
spring.datasource.url=jdbc:h2:mem:studentdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
# JPA Configuration
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
# H2 Console (accessible via browser)
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
7.2 Accessing the H2 Console
Once the application is started, access:
http://localhost:8080/h2-console
Connection Parameters:
| Field | Value |
|---|---|
| JDBC URL | jdbc:h2:mem:studentdb |
| User Name | sa |
| Password | (leave blank) |
8. Testing the Endpoints
8.1 Starting the Application
In VS Code:
mvn clean install -DskipTests spring-boot:run
Expected Logs:
Hibernate: create table students (...)
Started StudentManagementApplication in X.XXX seconds
8.2 Testing with the Browser
Test 1: Create a student (POST)
Note: The browser cannot easily perform POST. Use Postman or curl.
With curl (PowerShell/CMD):
curl -X POST http://localhost:8080/api/students ^
-H "Content-Type: application/json" ^
-d "{\"firstName\":\"Jean\",\"lastName\":\"Dupont\",\"email\":\"jean.dupont@example.com\",\"age\":20}"
Test 2: List all students (GET)
In the browser:
http://localhost:8080/api/students
Expected Response:
[
{
"id": 1,
"firstName": "Jean",
"lastName": "Dupont",
"email": "jean.dupont@example.com",
"age": 20
}
]
Test 3: Retrieve a student by ID (GET)
http://localhost:8080/api/students/1
Expected Response:
{
"id": 1,
"firstName": "Jean",
"lastName": "Dupont",
"email": "jean.dupont@example.com",
"age": 20
}
8.3 Testing with Postman
Installing Postman
- Download Postman: https://www.postman.com/downloads/
- Install and launch Postman
Test Collection
Test 1: CREATE (POST)
- Method: POST
- URL:
http://localhost:8080/api/students - Headers:
Content-Type: application/json - Body (raw JSON):
{
"firstName": "Alice",
"lastName": "Martin",
"email": "alice.martin@example.com",
"age": 22
}
Expected Result: Status 201 CREATED
Test 2: READ ALL (GET)
- Method: GET
- URL:
http://localhost:8080/api/students
Expected Result: Status 200 OK + List of students
Test 3: READ ONE (GET)
- Method: GET
- URL:
http://localhost:8080/api/students/1
Expected Result: Status 200 OK + Student details
Test 4: UPDATE (PUT)
- Method: PUT
- URL:
http://localhost:8080/api/students/1 - Headers:
Content-Type: application/json - Body (raw JSON):
{
"firstName": "Alice",
"lastName": "Martin-Updated",
"email": "alice.martin.updated@example.com",
"age": 23
}
Expected Result: Status 200 OK + Updated student
Test 5: DELETE (DELETE)
- Method: DELETE
- URL:
http://localhost:8080/api/students/1
Expected Result: Status 204 NO CONTENT
Test 6: SEARCH BY EMAIL (GET)
- Method: GET
- URL:
http://localhost:8080/api/students/email/alice.martin@example.com
Expected Result: Status 200 OK + Student found
8.4 Verification in the H2 Console
- Access
http://localhost:8080/h2-console - Log in
- Execute the query:
SELECT * FROM students;
You should see all the created students.
9. Additional Exercises
Exercise 1: Add a "Program" Field
Objective: Add a program (String) field to the Student entity
Instructions:
- Modify the
Student.javaclass - Add the
programfield - Generate getters/setters
- Test creating a student with a program
Exercise 2: Search by Last Name
Objective: Implement a search by last name
Instructions:
- Add a method in
StudentRepository:List<Student> findByLastName(String lastName); - Add a method in
StudentService - Add an endpoint in
StudentController:@GetMapping("/lastname/\{lastName\}") - Test with Postman
Exercise 3: Pagination
Objective: Add pagination to the student list
Instructions:
- Modify the
getAllStudents()method in the Service:public Page<Student> getAllStudents(int page, int size) {
Pageable pageable = PageRequest.of(page, size);
return studentRepository.findAll(pageable);
} - Modify the Controller to accept
pageandsizeparameters - Test:
http://localhost:8080/api/students?page=0&size=5
Exercise 4: Custom Error Handling
Objective: Create global error handling
Instructions:
- Create
ResourceNotFoundException.java:
package com.example.student.exception;
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String message) {
super(message);
}
}
- Create
GlobalExceptionHandler.java:
package com.example.student.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<String> handleResourceNotFound(ResourceNotFoundException ex) {
return new ResponseEntity<>(ex.getMessage(), HttpStatus.NOT_FOUND);
}
}
- Use
ResourceNotFoundExceptionin the Service - Test by trying to access a non-existent student
Exercise 5: Advanced Validation
Objective: Add custom validations
Instructions:
- Add constraints in
Student.java:@Min(value = 18, message = "Minimum age is 18 years")
@Max(value = 100, message = "Maximum age is 100 years")
private Integer age; - Test by creating a student with an invalid age
- Observe the 400 BAD REQUEST code
Exercise 6: Migration to PostgreSQL
Objective: Replace H2 with PostgreSQL
Instructions:
- Add the PostgreSQL dependency in
pom.xml - Modify
application.properties:spring.datasource.url=jdbc:postgresql://localhost:5432/studentdb
spring.datasource.username=postgres
spring.datasource.password=postgres
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.hibernate.ddl-auto=update - Create the
studentdbdatabase in PostgreSQL - Restart the application
- Verify that data persists after restart
Troubleshooting
Problem 1: 404 Error on All Endpoints
Cause: The Controller is not scanned by Spring
Solution:
Ensure StudentController is in the same package (or sub-package) as StudentManagementApplication.
Problem 2: Validation Error
Symptom:
Validation failed for object='student'
Solution:
Ensure the data sent respects the @NotBlank, @Email, @Size, etc. constraints.
Problem 3: H2 Console Not Accessible
Symptom:
404 on /h2-console
Solution:
Check in application.properties:
spring.h2.console.enabled=true
Problem 4: Email Conflict
Symptom:
Status 409 CONFLICT
Solution:
Normal! A student with this email already exists. This is validation working.
Review Questions
-
What is the responsibility of each layer (Controller, Service, Repository)?
-
Why separate the code into layers?
-
What is the difference between
@RestControllerand@Service? -
What does the
@Validannotation do in the Controller? -
How does Spring Data JPA generate SQL queries?
-
What is the difference between
save()for create and update? -
Why use
Optional<Student>in the Service? -
What HTTP code to return after a successful deletion?
Deliverables
To submit:
- Complete source code of the project
- Postman screenshots showing the 6 successful tests
- H2 Console screenshot with data
- At least 2 completed exercises
- Document with answers to the review questions
Evaluation Criteria
| Criterion | Points |
|---|---|
| Correctly implemented layered architecture | 20% |
| Student entity with validations | 15% |
| Functional repository | 10% |
| Service with business logic | 20% |
| Controller with all CRUD endpoints | 20% |
| Successful tests (Postman/Browser) | 10% |
| Additional exercises | 5% |
| Total | 100% |
Summary
What You Learned
- Layered architecture (Controller → Service → Repository → Database)
- Creating a complete REST CRUD API
- CRUD operations: Create, Read, Update, Delete
- Data validation with Bean Validation
- Handling HTTP status codes
- Using H2 for development
- API testing with Postman
Best Practices
- Separate responsibilities into distinct layers
- Validate data at entry (Controller)
- Handle errors properly (custom exceptions)
- Use appropriate HTTP codes
- Test all endpoints
- Document the API
Next Steps
In the next practical works, we will see:
- TP4: Securing with Spring Security
- TP5: Documentation with Swagger/OpenAPI
- TP6: Deployment in production