Files
BanGUI/Docs/TYPE_SAFETY.md
Lukas b587c6e850 docs: add TYPE_SAFETY.md documenting frontend/backend type conventions
Establishes shared type conventions to prevent runtime type mismatches
between TS frontend and Python backend. Covers snake_case JSON field
names, null vs empty string handling, timestamp formats, and validation
patterns for country codes, bans, and jail configuration.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-03 00:12:44 +02:00

4.6 KiB

Type Safety Between Frontend and Backend

This document describes how BanGUI maintains type alignment between the TypeScript frontend and Python backend, and the constraints that keep runtime type mismatches from occurring.


1. The Problem

Frontend TypeScript types and backend Pydantic models are defined independently. Drift between them causes runtime errors:

  • Empty string vs. nullcountry_code: string | null in TypeScript but backend returns "" for unresolved geo lookups. Frontend truthiness check if (ban.country_code) passes for "" but the value is meaningless.
  • Timestamp ambiguity — Frontend expects ISO 8601 strings; backend was passing mixed UNIX integers in some paths.
  • Silent zero values0 can be indistinguishable from "not set" in weakly-typed paths.

2. Shared Type Conventions

All JSON field names use snake_case in both backend (Python/Pydantic) and frontend (TypeScript). No alias generators are applied.

Python type TypeScript type Notes
str string
str | None string | null Null-capable strings use explicit null
int number
bool boolean
list[T] T[]
dict[str, T] Record<string, T>

Country Code Constraint

country_code is always string | null. An empty string "" is never a valid value. The backend normalises empty strings to None at the Pydantic validator level so the frontend always receives either a valid 2-char uppercase code or null.


3. Backend Validation Layer

Every Pydantic response model that includes a country code has a field_validator that coerces empty strings to None:

@field_validator("country_code")
@classmethod
def _normalize_empty_country_code(cls, v: str | None) -> str | None:
    if v == "":
        return None
    return v

Models affected:

  • DashboardBanItemcountry_code
  • ActiveBancountry
  • Bancountry

The same pattern should be applied to any new field that could arrive as "" from the geo enrichment layer.

Why the Validator Lives in the Model

Validation at the Pydantic model layer means:

  • It fires on every API response, regardless of which router endpoint produced it.
  • The mapper layer cannot accidentally skip normalisation.
  • Serialisation from domain → response model is automatically safe.

4. Timestamp Standard

All ban timestamps are transmitted as ISO 8601 UTC strings ("2026-04-28T07:00:00+00:00"). UNIX integers are used internally in repositories but converted to ISO strings using ts_to_iso() before entering the response model:

from datetime import UTC, datetime

def ts_to_iso(unix_ts: int) -> str:
    return datetime.fromtimestamp(unix_ts, tz=UTC).isoformat()

This conversion happens once — in the service layer when building DomainDashboardBanItem and similar domain objects — so all response models receive pre-formatted strings.


5. Frontend Type Narrowing Rules

When consuming country_code in TypeScript, use explicit null checks rather than truthiness:

// BAD — empty string passes this check
if (ban.country_code) { ... }

// GOOD — only null/undefined are falsy
if (ban.country_code !== null) { ... }

The backend normalisation ensures that !ban.country_code and ban.country_code === null are equivalent, but the explicit form is clearer and defensive against future changes.


6. CI Type Synchronisation

A type-generation script (planned) will emit a combined JSON schema from all Pydantic models and validate the generated TypeScript types against it on every build. Until that is in place:

  • Any change to a Pydantic model field type must be mirrored in the corresponding TypeScript interface in frontend/src/types/ban.ts.
  • Run pytest tests/test_models.py to verify model-level validation after changing ban.py.

7. Adding New Shared Types

When adding a new response model to backend/app/models/:

  1. Define the Pydantic model with explicit str | None (not Optional[str]) for nullable strings.
  2. Add field_validator stubs for any field that could receive an empty string from a database or external API.
  3. Add the corresponding TypeScript interface in frontend/src/types/.
  4. Add model-level unit tests in tests/test_models.py.
  5. Run the full test suite before committing.