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)
13 KiB
Coding Guidelines — BilHej
Note: This file is loaded by OpenCode via
AGENTS.mdas an external instruction source. It is referenced byopencode.jsoninstructions. 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
@RequiredArgsConstructortriggering "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 contextUncommented suppressions are indistinguishable from ignoring a real problem and are treated as errors.
- Java:
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
masterordevelop. 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
developwithin a few days. Don't let branches drift. - Update before merging. Pull latest
developinto 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 tomaster→ run tests + deploy. - Squash trivial fix commits before pushing (typo fixes, formatting, etc.).
- Never commit
.env, credentials, or secrets..env.exampleonly.
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
<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-modelbindings usedefineModel()or explicitmodelValue+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/*.tsmodules, not in components. - Use
fetchoraxiosvia 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
@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@Autowiredfield injection. - DTOs: prefer Java records over classes for immutable data.
- Validation:
@Validon controller parameters,jakarta.validationannotations 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_casecolumn naming explicitly (@Column(name = "created_at")). - Database migrations: Flyway. All schema changes go through SQL migration files in
db/migration/. - Lombok:
@RequiredArgsConstructor,@Getter,@Setter,@NoArgsConstructorare 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 namedid. - Timestamps:
created_at,updated_at. UseInstantin Java. - Enums: stored as
VARCHARin 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
// 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
@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 infrontend/e2e/. Requiresdocker 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
./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
@PreAuthorizeor security filter chain - User-visible strings use Swedish
- API responses never expose internal IDs or stack traces
- Input validated on both frontend and backend