Files
backend/AGENTS.md

12 KiB

AGENTS.md — LfK Backend

Guidance for agentic coding agents working in this repository.


Project Overview

Express + routing-controllers REST API written in TypeScript. Uses TypeORM for database access (SQLite in dev/test, PostgreSQL or MySQL in production). OpenAPI docs are auto-generated from decorators at startup.

Runtime & Package Manager: Bun (replaces Node.js + npm/pnpm).


Build / Run / Test Commands

Development

bun run dev           # Start dev server with auto-reload (uses Bun's --watch)

Auto-reload: The dev script uses Bun's built-in --watch flag, which automatically restarts the server when TypeScript files in src/ change. Bun runs TypeScript directly - no build step needed.

Performance: Bun delivers 8-15% better latency under concurrent load compared to Node.js. See BUN_BENCHMARK_RESULTS.md for details.

Build

bun run build         # rimraf dist && tsc && copy static assets → dist/

Note: The build script exists for legacy compatibility and type-checking, but is not required for development or production. Bun runs TypeScript source files directly.

Production

bun start             # bun src/app.ts (runs TypeScript directly)

Tests

Tests are integration tests that hit a live running server via HTTP. The server must be started before Jest is invoked.

# Full CI test flow (generates .env, starts server, runs jest):
bun run test:ci

# Run Jest directly (server must already be running):
bun test

# Watch mode:
bun run test:watch

# Run a single test file:
bunx jest src/tests/runners/runner_add.spec.ts

# Run tests matching a name pattern:
bunx jest --testNamePattern="POST /api/runners"

# Run all tests in a subdirectory:
bunx jest src/tests/runners/

Run all tests in a subdirectory:

bunx jest src/tests/runners/


> **Important:** `bun test` alone will fail unless the dev server is already running on `http://localhost:<config.internal_port>`. In CI, `start-server-and-test` handles this automatically via `bun run test:ci`.

### Other Utilities

```sh
bun run seed                 # Sync DB schema and run seeders
bun run openapi:export       # Export OpenAPI spec to file
bun run docs                 # Generate TypeDoc documentation
bun run licenses:export      # Export third-party license report

TypeScript Configuration

  • Target: ES2020, Module: CommonJS
  • strict: false — TypeScript strictness is disabled; types are used but not exhaustively enforced
  • experimentalDecorators: true and emitDecoratorMetadata: true — required by routing-controllers, TypeORM, and class-validator
  • Spec files (**/*.spec.ts) are excluded from compilation
  • Source root: src/, output: dist/

Code Style Guidelines

No Linter / Formatter Configured

There is no ESLint or Prettier configuration. Follow the patterns already established in the codebase rather than introducing new tooling.

Imports

  • Use named imports for decorator packages: import { Get, JsonController, Param } from 'routing-controllers'
  • Use named imports for TypeORM: import { Column, Entity, getConnectionManager } from 'typeorm'
  • Use named imports for class-validator: import { IsInt, IsOptional, IsString } from 'class-validator'
  • Use import * as X from 'module' for modules without clean default exports (e.g., import * as jwt from 'jsonwebtoken')
  • Use default imports for simple modules (e.g., import cookie from 'cookie')
  • reflect-metadata is imported once at the top of src/app.ts — do not re-import it
  • No barrel/index re-export files; import source files directly by path

Naming Conventions

Construct Convention Example
Classes PascalCase RunnerController, CreateRunner
Files PascalCase.ts matching class name RunnerController.ts
Local variables camelCase (some snake_case in tests) accessToken, access_token
DB entity fields snake_case preferred created_at, updated_at
Controller methods REST-conventional getAll, getOne, post, put, remove
Custom errors {Entity}{Issue}Error RunnerNotFoundError, RunnerIdsNotMatchingError
Response DTOs Response{Entity} ResponseRunner, ResponseAuth
Create DTOs Create{Entity} CreateRunner
Update DTOs Update{Entity} UpdateRunner
Enums PascalCase ResponseObjectType, PermissionAction

Formatting

  • 4-space indentation (observed throughout the codebase)
  • Single quotes for string literals in most files
  • No trailing semicolons style inconsistency — follow what's already in the file you're editing

Types

  • Add TypeScript types to all function parameters and return values
  • Use class-validator decorators (@IsString, @IsInt, @IsOptional, @IsUUID, etc.) on every DTO and response class field — these drive both runtime validation and OpenAPI schema generation
  • Use abstract classes for shared entity base types (e.g., abstract class Participant)
  • Use interfaces for response contracts (e.g., interface IResponse)
  • Use enums for typed string/number constants
  • Avoid any where possible; when unavoidable, keep it localised
  • strict is off — but still annotate types explicitly rather than relying on inference

Controller Pattern

import { Authorized, Body, Delete, Get, JsonController, Param, Post, Put } from 'routing-controllers';
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';

@JsonController('/runners')
@Authorized()
export class RunnerController {
    @Get('/')
    @OpenAPI({ description: 'Returns all runners' })
    @ResponseSchema(ResponseRunner, { isArray: true })
    async getAll() { ... }

    @Get('/:id')
    @ResponseSchema(ResponseRunner)
    async getOne(@Param('id') id: number) { ... }

    @Post('/')
    @ResponseSchema(ResponseRunner)
    async post(@Body({ validate: true }) createRunner: CreateRunner) { ... }

    @Put('/:id')
    @ResponseSchema(ResponseRunner)
    async put(@Param('id') id: number, @Body({ validate: true }) updateRunner: UpdateRunner) { ... }

    @Delete('/:id')
    @ResponseSchema(ResponseRunner)
    async remove(@Param('id') id: number) { ... }
}

Error Handling

  • Define custom error classes in src/errors/ extending routing-controllers error types (NotFoundError, NotAcceptableError, etc.)
  • Every custom error class must include @IsString() decorated name and message fields for OpenAPI schema generation
  • Throw custom errors directly in controllers: throw new RunnerNotFoundError()
  • Use try/catch in controllers and re-throw meaningful errors; do not swallow errors silently
  • The global ErrorHandler middleware (registered in src/middlewares/) catches all unhandled errors and serialises them as JSON — do not duplicate this logic in controllers
  • Auth errors are thrown from src/middlewares/authchecker.ts, not from individual controllers

Entity Pattern (TypeORM)

  • Entities live in src/models/entities/
  • Decorate every entity with @Entity() and every column with the appropriate @Column, @PrimaryGeneratedColumn, etc.
  • Use @CreateDateColumn() / @UpdateDateColumn() for timestamp fields
  • Use table inheritance (@TableInheritance + @ChildEntity) for polymorphic entities
  • Access repositories via getConnectionManager().get().getRepository(EntityClass) — do not inject repositories as constructor dependencies
  • Database schema is synchronised automatically on startup (connection.synchronize()) — no manual migration files

DTO Pattern (Create / Update)

  • Create DTOs in src/models/actions/create/ and src/models/actions/update/
  • Use class-validator decorators for every field
  • @IsOptional() for fields that are not required on update; all fields on create DTOs should be mandatory unless explicitly optional in the API contract
  • Response DTOs live in src/models/responses/ and follow the Response{Entity} naming pattern

Test Style Guidelines

IMPORTANT: Do not run existing tests and do not create new tests. The existing test suite in src/tests/ is outdated and no longer reflects the current state of the codebase. Ignore all test files when working in this repository. Do not write new tests for any changes or additions.

All tests are integration tests in src/tests/ organised by domain entity:

src/tests/
  auth/
    auth_login.spec.ts
    auth_refresh.spec.ts
  runners/
    runner_add.spec.ts
    runner_get.spec.ts
    runner_update.spec.ts
    runner_delete.spec.ts
  ...

Test File Template

import axios from 'axios';
import { config } from '../../config';
const base = "http://localhost:" + config.internal_port;

let access_token: string;
let axios_config: object;

beforeAll(async () => {
    jest.setTimeout(20000);
    const res = await axios.post(base + '/api/auth/login', { username: "demo", password: "demo" });
    access_token = res.data["access_token"];
    axios_config = {
        headers: { "authorization": "Bearer " + access_token },
        validateStatus: undefined   // prevents axios from throwing on non-2xx responses
    };
});

describe('POST /api/runners working', () => {
    it('creating a runner with required params should return 200', async () => {
        const res = await axios.post(base + '/api/runners', { ... }, axios_config);
        expect(res.status).toEqual(200);
        expect(res.headers['content-type']).toContain("application/json");
    });
});

describe('POST /api/runners failing', () => {
    it('creating a runner without required params should return 400', async () => {
        const res = await axios.post(base + '/api/runners', {}, axios_config);
        expect(res.status).toEqual(400);
    });
});
  • Always set validateStatus: undefined in axios_config to prevent axios throwing on error responses
  • Group tests by HTTP verb + route in describe() blocks; separate "working" and "failing" cases
  • Use jest.setTimeout(20000) in beforeAll for slow integration tests
  • Assert both res.status and res.headers['content-type'] on success paths

Environment Configuration

  • Copy .env.example to .env and fill in values before running locally
  • Database type is set via DB_TYPE env var (sqlite, postgres, or mysql)
  • Server port is set via INTERNAL_PORT (accessed as config.internal_port in code)
  • All config values are validated at startup in src/config.ts
  • CI env is generated by bun run test:ci:generate_env (scripts/create_testenv.ts)

NATS Configuration

The backend uses NATS JetStream as a KV cache for scan intake performance optimization.

  • NATS_URL — connection URL for NATS server (default: nats://localhost:4222)
  • NATS_PREWARM — if true, preloads all runner state into the KV cache at startup to eliminate DB reads from the first scan onward (default: false)

KV buckets (auto-created by NatsClient at startup):

  • station_state — station token cache (1-hour TTL)
  • card_state — card→runner mapping cache (1-hour TTL)
  • runner_state — runner display name, total distance, latest scan timestamp (no TTL, CAS-based updates)

Development: NATS runs in Docker via docker-compose.yml (port 4222). The JetStream volume is persisted to ./nats-data/ to survive container restarts.

Station intake hot path: POST /api/scans/trackscans from scan stations uses a KV-first flow that eliminates DB reads on cache hits and prevents race conditions via compare-and-swap (CAS) updates. See SCAN_NATS_PLAN.md for full architecture details.