bilhej/CODING_GUIDELINES.md

316 lines
11 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.
---
## 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.js`, `usePayment.js` |
| Store | camelCase, in `stores/` | `authStore.js`, `orderStore.js` |
| API module | camelCase, in `api/` | `orders.js`, `templates.js` |
### 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/*.js` modules, not in components.
- Use `fetch` or `axios` via a single configured instance (base URL, auth header interceptor).
---
## 4. Backend — Spring Boot 3
### 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/`.
- No Lombok beyond `@RequiredArgsConstructor`. Prefer explicit getters/setters or records.
### 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.js — 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
- Backend: JUnit 5 + Mockito. Service layer tests as unit tests. Controller tests with `@WebMvcTest`.
- Frontend: Vitest for composables and utility functions. Cypress or Playwright for E2E (Phase 1).
- Test naming: `shouldXxxWhenYyy` — e.g., `shouldReturn404WhenPlateNotFound`.
- Aim for test coverage on business logic, not on getters/setters/boilerplate.
---
## 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