bilhej/CODING_GUIDELINES.md

12 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.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-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 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.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

  • 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.
  • 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.

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