RESTful API backend for student management developed with Spring Boot and Hexagonal Architecture. It implements CRUD operations with a focus on maintainability, scalability, clean code, and best practices.
- Hexagonal Architecture with clear separation of responsibilities
- DTOs for requests and responses
- Centralized error handling with custom exceptions, controller advisor, error catalog and use of a specific model for error normalization
- Endpoint-independent versioning via HTTP method
- Automated mapping between entities and DTOs
- Robust validation of input data
src/
βββ main/
β βββ java/
β β βββ com/
β β βββ backend.student.app/
β β βββ application/ β€ Application (Use Cases and Ports)
β β β βββ ports/
β β β β βββ input β€ Input Ports (Service Port)
β β β β βββ output β€ Output Ports (Persistence Port)
β β β βββ usecase/ β€ Interactors (Use Cases)
β β βββ domain/ β€ Core - Domain
β β β βββ model/ β€ Models
β β β βββ exception/ β€ Custom Exceptions
β β βββ infrastructure/ β€ Infrastructure (Adapters)
β β β βββ adapters/
β β β βββ input/ β€ Input Adapters
β β β β βββ rest/
β β β β βββ dto/ β€ Data Transfer Obejects
β β β β β βββ request/
β β β β β βββ response/
β β β β βββ mapper/ β€ Mappers DTO <-> Model
β β β β βββ GlobalControllerAdviser.java β€ Error Handler
β β β β βββ StudentRestController.java β€ REST Controller
β β β βββ output/ β€ Output Adapters
β β β βββ database/
β β β βββ postgres/
β β β βββ entity/ β€ Database Entities
β β β βββ mapper/ β€ Mappers Entity <-> Model
β β β βββ repository/ β€ JPA - Hibernate
β β β βββ StudentPersistenceAdapter/ β€ Persistence Adapter
β β βββ utils/
β β β βββ ErrorCatalog.java β€ Error Catalog
β β βββ StudentApplication.java β€ Spring Boot Main Run File
β βββ resources/
β βββ application.yml β€ Configurations
βββ test/ β€ Tests
docker-compose.yml β€ Docker Compose File
- Core: Java 21, Spring Boot 3.5.3
- Architecture: Hexagonal (Ports & Adapters)
- Persistence: Spring Data JPA, Hibernate, PostgreSQL
- Anotation: Lombok
- Mapping: MapStruct
- Validation: Jakarta Validation
- Dependency Management: Maven
- Container Platform: Docker
-
Requirements:
- Java 21
- PostgreSQL 14+
- Maven 3.8+
- Docker 4.42+
-
Clone repository:
git clone https://github.com/alexdevzz/spring-hex-student-backend.git cd spring-hex-student-backend
-
Configure database (application.yml):
spring: datasource: url: jdbc:postgresql://localhost:5432/your-db username: your-user password: your-pass driver-class-name: org.postgresql.Driver jpa: hibernate: ddl-auto: update
-
Run:
mvn spring-boot:run
Method | Endpoint | Description |
---|---|---|
POST | students/v1/api/ |
Create new student |
GET | students/v1/api/{id} |
Get student by ID |
GET | students/v1/api/ |
List all students |
PUT | students/v1/api/{id} |
Update student |
DELETE | students/v1/api/{id} |
Delete student |
@RestControllerAdvice
public class GlobalControllerAdviser {
@ResponseStatus(HttpStatus.NOT_FOUND)
@ExceptionHandler(StudentNotFoundException.class) // com.backend.student.app.domain.exception.StudentNotFoundException
public ErrorResponse handleStudentNotFoundException() { // com.backend.student.app.domain.model.ErrorResponse
return ErrorResponse.builder()
.code(STUDENT_NOT_FOUND.getCode()) // com.backend.student.app.utils.ErrorCatalog.STUDENT_NOT_FOUND
.message(STUDENT_NOT_FOUND.getMessage()) // com.backend.student.app.utils.ErrorCatalog.STUDENT_NOT_FOUND
.timestamp(LocalDateTime.now())
.build();
}
// More handlers ...
}
@Builder @Getter @Setter
@AllArgsConstructor @NoArgsConstructor
public class StudentCreateRequest {
@NotBlank(message = "Field firt_name cannot be empty or null")
private String firstName;
@NotBlank(message = "Field last_name cannot be empty or null")
private String lastName;
@NotBlank(message = "Field address cannot be empty or null")
private String address;
@NotNull(message = "Flied age cannot be null")
private Integer Age;
}
@Mapper(componentModel = "spring") // using Mapstruct ...
public interface StudentRestMapper {
@Mapping(target = "id", ignore = true)
Student toStudent(StudentCreateRequest request);
StudentResponse toStudentResponse(Student student);
List<StudentResponse> toStudentResponseList(List<Student> studentList);
}
// Port
public interface StudentPersistencePort {
Optional<Student> findById(Long id);
List<Student> findAll();
Student save(Student student);
void deleteById(Long id);
}
// Adapter
@Component
@RequiredArgsConstructor
public class StudentPersistenceAdapter implements StudentPersistencePort {
private final StudentJpaRepository studentJpaRepository;
private final StudentPersistenceMapper studentPersistenceMapper;
@Override
public Optional<Student> findById(Long id) {
return studentJpaRepository.findById(id)
.map(studentPersistenceMapper::ToStudent);
}
// More methods implementations ....
}
@RestController
@RequestMapping("/students")
@RequiredArgsConstructor
public class StudentRestController {
private final StudentServicePort servicePort;
private final StudentRestMapper restMapper;
// ...
@GetMapping("/v1/api/{id}")
public StudentResponse findById(@PathVariable Long id) {
return restMapper.toStudentResponse(servicePort.findStudentById(id));
}
@PostMapping("/v1/api")
public ResponseEntity<StudentResponse> create(@Valid @RequestBody StudentCreateRequest request) {
return ResponseEntity.status(HttpStatus.CREATED)
.body(restMapper.toStudentResponse(servicePort.createStudent(restMapper.toStudent(request))));
}
// ...
}
erDiagram
STUDENT {
Long id PK
String first_name
String last_name
String address
String Age
}
docker-compose up -d
Docker Compose Configuration:
services:
postgres:
image: postgres:14.18-bookworm
container_name: postgres_spring_student_backend_container
environment:
POSTGRES_USER: your-user
POSTGRES_PASSWORD: your-pass
POSTGRES_DB: your-db
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
restart: unless-stopped
volumes:
postgres_data:
- Fork the project
- Create your feature branch (
git checkout -b feature/new-feature
) - Commit your changes (
git commit -am 'Add new feature'
) - Push to the bransh (
git push origin feature/new-feature
) - Open a Pull Request
Distributed under the MIT License. See LICENSE
for more information.