11 KiB
11 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.
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.js, usePayment.js |
| Store | camelCase, in stores/ |
authStore.js, orderStore.js |
| API module | camelCase, in api/ |
orders.js, templates.js |
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/*.jsmodules, not in components. - Use
fetchoraxiosvia 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
@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/. - 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 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.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
@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
@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