Skip to main content

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​

  1. Layered Architecture
  2. Project Creation
  3. Creating the Student Entity
  4. Creating the Repository
  5. Creating the Service
  6. Creating the Controller
  7. H2 Configuration
  8. Testing the Endpoints
  9. 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:

ParameterValue
ProjectMaven
LanguageJava
Spring Boot3.2.0 (or latest stable version)
Groupcom.example
Artifactstudent-management
Namestudent-management
Package namecom.example.student
PackagingJar
Java17

Dependencies to add:

  1. Spring Web - To create the REST API
  2. Spring Data JPA - For persistence
  3. H2 Database - In-memory database
  4. Validation - To validate data
  5. Spring Boot DevTools - For automatic reload

2.2 Import into VS Code​

  1. Download the project (ZIP)
  2. Unzip into your workspace
  3. Open VS Code
  4. File → Open Folder → Select student-management
  5. 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​

AnnotationRole
@EntityMarks the class as a JPA entity
@Table(name = "students")SQL table name
@IdPrimary key
@GeneratedValue(strategy = GenerationType.IDENTITY)Auto-increment
@ColumnColumn configuration
@NotBlankValidation: non-empty field
@SizeValidation: min/max size
@EmailValidation: 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:

MethodDescription
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 email
  • existsByEmail(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:

  1. Validation: Check if email exists before creation
  2. Error Handling: Throw exceptions if student not found
  3. Orchestration: Coordinate calls to the Repository
  4. 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 MethodEndpointActionSuccess Code
POST/api/studentsCreate a student201 CREATED
GET/api/studentsList all students200 OK
GET/api/students/\{id\}Retrieve a student200 OK
PUT/api/students/\{id\}Update a student200 OK
DELETE/api/students/\{id\}Delete a student204 NO CONTENT
GET/api/students/email/\{email\}Search by email200 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:

FieldValue
JDBC URLjdbc:h2:mem:studentdb
User Namesa
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​

  1. Download Postman: https://www.postman.com/downloads/
  2. 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​

  1. Access http://localhost:8080/h2-console
  2. Log in
  3. 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:

  1. Modify the Student.java class
  2. Add the program field
  3. Generate getters/setters
  4. Test creating a student with a program

Exercise 2: Search by Last Name​

Objective: Implement a search by last name

Instructions:

  1. Add a method in StudentRepository:
    List<Student> findByLastName(String lastName);
  2. Add a method in StudentService
  3. Add an endpoint in StudentController:
    @GetMapping("/lastname/\{lastName\}")
  4. Test with Postman

Exercise 3: Pagination​

Objective: Add pagination to the student list

Instructions:

  1. 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);
    }
  2. Modify the Controller to accept page and size parameters
  3. Test: http://localhost:8080/api/students?page=0&size=5

Exercise 4: Custom Error Handling​

Objective: Create global error handling

Instructions:

  1. Create ResourceNotFoundException.java:
package com.example.student.exception;

public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String message) {
super(message);
}
}
  1. 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);
}
}
  1. Use ResourceNotFoundException in the Service
  2. Test by trying to access a non-existent student

Exercise 5: Advanced Validation​

Objective: Add custom validations

Instructions:

  1. 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;
  2. Test by creating a student with an invalid age
  3. Observe the 400 BAD REQUEST code

Exercise 6: Migration to PostgreSQL​

Objective: Replace H2 with PostgreSQL

Instructions:

  1. Add the PostgreSQL dependency in pom.xml
  2. 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
  3. Create the studentdb database in PostgreSQL
  4. Restart the application
  5. 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​

  1. What is the responsibility of each layer (Controller, Service, Repository)?

  2. Why separate the code into layers?

  3. What is the difference between @RestController and @Service?

  4. What does the @Valid annotation do in the Controller?

  5. How does Spring Data JPA generate SQL queries?

  6. What is the difference between save() for create and update?

  7. Why use Optional<Student> in the Service?

  8. What HTTP code to return after a successful deletion?


Deliverables​

To submit:

  1. Complete source code of the project
  2. Postman screenshots showing the 6 successful tests
  3. H2 Console screenshot with data
  4. At least 2 completed exercises
  5. Document with answers to the review questions

Evaluation Criteria​

CriterionPoints
Correctly implemented layered architecture20%
Student entity with validations15%
Functional repository10%
Service with business logic20%
Controller with all CRUD endpoints20%
Successful tests (Postman/Browser)10%
Additional exercises5%
Total100%

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

Additional Resources​

Documentation​

Tutorials​


Back to Module 3