# 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. null** — `country_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 values** — `0` 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` | | ### 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`: ```python @field_validator("country_code") @classmethod def _normalize_empty_country_code(cls, v: str | None) -> str | None: if v == "": return None return v ``` Models affected: - `DashboardBanItem` — `country_code` - `ActiveBan` — `country` - `Ban` — `country` 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: ```python 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: ```typescript // 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. --- ## 8. TypedDict for Error Metadata Error response metadata uses `ErrorMetadata` (a `TypedDict` with `total=False`) instead of generic `dict[str, str | int | float | bool | None]`. This enables type-safe field access in exception handlers and type checkers can verify correct field usage. ```python # BAD — generic dict, no type narrowing def get_error_metadata(self) -> dict[str, str | int | float | bool | None]: return {"jail_name": self.name} # GOOD — TypedDict, type checker knows exact fields def get_error_metadata(self) -> ErrorMetadata: return {"jail_name": self.name} ``` When accessing error metadata in exception handlers, the type checker can now verify which keys are present: ```python metadata = exc.get_error_metadata() jail_name = metadata["jail_name"] # type checker verifies "jail_name" exists ``` `ErrorMetadata` is defined in `backend/app/models/response.py` and imported via `TYPE_CHECKING` blocks in `exceptions.py` and `main.py` to avoid circular dependencies at runtime. ## 9. Related Documents - [Architekture.md](Architekture.md) — system architecture and data flow - [Backend-Development.md](Backend-Development.md) — Python coding conventions, Pydantic usage - [Web-Development.md](../frontend/Docs/Web-Development.md) — TypeScript conventions