Covers environment setup, daily workflow, three walkthroughs (add domain, add endpoint, add frontend page), and a conventions reference. All file paths verified against current main. Walkthroughs follow TDD order (Red before Green). Resolves all persona feedback from issue #398. Closes #398 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
12 KiB
Contributing to Familienarchiv
For the full collaboration rules (issue workflow, PR process, Red/Green TDD, commit conventions) see COLLABORATING.md. For coding style see CODESTYLE.md. For the system architecture see docs/ARCHITECTURE.md. For domain terminology see docs/GLOSSARY.md.
1. Environment setup
Prerequisites: Java 21 (SDKMAN), Node 24 (nvm), Docker
Activate SDKMAN and nvm before running java, mvn, node, or npm:
source "$HOME/.sdkman/bin/sdkman-init.sh"
export NVM_DIR="$HOME/.nvm" && [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
2. Daily development workflow
Startup order — services must start in this sequence:
# 1. Start PostgreSQL and MinIO
docker compose up -d db minio
# 2. Start the backend (separate terminal)
cd backend && ./mvnw spring-boot:run
# 3. Start the frontend (separate terminal)
cd frontend && npm install && npm run dev
npm installalso wires up the Husky pre-commit hook via thepreparescript. Run it before your first commit, or the hook will fail to execute.
Do not use
docker-compose.ci.ymllocally — it disables the bind mounts that the dev workflow depends on.
Regenerate TypeScript types after any backend API change:
# Backend must be running with dev profile
cd frontend && npm run generate:api
⚠️ Forgetting this step is the most common cause of "where did my TypeScript type go?" — always regenerate after changing models or endpoints.
Test commands:
cd backend && ./mvnw test # backend unit + slice tests
cd frontend && npm run test # Vitest unit tests
cd frontend && npm run check # svelte-check (type errors)
cd frontend && npx playwright test # Playwright e2e tests
Branch naming: <type>/<issue-number>-<short-description>, e.g. feat/398-contributing
Commits: one logical change per commit; reference the Gitea issue:
feat(person): add aliases endpoint
Closes #42
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Test-type decision matrix
| What you're testing | Test type | Tool |
|---|---|---|
| Service business logic, calculations | Unit test | JUnit + @ExtendWith(MockitoExtension.class) |
| HTTP contract, request validation, error codes | Controller slice test | @WebMvcTest |
Server load function |
Vitest unit | Import directly, mock fetch |
| Shared UI component | Vitest browser-mode | render() + getByRole() |
| Full user-facing flow, navigation, forms | E2E | Playwright |
3. Walkthrough A — Add a new domain
Example: adding a citation domain (formal references to documents).
Both the backend and frontend are organised domain-first. A new domain means adding a package on both sides under the same name.
Backend
-
Create
backend/src/main/java/org/raddatz/familienarchiv/citation/ -
Add entity, repository, service, controller, and DTOs flat in the package:
- Entity
Citation.java— annotate with@Entity @Data @Builder @NoArgsConstructor @AllArgsConstructor; use@GeneratedValue(strategy = GenerationType.UUID)for theidfield; add@Schema(requiredMode = REQUIRED)on every field the backend always populates - Repository
CitationRepository.java— extendsJpaRepository<Citation, UUID> - Service
CitationService.java—@Service @RequiredArgsConstructor; write methods@Transactional, read methods unannotated; cross-domain data goes through the other domain's service, never its repository - Controller
CitationController.java—@RestController @RequestMapping("/api/citations")
- Entity
-
Add
@RequirePermission(Permission.WRITE_ALL)on everyPOST,PUT,PATCH, andDELETEendpoint — this is not optional. Read-onlyGETendpoints stay unannotated. -
Add a Flyway migration:
backend/src/main/resources/db/migration/V{n}__{description}.sql(use the next sequential number after the highest existing one). -
Write failing tests before any implementation (Red step):
- Service unit test for business logic (
@ExtendWith(MockitoExtension.class)) @WebMvcTestslice test for each HTTP endpoint
- Service unit test for business logic (
-
Rebuild with
--spring.profiles.active=devand runnpm run generate:apiinfrontend/.
Frontend
-
Create
frontend/src/lib/citation/— domain-specific Svelte components and TypeScript utilities go here. -
Add routes under
frontend/src/routes/citations/as needed. -
Add a per-domain
README.mdin both the backend package folder andfrontend/src/lib/citation/(per DOC-6).
Documentation
- Update
docs/ARCHITECTURE.mdSection 2 to include the new domain. - Update
docs/GLOSSARY.mdif new terms are introduced. - Update the ESLint boundary allow-list in
frontend/eslint.config.jsif the domain needs to import from another domain.
4. Walkthrough B — Add a new endpoint
Example: POST /api/persons/{id}/aliases — attach a name alias to an existing person.
Red (write failing tests first)
-
Write a failing
@WebMvcTestcontroller slice test:@Test void addAlias_returns201_whenAliasCreated() { ... } -
Write a failing service unit test:
@Test void addAlias_throwsNotFound_whenPersonDoesNotExist() { ... }
Green (implement)
-
Add the service method in
PersonService.java:@Transactional public PersonNameAlias addAlias(UUID personId, PersonNameAliasDTO dto) { ... } -
Add the controller method in
PersonController.java:@PostMapping("/{id}/aliases") @RequirePermission(Permission.WRITE_ALL) public ResponseEntity<PersonNameAlias> addAlias(@PathVariable UUID id, @RequestBody PersonNameAliasDTO dto) { ... }@RequirePermission(Permission.WRITE_ALL)on every state-mutating endpoint — not optional. -
Validate user-supplied inputs at the controller boundary:
if (dto.name() == null || dto.name().isBlank()) throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "name is required");Validate at system boundaries; trust internal service code.
-
Use
DomainExceptionfor domain errors:DomainException.notFound(ErrorCode.PERSON_NOT_FOUND, "Person not found: " + id)If you need a new error code, add it to
ErrorCode.java, mirror it infrontend/src/lib/shared/errors.ts, and add translation keys inmessages/{de,en,es}.json. -
Mark every field the backend always populates with
@Schema(requiredMode = REQUIRED)— this drives TypeScript type generation.
Types and tests
-
Rebuild with
--spring.profiles.active=dev, thennpm run generate:apiinfrontend/.⚠️ Always regenerate types after any API change. This is the #1 cause of "where did my TypeScript type go?"
-
Run the full test suite — all green before committing.
5. Walkthrough C — Add a new frontend page
Example: /persons/[id]/timeline — a chronological event timeline for one person.
Red (write failing test first)
- Write a failing Playwright E2E test for the user flow:
test('timeline shows events in chronological order', async ({ page }) => { await page.goto('/persons/1/timeline'); // assertions... });
Green (implement)
-
Create
frontend/src/routes/persons/[id]/timeline/+page.svelte -
Add
frontend/src/routes/persons/[id]/timeline/+page.server.tsfor the SSR load:import { createApiClient } from '$lib/shared/api.server'; export const load: PageServerLoad = async ({ params, fetch }) => { const api = createApiClient(fetch); const result = await api.GET('/api/persons/{id}', { params: { path: { id: params.id } } }); if (!result.response.ok) throw error(result.response.status, '...'); return { person: result.data! }; }; -
Domain-specific components (e.g.
TimelineEntry.svelte) →frontend/src/lib/person/ -
Shared primitives (e.g. a generic date-range display) →
frontend/src/lib/shared/primitives/ -
UI patterns to follow:
- Back navigation:
import BackButton from '$lib/shared/primitives/BackButton.svelte' - Date display: always append
T12:00:00—new Intl.DateTimeFormat('de-DE', …).format(new Date(val + 'T12:00:00'))— prevents UTC off-by-one errors - Brand colors:
brand-navy,brand-mint,brand-sand(defined insrc/routes/layout.css) - Accessibility: touch targets ≥ 44 px (
min-h-[44px]); focus rings (focus-visible:ring-2 focus-visible:ring-brand-navy);aria-labelon icon-only buttons;aria-live="polite"on dynamic status messages
- Back navigation:
-
Add Paraglide i18n keys in
messages/de.json,messages/en.json,messages/es.json. -
If adding a new error code: mirror in
frontend/src/lib/shared/errors.tsand add translation keys. -
Make all tests green before committing.
6. Conventions reference
Error handling
| Scenario | Pattern |
|---|---|
| Domain entity not found | DomainException.notFound(ErrorCode.X, "…") |
| Permission denied | DomainException.forbidden("…") |
| Concurrent edit conflict | DomainException.conflict(ErrorCode.X, "…") |
| Infrastructure failure | DomainException.internal(ErrorCode.X, "…") |
| Simple controller validation | throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "…") |
New error code: ErrorCode.java → frontend/src/lib/shared/errors.ts → messages/{de,en,es}.json.
DTOs
- Input DTOs live flat in the domain package (e.g.
PersonUpdateDTO.java) - Responses are the entity itself — no separate response DTOs
@Schema(requiredMode = REQUIRED)on every field the backend always populates
Frontend API client
const api = createApiClient(fetch); // from $lib/shared/api.server
const result = await api.GET('/api/persons/{id}', { params: { path: { id } } });
if (!result.response.ok) {
const code = (result.error as unknown as { code?: string })?.code;
throw error(result.response.status, getErrorMessage(code));
}
return { person: result.data! }; // non-null assertion is safe after the ok check
For multipart/form-data (file uploads): bypass the typed client and use raw fetch — the client cannot handle it.
Date handling
| Context | Pattern |
|---|---|
| Form display | German dd.mm.yyyy with auto-dot insertion via handleDateInput() |
| Wire format | ISO 8601 via a hidden <input type="hidden" name="documentDate" value={dateIso}> |
| Display | new Intl.DateTimeFormat('de-DE', …).format(new Date(val + 'T12:00:00')) |
Security checklist (new endpoint)
@RequirePermission(Permission.WRITE_ALL)on everyPOST,PUT,PATCH,DELETE— required, not optional- Validate all user-supplied inputs at the controller boundary before passing to the service
- Parameterised queries only — never interpolate user input into JPQL/SQL strings
- No raw user input in log messages — use
{}placeholders:log.warn("Not found: {}", id) - Validate content-type and size on upload endpoints before reading the stream
Accessibility baseline (new frontend page)
- Touch targets ≥ 44 px on all interactive elements (
min-h-[44px]) - Focus rings on all focusable elements (
focus-visible:ring-2 focus-visible:ring-brand-navy) aria-labelon every icon-only buttonaria-live="polite"on dynamic status messages- Color is never the sole status indicator
Full WCAG 2.1 AA reference: docs/STYLEGUIDE.md.
Lint and format
# Frontend
cd frontend && npm run lint # Prettier + ESLint check
cd frontend && npm run format # Auto-fix formatting
cd frontend && npm run check # svelte-check (type errors)
# Backend — checkstyle is enforced via Maven
cd backend && ./mvnw checkstyle:check