bilhej/CODING_GUIDELINES.md

11 KiB

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

<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

@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

// 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 @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