bilhej/CODING_GUIDELINES.md
Joakim Mörling 3fa4f6831e docs: add coverage thresholds, ./gradlew coverage, and LSP warning discipline
AGENTS.md:
  - Add "./gradlew coverage" to All-in-one quick-start section
  - Add "npm run test:coverage" to Frontend commands
  - Add Coverage section: command, threshold table (70% lines, 60%
    branches, 70% functions), HTML report paths for both layers
  - Note that coverage is enforced during ./gradlew check

CODING_GUIDELINES.md:
  - Section 1 (General Principles): add "Treat warnings as mistakes"
    rule — LSP diagnostics, compiler warnings, and lint warnings are
    bugs that must be fixed before commit
  - Known false positives (Lombok, getActivePinia) must be suppressed
    explicitly at the narrowest scope with a comment explaining why
  - Uncommented suppressions are treated as errors
  - Section 7 (Testing): add Coverage subsection with thresholds table,
    command reference, report paths, and enforcement rule (PRs must
    maintain or improve coverage)
2026-05-15 12:16:16 +02:00

352 lines
13 KiB
Markdown

# Coding Guidelines — BilHej
> **Note:** This file is loaded by OpenCode via `AGENTS.md` as an external
> instruction source. It is referenced by `opencode.json` instructions.
> Keep it current — OpenCode uses it for decision-making.
Conventions and standards for the BilHej codebase. These exist to keep the project consistent — especially important as a solo developer returning to the code after weeks or months away.
---
## 1. General Principles
- **Readability over cleverness.** Write code you can understand at 2 AM six months from now.
- **English for code, Swedish for user-facing strings.** Variable names, comments, commit messages: English. UI text, error messages shown to users, templates: Swedish.
- **No commented-out code.** Delete it. Git history keeps it if needed.
- **Keep functions small.** A function should do one thing. If it's over 30 lines, it probably does too much.
- **No magic numbers.** Use named constants or enums.
- **Treat warnings as mistakes.** LSP diagnostics, compiler warnings, and lint
warnings are bugs. Never commit code that produces them. If a warning is a
known false positive (e.g. Lombok `@RequiredArgsConstructor` triggering
"uninitialized final field"), suppress it explicitly at the narrowest scope
with a comment explaining why:
- Java: `@SuppressWarnings("...") // Lombok generates constructor`
- TypeScript: `// @ts-expect-error — pinia getActivePinia returns null in test context`
Uncommented suppressions are indistinguishable from ignoring a real problem
and are treated as errors.
---
## 2. Git
### Branch Model
Three permanent branches, plus short-lived work branches:
```
master ──────────────────────────────────────●──● (production, deployed)
↗ ↗
develop ────────●──────────●────────●───────●──● (integration)
↗ ↗ ↗
feature/foo ──●──●──●───→┘ │
feature/bar ───────────────────────●──●─────→┘
```
| Branch | Purpose | Branches from | Merges to |
|--------|---------|---------------|-----------|
| `master` | Production — what runs on the server | — | — |
| `develop` | Integration — all work branches land here | `master` | `master` (on release) |
| `feature/*` | New feature work | `develop` | `develop` |
| `fix/*` | Bug fix | `develop` | `develop` |
| `chore/*` | Maintenance, upgrades, docs | `develop` | `develop` |
### Rules
- **Never commit directly to `master` or `develop`.** All work happens in feature/fix/chore branches.
- **Merge strategy**: fast-forward or merge — either is fine for a solo dev project. Prefer simple over complex.
- **Keep branches short-lived.** Merge back to `develop` within a few days. Don't let branches drift.
- **Update before merging.** Pull latest `develop` into your branch before merging back to avoid surprises.
- **Tag releases on `master`**: `v0.1.0`, `v0.2.0`, etc. These mark deployable states.
- **Rebase**: only rebase your own unpublished branches. Once pushed/shared, merge instead.
- **CI triggers**: push to `develop` → run tests. Push to `master` → run tests + deploy.
- **Squash trivial fix commits** before pushing (typo fixes, formatting, etc.).
- **Never commit `.env`, credentials, or secrets. `.env.example` only.**
### Branch Naming
```
feature/plate-lookup
fix/stripe-webhook-timeout
chore/upgrade-spring-boot-3.3
```
### Commit Messages
```
<type>: <imperative verb, lowercase, no period>
feat: add license plate input component with validation
fix: handle empty response from Transportstyrelsen lookup
refactor: extract letter preview into composable
chore: bump vue to 3.5
```
Types: `feat`, `fix`, `refactor`, `chore`, `docs`, `test`, `style`
---
## 3. Frontend — Vue.js 3
### File Naming
| Type | Convention | Example |
|--------------|-----------------------------|----------------------------------|
| Page | PascalCase, in `pages/` | `HomePage.vue`, `OrderHistoryPage.vue` |
| Component | PascalCase, in `components/`| `PlateInput.vue`, `LetterPreview.vue` |
| Composable | camelCase, `use` prefix | `useAuth.ts`, `usePayment.ts` |
| Store | camelCase, in `stores/` | `authStore.ts`, `orderStore.ts` |
| API module | camelCase, in `api/` | `orders.ts`, `templates.ts` |
### Component Structure
```vue
<script setup>
// 1. Imports
import { ref, computed } from 'vue'
import { useAuth } from '@/composables/useAuth'
// 2. Props & Emits
const props = defineProps({
plate: { type: String, required: true }
})
const emit = defineEmits(['submit'])
// 3. Composables
const { user } = useAuth()
// 4. Reactive state
const loading = ref(false)
// 5. Computed
const isValid = computed(() => /^[A-Z]{3}\d{3}$/.test(props.plate))
// 6. Methods
async function handleSubmit() {
loading.value = true
// ...
emit('submit')
}
</script>
<template>
<div class="plate-input">
<!-- template here -->
</div>
</template>
<style scoped>
.plate-input {
/* scoped styles */
}
</style>
```
### Conventions
- Use `<script setup>` with Composition API. No Options API.
- Props: type and required/default always declared.
- `v-model` bindings use `defineModel()` or explicit `modelValue` + `update:modelValue`.
- No global CSS unless it's a design token or reset. Component styles are scoped.
- Prefer Pinia stores over prop drilling for shared state (auth, current order).
- API calls live in `api/*.ts` modules, not in components.
- Use `fetch` or `axios` via a single configured instance (base URL, auth header interceptor).
---
## 4. Backend — Spring Boot 4
### Package Structure
```
se.bilhalsning
├── config # @Configuration classes (SecurityConfig, StripeConfig, CorsConfig)
├── controller # @RestController classes, one per resource
├── dto # Request/response DTOs, use records where possible
├── entity # @Entity JPA classes
├── repository # Spring Data JPA repositories
├── service # @Service business logic
├── security # JwtFilter, UserDetailsServiceImpl
├── exception # Custom exceptions + @ControllerAdvice
└── mapper # Entity ↔ DTO mapping (MapStruct or manual)
```
### Naming Conventions
| Layer | Convention | Example |
|--------------|-----------------------------------|----------------------------------|
| Controller | `{Resource}Controller` | `OrderController` |
| Service | `{Resource}Service` | `OrderService` |
| Repository | `{Entity}Repository` | `OrderRepository` |
| Entity | `{SingularNoun}` | `Order`, `User`, `Template` |
| DTO request | `{Action}{Resource}Request` | `CreateOrderRequest` |
| DTO response | `{Resource}Response` | `OrderResponse` |
### Controller Pattern
```java
@RestController
@RequestMapping("/api/orders")
@RequiredArgsConstructor
public class OrderController {
private final OrderService orderService;
@PostMapping
public ResponseEntity<OrderResponse> create(
@Valid @RequestBody CreateOrderRequest request,
@AuthenticationPrincipal UserDetails user) {
return ResponseEntity.ok(orderService.create(request, user));
}
@GetMapping
public ResponseEntity<List<OrderResponse>> list(
@AuthenticationPrincipal UserDetails user) {
return ResponseEntity.ok(orderService.list(user));
}
}
```
### Conventions
- Controllers stay thin. No business logic. Delegate to services.
- Services contain business logic. Services call repositories and other services.
- Use constructor injection with `@RequiredArgsConstructor`. No `@Autowired` field injection.
- DTOs: prefer Java records over classes for immutable data.
- Validation: `@Valid` on controller parameters, `jakarta.validation` annotations on DTOs.
- Exception handling: `@ControllerAdvice` + `@ExceptionHandler`. Never return 500 with a stack trace to the client.
- All responses: `ResponseEntity<T>`. Never return bare entities.
- Entity fields use `snake_case` column naming explicitly (`@Column(name = "created_at")`).
- Database migrations: Flyway. All schema changes go through SQL migration files in `db/migration/`.
- Lombok: `@RequiredArgsConstructor`, `@Getter`, `@Setter`, `@NoArgsConstructor` are all fine. Prefer records for DTOs.
### API Path Conventions
```
GET /api/orders List user's orders
POST /api/orders Create order
GET /api/orders/{id} Get single order
GET /api/templates List active templates
POST /api/auth/register Register user
POST /api/auth/login Login, returns JWT
GET /api/vehicles/{plate} Get public vehicle info
```
---
## 5. Database
- Table names: `snake_case`, plural (`orders`, `users`, `templates`).
- Column names: `snake_case`.
- Primary keys: `UUID`, generated in application code (not DB-generated), column named `id`.
- Timestamps: `created_at`, `updated_at`. Use `Instant` in Java.
- Enums: stored as `VARCHAR` in DB, mapped to Java enums. Prefer enums over magic strings.
- Indexes: add for any column used in WHERE or JOIN clauses. Add for every FK.
---
## 6. Error Handling
### Frontend
```javascript
// api/client.ts — centralized fetch wrapper
async function request(url, options) {
const response = await fetch(`${BASE_URL}${url}`, {
...options,
headers: {
'Content-Type': 'application/json',
...(authToken() && { Authorization: `Bearer ${authToken()}` }),
...options.headers,
},
})
if (!response.ok) {
const error = await response.json().catch(() => ({}))
throw new ApiError(response.status, error.message || 'Något gick fel')
}
return response.json()
}
```
### Backend
```java
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(OrderNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(OrderNotFoundException ex) {
return ResponseEntity.status(404).body(new ErrorResponse(ex.getMessage()));
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidation(MethodArgumentNotValidException ex) {
String message = ex.getBindingResult().getFieldErrors().stream()
.map(e -> e.getField() + ": " + e.getDefaultMessage())
.collect(Collectors.joining(", "));
return ResponseEntity.badRequest().body(new ErrorResponse(message));
}
}
```
- Always return a consistent JSON error shape: `{ "message": "..." }`
- Log the actual exception server-side. Never leak stack traces to the client.
---
## 7. Testing
This project follows **Test-Driven Development (TDD)**. Write tests before
or alongside implementation. Every feature ticket should include tests in
the same PR — never merge code without corresponding tests.
- Backend: JUnit 5 + Mockito. Service layer tests as unit tests. Controller tests with `@WebMvcTest`.
- Frontend: Vitest for composables and utility functions. Component tests with Vue Test Utils.
- E2E: Playwright (`npm run test:e2e`). Tests in `frontend/e2e/`. Requires `docker compose up`.
- Test naming: `shouldXxxWhenYyy` — e.g., `shouldReturn404WhenPlateNotFound`.
- Aim for test coverage on business logic, not on getters/setters/boilerplate.
- All database interaction in tests must go through JPA repositories
or EntityManager. Never use JdbcTemplate, DataSource queries, or
raw SQL in test code. Tests interact with the database the same way
production code does: through the ORM.
### Coverage
```bash
./gradlew coverage # backend + frontend tests with coverage
```
Coverage is enforced via `./gradlew check`. Thresholds:
| Layer | Lines | Branches | Functions |
|----------|-------|----------|-----------|
| Backend | 70% | 60% | — |
| Frontend | 70% | 60% | 70% |
- Backend: JaCoCo (`backend/build/reports/jacoco/index.html`).
- Frontend: Vitest v8 provider (`frontend/coverage/index.html`).
- PRs must maintain or improve coverage levels. If a new feature changes
coverage, update the test suite — never lower thresholds without discussion.
---
## 8. Linting & Formatting
| Layer | Tool |
|-----------|---------------|
| Frontend | ESLint + Prettier |
| Backend | Checkstyle or Spotless (Google Java Format) |
Run before every commit manually. CI will enforce it later.
---
## 9. Security Checklist (per PR)
- [ ] No secrets/logins in committed code
- [ ] JWT endpoints correctly annotated with `@PreAuthorize` or security filter chain
- [ ] User-visible strings use Swedish
- [ ] API responses never expose internal IDs or stack traces
- [ ] Input validated on both frontend and backend