Imported from Dragomitch/PAE-PostRemise (
backend/AGENTS.md). Install upstream withnpx skills add Dragomitch/PAE-PostRemise --skill backend. Copyright stays with the author.
EMA Backend Guide
Project Overview
This document provides instructions for working on the Java backend of the Erasmus Management Application. The backend exposes a REST API built with Spring Boot and handles business logic, persistence and security.
Tech Stack
List the main technologies and tools used in the project:
- Language: Java 21
- Framework: Spring Boot 3
- Database: PostgreSQL
- Build Tool: Maven
- Deployment: Docker/Docker Compose
- Testing: JUnit 5 (Jupiter) with Mockito and Spring Boot Test
Project Structure
backend/
├── Dockerfile
src/
├── main/
│ ├── java/
│ ├── resources/
│ └── webapp/
└── test/
├── java/ # mirrors the main package structure
└── java/.../persistence/mocks # in-memory DAOs used by UnitTestConfig
pom.xml
Development Guidelines
Code Style
- Follow the Google Java Style Guide.
- Use the Maven formatter plugin before committing.
- Keep methods short and well-documented.
Naming Conventions
- File naming: match the public class name.
- Variable naming: camelCase.
- Function naming: camelCase.
- Class naming: PascalCase.
Git Workflow
- Branch from
masterusingfeature/orbugfix/prefixes. - Write meaningful commit messages in English.
- Open a Pull Request with a summary of changes and link issues when relevant.
Environment Setup
Development Requirements
- Java 21
- Maven 3.9+
- A running PostgreSQL instance
Installation Steps
# 1. Clone the project
git clone [repository-url]
# 2. Build and run tests
mvn verify
# 3. Start the application (dev profile)
mvn spring-boot:run
Core Feature Implementation
Feature Module 1
Requests flow through three layers, all Spring beans:
- Controllers (
web): one@RestControllerper resource under/api/1.0(ApiPaths.BASE). They only bind the request (JSON@RequestBody,@PathVariable,@RequestParam, the authenticatedCurrentUser), call a use case and return its result (lists read by DataTables are wrapped inDataResponse, i.e.{"data": [...]}). Roles are checked with@PreAuthorize(ApiPaths.PROFESSOR)etc. Errors are rendered byApiExceptionHandleronly. - Use cases (
uccontrollers):@Serviceclasses, annotated@Transactionalat class level (queries@Transactional(readOnly = true)), without any web annotation. Nested use-case calls join the caller's transaction; any runtime exception rolls it back. - DAOs (
persistence): see below.
@RestController
@RequestMapping(ApiPaths.BASE + "/users")
public class UserController {
@GetMapping
@PreAuthorize(ApiPaths.PROFESSOR)
public DataResponse<UserDto> showAll() {
return new DataResponse<>(userUcc.showAll());
}
}
Security (config/SecurityConfig, security): stateless. Sign-in (POST /api/1.0/session, JSON {username, password}) issues a signed HS256 JWT (sub = user id, role, iat, exp) in the HttpOnly, SameSite=Lax session cookie (app.session.validity, default 12h; app.session.cookie-secure, default false). The OAuth2 resource server validates it from the cookie or an Authorization: Bearer header. /api/** requires authentication except POST /session, POST /users and GET /options. CSRF uses the SPA recipe: the XSRF-TOKEN cookie must be echoed in the X-XSRF-TOKEN header of every state-changing request (csrf() in MockMvc tests).
Feature Module 2
Persistence is handled by JDBC DAOs under persistence/implementations (@Repository). They prepare their statements through DalBackendServices, which uses the connection of the current Spring transaction (DataSourceUtils.getConnection) and throws if no transaction is active. Updates check the entity version and throw a ConcurrentModificationException (answered with error 120) when it is stale. Unit tests replace the DAOs with the in-memory mocks from src/test/java/.../persistence/mocks via UnitTestConfig (emptied before each test by MockDaoResetListener); the *IT tests run the real DAOs against an embedded PostgreSQL.
DAO contracts (asserted by the *IT tests against PostgreSQL; the mocks follow them too):
- Every statement is closed (try-with-resources) and every SQL failure is a
FatalExceptioncarrying theSQLExceptionas cause. - An update that matches no row throws
ConcurrentModificationException: stale version (optimistic locking) or unknown id. Updates give the DTO its new version.denial_reasonshas no version column, soDenialReasonDao.updateonly detects an unknown id. - Optional relations are LEFT-joined and
nullwhen absent: a partner without option (emptygetOptions()), a mobility choice / mobility / payment without partner or country. - An unknown
findAllfilter is aBusinessException(INVALID_MOBILITY_CHOICE_FILTER_323,INVALID_PARTNER_FILTER_709). - A nominated student shares the id and the version of its user (
NominatedStudentDao.createstores and returns the DTO's version). UserDao.promoteToProfessor(int userId, int expectedVersion)andpromoteToProfessor(String username, int expectedVersion)write only the role and the version;UserController'sPUT /api/1.0/users/{id}/promoteuses the id variant,PUT /api/1.0/users/by-username/{username}/promotethe username one (both professors only, CSRF-protected; a professor is left unchanged).- Every
ErrorFormatcode must exist insrc/main/resources/errors.json(ErrorCatalogueTest):ApiExceptionHandleranswers aBusinessException(e.g. an unknown filter, 323/709) and aConcurrentModificationException(120, "reload the data") with 400 and the catalogue entry. - The API leaves
nullproperties out of the JSON (NON_NULL,JacksonConfig), as the legacy UI expects; the UI checks optional properties with== null, which also covers an explicitnull.
Testing Strategy
Unit Testing
- Testing framework: JUnit 5 (
org.junit.jupiter); useassertThrowsinstead of@Test(expected = ...) - Test coverage requirements: aim for 80%
- Test file organization: mirror package structure under
src/test/java - The stateful mock DAOs implement
ResettableMock;MockDaoResetListener(registered for every Spring test insrc/test/resources/META-INF/spring.factories) empties them before every test method, before the test's own@BeforeEach, so tests must not depend on each other. Check withmvn test -Dsurefire.runOrder=random '-Djunit.jupiter.testmethod.order.default=org.junit.jupiter.api.MethodOrderer$Random'. - Production code never writes to the console (
SourceHygieneTest); log through SLF4J (LoggerFactory.getLogger).
Integration Testing
- Test scenarios: every DAO method against the real schema (
*IT, run by Failsafe inmvn verify) - Testing tools: an embedded PostgreSQL (io.zonky, no Docker) loaded with
SQLRessources/init.sql;AbstractDaoITruns each test in a transaction that is always rolled back, with the fixtures ofsrc/test/resources/db/fixtures
End-to-End Testing
- Test workflow: executed via Docker Compose with the frontend
- Automation tools: Maven profiles
Deployment Guide
Build Process
mvn clean package
Deployment Steps
- Build the Docker image
- Configure environment variables
- Run
docker compose up backend - Verify the API is reachable on port 8080
Environment Variables
DB_HOST=
DB_PORT=
DB_NAME=
DB_USERNAME=
DB_PASSWORD=
JWT_SECRET= # 32+ bytes, signs the session JWT
APP_SESSION_VALIDITY=12h # session lifetime
APP_SESSION_COOKIE_SECURE=false # true behind HTTPS
APP_CORS_ALLOWED_ORIGINS=http://localhost:4200
Performance Optimization
Backend Optimization
- Index frequently queried columns
- Cache heavy computations when possible
- Use connection pooling
Security Considerations
Data Security
- Validate all input data
- Use prepared statements to avoid SQL injection
- Sanitize user-facing output
Authentication & Authorization
- Spring Security manages authentication (stateless JWT in the
sessioncookie, OAuth2 resource server) @PreAuthorizerole checks on every controller method (ROLE_PROFESSOR,ROLE_STUDENT)- CSRF protection with the
XSRF-TOKENcookie /X-XSRF-TOKENheader - 401 and 403 are rendered by
ApiExceptionHandler, like every other API error
Monitoring and Logging
Application Monitoring
- Use Spring Boot Actuator for health checks
- Track errors via logs
- Optionally integrate with Prometheus/Grafana
Log Management
- Log levels configured in
application.properties - Use a rolling file appender
- Store logs within the container or forward to a central system
Common Issues
Issue 1: Database connection failure
Solution: Verify environment variables and ensure the PostgreSQL container is running.
Issue 2: Tests fail due to context loading
Solution: Unit tests should use @SpringJUnitConfig(UnitTestConfig.class) (mock DAOs, no database). Controller tests use @WebMvcTest with @Import(WebTestConfig.class) (real security chain, JSON mapping) and @MockBean use cases; authenticate with TestUsers.professor() / student() and add csrf() to state-changing requests. @SpringBootTest loads the real beans; it needs no database as long as the test does not hit a DAO.
Reference Resources
Changelog
v1.0.0 (YYYY-MM-DD)
- Initial backend release
- Basic REST endpoints implemented
