Vite runs inside the frontend container where 'localhost' resolves to the container itself, not the backend. Change the /api proxy target from http://localhost:8000 to http://backend:8000 so the request is routed to the backend service over the compose network.
21 KiB
21 KiB
Frontend Development — Rules & Guidelines
Rules and conventions every frontend developer must follow. Read this before writing your first line of code.
1. Language & Typing
- TypeScript is mandatory — no plain JavaScript files (
.js,.jsx) in the codebase. - Use strict mode (
"strict": trueintsconfig.json) — the project must compile with zero errors. - Never use
any. If a type is truly unknown, useunknownand narrow it with type guards. - Prefer interfaces for object shapes that may be extended, type aliases for unions, intersections, and utility types.
- Every function must have explicit parameter types and return types — including React components (
React.FCis discouraged; type props and returnJSX.Elementexplicitly). - Use
T | nullorT | undefinedinstead ofOptionalpatterns — be explicit about nullability. - Use
as constfor constant literals and enums where it improves type narrowness. - Run
tsc --noEmitin CI — the codebase must pass with zero type errors.
// Good
interface BanEntry {
ip: string;
jail: string;
bannedAt: string;
expiresAt: string | null;
}
function BanRow({ ban }: { ban: BanEntry }): JSX.Element {
return <tr><td>{ban.ip}</td><td>{ban.jail}</td></tr>;
}
// Bad — untyped, uses `any`
function BanRow({ ban }: any) {
return <tr><td>{ban.ip}</td></tr>;
}
2. Reusable Types
- All shared type definitions live in a dedicated
types/directory. - Group types by domain:
types/ban.ts,types/jail.ts,types/auth.ts,types/api.ts, etc. - Import types using the
import typesyntax when the import is only used for type checking — this keeps the runtime bundle clean. - Component-specific prop types may live in the same file as the component, but any type used by two or more files must move to
types/. - Never duplicate a type definition — define it once, import everywhere.
- Export API response shapes alongside their domain types so consumers always know what the server returns.
// types/ban.ts
export interface Ban {
ip: string;
jail: string;
bannedAt: string;
expiresAt: string | null;
banCount: number;
country: string | null;
}
export interface BanListResponse {
bans: Ban[];
total: number;
}
// components/BanTable.tsx
import type { Ban } from "../types/ban";
3. Type Safety in API Calls
- Every API call must have a typed request and typed response.
- Define response shapes as TypeScript interfaces in
types/and cast the response through them. - Use a central API client (e.g., a thin wrapper around
fetchoraxios) that returns typed data — individual components never callfetchdirectly. - Validate or assert the response structure at the boundary when dealing with untrusted data; for critical flows, consider a runtime validation library (e.g.,
zod). - API endpoint paths are constants defined in a single file (
api/endpoints.ts) — never hard-code URLs in components.
// api/client.ts
const BASE_URL = import.meta.env.VITE_API_URL ?? "/api";
async function get<T>(path: string): Promise<T> {
const response: Response = await fetch(`${BASE_URL}${path}`, {
credentials: "include",
});
if (!response.ok) {
throw new ApiError(response.status, await response.text());
}
return (await response.json()) as T;
}
export const api = { get, post, put, del } as const;
// api/bans.ts
import type { BanListResponse } from "../types/ban";
import { api } from "./client";
export async function fetchBans(hours: number): Promise<BanListResponse> {
return api.get<BanListResponse>(`/bans?hours=${hours}`);
}
4. Code Organization
Project Structure
frontend/
├── public/
├── src/
│ ├── api/ # API client, endpoint definitions, per-domain request files
│ ├── assets/ # Static images, fonts, icons
│ ├── components/ # Reusable UI components (buttons, modals, tables, etc.)
│ ├── hooks/ # Custom React hooks
│ ├── layouts/ # Page-level layout wrappers (sidebar, header, etc.)
│ ├── pages/ # Route-level page components (one per route)
│ ├── providers/ # React context providers (auth, theme, etc.)
│ ├── theme/ # Fluent UI custom theme, tokens, and overrides
│ ├── types/ # Shared TypeScript type definitions
│ ├── utils/ # Pure helper functions, constants, formatters
│ ├── App.tsx # Root component, FluentProvider + router setup
│ ├── main.tsx # Entry point
│ └── vite-env.d.ts # Vite type shims
├── .eslintrc.cjs
├── .prettierrc
├── tsconfig.json
├── vite.config.ts # Dev proxy: /api → http://backend:8000 (service DNS)
└── package.json
Dev proxy target:
vite.config.tsproxies all/apirequests tohttp://backend:8000. Use the compose service name (backend), notlocalhost— inside the container networklocalhostresolves to the frontend container itself and causesECONNREFUSED.
Separation of Concerns
- Pages handle routing and compose layout + components — they contain no business logic.
- Components are reusable, receive data via props, and emit changes via callbacks — they never call the API directly.
- Hooks encapsulate stateful logic, side effects, and API calls so components stay declarative.
- API layer handles all HTTP communication — components and hooks consume typed functions from
api/, never rawfetch. - Types are purely declarative — no runtime code in
types/files. - Utils are pure functions with no side effects and no React dependency.
- Theme contains exclusively Fluent UI custom token overrides and theme definitions — no component logic.
5. UI Framework — Fluent UI React (v9)
- Fluent UI React Components v9 (
@fluentui/react-components) is the only UI component library allowed — do not add alternative component libraries (Material UI, Chakra, Ant Design, etc.). - Install via npm:
npm install @fluentui/react-components @fluentui/react-icons
FluentProvider
- Wrap the entire application in
<FluentProvider>at the root — this supplies the theme and design tokens to all Fluent components. - The provider must sit above the router so every page inherits the theme.
// App.tsx
import { FluentProvider, webLightTheme } from "@fluentui/react-components";
import { BrowserRouter } from "react-router-dom";
import AppRoutes from "./AppRoutes";
function App(): JSX.Element {
return (
<FluentProvider theme={webLightTheme}>
<BrowserRouter>
<AppRoutes />
</BrowserRouter>
</FluentProvider>
);
}
export default App;
Theming & Design Tokens
- Use the built-in themes (
webLightTheme,webDarkTheme) as the base. - Customise design tokens by creating a custom theme in
theme/— never override Fluent styles with raw CSS. - Reference tokens via the
tokensobject from@fluentui/react-componentswhen writingmakeStylesrules. - If light/dark mode is needed, switch the
themeprop onFluentProvider— never duplicate style definitions for each mode.
// theme/customTheme.ts
import { createLightTheme, createDarkTheme } from "@fluentui/react-components";
import type { BrandVariants, Theme } from "@fluentui/react-components";
const brandColors: BrandVariants = {
10: "#020305",
// ... define brand colour ramp
160: "#e8ebf9",
};
export const lightTheme: Theme = createLightTheme(brandColors);
export const darkTheme: Theme = createDarkTheme(brandColors);
Styling with makeStyles (Griffel)
- All custom styling is done via
makeStylesfrom@fluentui/react-components— Fluent UI uses Griffel (CSS-in-JS with atomic classes) under the hood. - Never use inline
styleprops, global CSS, or external CSS frameworks for Fluent components. - Co-locate styles in the same file as the component they belong to, defined above the component function.
- Use
mergeClasseswhen combining multiple style sets conditionally. - Reference Fluent design tokens (
tokens.colorBrandBackground,tokens.fontSizeBase300, etc.) instead of hard-coded values — this ensures consistency and automatic theme support.
import { makeStyles, tokens, mergeClasses } from "@fluentui/react-components";
const useStyles = makeStyles({
root: {
padding: tokens.spacingVerticalM,
backgroundColor: tokens.colorNeutralBackground1,
},
highlighted: {
backgroundColor: tokens.colorPaletteRedBackground2,
},
});
function BanCard({ isHighlighted }: BanCardProps): JSX.Element {
const styles = useStyles();
return (
<div className={mergeClasses(styles.root, isHighlighted && styles.highlighted)}>
{/* ... */}
</div>
);
}
Component Usage Rules
- Always prefer Fluent UI components over plain HTML elements for interactive and presentational UI:
<Button>,<Input>,<Table>,<Dialog>,<Card>,<Badge>,<Spinner>,<Toast>,<MessageBar>, etc. - Use
<DataGrid>for data-heavy tables (ban lists, jail lists) — it provides sorting, selection, and accessibility out of the box. - Use Fluent UI
<Dialog>for modals and confirmations — never build custom modal overlays. - Use
@fluentui/react-iconsfor all icons — do not mix icon libraries. - Customise Fluent components only through their public API (props, slots,
makeStyles) — never patch internal DOM or override internal class names.
Libraries you must NOT use alongside Fluent UI
tailwindcss— usemakeStylesand design tokens.styled-components/emotion— Fluent UI uses Griffel; mixing CSS-in-JS runtimes causes conflicts.@mui/*,antd,chakra-ui— one design system only.- Global CSS files that target Fluent class names — use
makeStylesoverrides.
6. Component Rules
- One component per file. The filename matches the component name:
BanTable.tsxexportsBanTable. - Use function declarations for components — not arrow-function variables.
- Keep components small and focused — if a component exceeds ~150 lines, split it.
- Props are defined as an
interfacenamed<ComponentName>Propsin the same file (or imported fromtypes/if shared). - Destructure props in the function signature.
- Never mutate props or state directly — always use immutable update patterns.
- Avoid inline styles — use
makeStylesfrom Fluent UI for all custom styling (see section 5). - Supply a
keyprop whenever rendering lists — never use array indices as keys if the list can reorder. - Prefer Fluent UI components (
Button,Table,Input, …) over raw HTML elements for any interactive or styled element.
import { Table, TableBody, TableRow, TableCell, Button } from "@fluentui/react-components";
import type { Ban } from "../types/ban";
interface BanTableProps {
bans: Ban[];
onUnban: (ip: string) => void;
}
function BanTable({ bans, onUnban }: BanTableProps): JSX.Element {
return (
<Table>
<TableBody>
{bans.map((ban) => (
<TableRow key={ban.ip}>
<TableCell>{ban.ip}</TableCell>
<TableCell>{ban.jail}</TableCell>
<TableCell>
<Button appearance="subtle" onClick={() => onUnban(ban.ip)}>Unban</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
);
}
export default BanTable;
7. Hooks & State Management
- Prefix custom hooks with
use— e.g.,useBans,useAuth,useJails. - Each hook lives in its own file under
hooks/. - Use
useStatefor local UI state,useReducerfor complex state transitions. - Use React Context sparingly — only for truly global concerns (auth, theme). Do not use context as a replacement for prop drilling one or two levels.
- Avoid
useEffectfor derived data — compute it during render or useuseMemo. - Always include the correct dependency arrays in
useEffect,useMemo, anduseCallback. Disable the ESLint exhaustive-deps rule only with a comment explaining why. - Clean up side effects (subscriptions, timers, abort controllers) in the
useEffectcleanup function.
// hooks/useBans.ts
import { useState, useEffect } from "react";
import type { Ban } from "../types/ban";
import { fetchBans } from "../api/bans";
interface UseBansResult {
bans: Ban[];
loading: boolean;
error: string | null;
}
function useBans(hours: number): UseBansResult {
const [bans, setBans] = useState<Ban[]>([]);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const controller = new AbortController();
async function load(): Promise<void> {
setLoading(true);
try {
const data = await fetchBans(hours);
setBans(data.bans);
setError(null);
} catch (err) {
if (!controller.signal.aborted) {
setError(err instanceof Error ? err.message : "Unknown error");
}
} finally {
setLoading(false);
}
}
load();
return () => controller.abort();
}, [hours]);
return { bans, loading, error };
}
export default useBans;
8. Naming Conventions
| Element | Convention | Example |
|---|---|---|
| Components | PascalCase | BanTable, JailCard |
| Component files | PascalCase .tsx |
BanTable.tsx |
| Hooks | camelCase with use prefix |
useBans, useAuth |
| Hook files | camelCase .ts |
useBans.ts |
| Type / Interface | PascalCase | BanEntry, JailListResponse |
| Type files | camelCase .ts |
ban.ts, jail.ts |
| Utility functions | camelCase | formatDate, buildQuery |
| Constants | UPPER_SNAKE_CASE | MAX_RETRIES, API_BASE_URL |
| makeStyles hooks | useStyles (file-scoped) |
const useStyles = makeStyles({…}) |
| makeStyles keys | camelCase slot names | root, header, highlighted |
| Directories | lowercase kebab‑case or camelCase | components/, hooks/ |
| Boolean props/variables | is/has/should prefix |
isLoading, hasError |
9. Linting & Formatting
- ESLint with the following plugins is required:
@typescript-eslint/eslint-plugin— TypeScript-specific rules.eslint-plugin-react— React best practices.eslint-plugin-react-hooks— enforce rules of hooks.eslint-plugin-import— ordered and valid imports.
- Prettier handles all formatting — ESLint must not conflict with Prettier (use
eslint-config-prettier). - Format on save is expected — every developer must enable it in their editor.
- Run
eslint . --max-warnings 0andprettier --check .in CI — zero warnings, zero formatting diffs. - Line length: 100 characters max.
- Strings: use double quotes (
"). - Semicolons: always.
- Trailing commas: all (ES5+).
- Indentation: 2 spaces.
- No unused variables, no unused imports, no
@ts-ignorewithout an accompanying comment. - Import order (enforced by ESLint): React → third-party → aliases → relative, each group separated by a blank line.
// .prettierrc
{
"semi": true,
"singleQuote": false,
"trailingComma": "all",
"printWidth": 100,
"tabWidth": 2
}
10. Clean Code Principles
- Single Responsibility: Every function, hook, and component does one thing well.
- DRY (Don't Repeat Yourself): Extract repeated JSX into components, repeated logic into hooks or utils. If you copy-paste, refactor.
- KISS (Keep It Simple, Stupid): Prefer the simplest solution that works. Avoid premature abstraction.
- Meaningful Names: Variable and function names describe what, not how. Avoid abbreviations (
btn→button,idx→index) except universally understood ones (id,url,ip). - Small Functions: If a function exceeds ~30 lines, it likely does too much — split it.
- No Magic Numbers / Strings: Extract constants with descriptive names.
- Explicit over Implicit: Favor clarity over cleverness. Code is written once and read many times.
- No Dead Code: Remove unused functions, commented-out blocks, and unreachable branches before committing.
- Early Returns: Reduce nesting by returning early from guard clauses.
- Immutability: Default to
const. Use spread /map/filterinstead of mutating arrays and objects.
// Bad — magic number, unclear name
if (data.length > 50) { ... }
// Good
const MAX_VISIBLE_BANS = 50;
if (data.length > MAX_VISIBLE_BANS) { ... }
11. Error Handling
- Wrap API calls in
try-catchinside hooks — components should never see raw exceptions. - Display user-friendly error messages — never expose stack traces or raw server responses in the UI.
- Use an error boundary (
ErrorBoundarycomponent) at the page level to catch unexpected render errors. - Log errors to the console (or a future logging service) with sufficient context for debugging.
- Always handle the loading, error, and empty states for every data-driven component.
12. Performance
- Use
React.memoonly when profiling reveals unnecessary re-renders — do not wrap every component by default. - Use
useMemoanduseCallbackfor expensive computations and stable callback references passed to child components — not for trivial values. - Lazy-load route-level pages with
React.lazy+Suspenseto reduce initial bundle size. - Avoid creating new objects or arrays inside render unless necessary — stable references prevent child re-renders.
- Keep bundle size in check — review dependencies before adding them and prefer lightweight alternatives.
13. Accessibility
- Use semantic HTML elements (
<button>,<nav>,<table>,<main>,<header>) — not<div>with click handlers. - Every interactive element must be keyboard accessible (focusable, operable with Enter/Space/Escape as appropriate).
- Images and icons require
alttext oraria-label. - Form inputs must have associated
<label>elements. - Maintain sufficient color contrast ratios (WCAG AA minimum).
- Test with a screen reader periodically.
14. Testing
- Write tests for every new component, hook, and utility function.
- Use Vitest (or Jest) as the test runner and React Testing Library for component tests.
- Test behavior, not implementation — query by role, label, or text, never by CSS class or internal state.
- Mock API calls at the network layer (e.g.,
msw— Mock Service Worker) — components should not know about mocks. - Aim for >80 % line coverage — critical paths (auth flow, ban/unban actions) must be 100 %.
- Test name pattern:
it("should <expected behavior> when <condition>"). - Wrap components under test in
<FluentProvider>so Fluent UI styles and tokens resolve correctly:
import { render, screen } from "@testing-library/react";
import { FluentProvider, webLightTheme } from "@fluentui/react-components";
import BanTable from "./BanTable";
import type { Ban } from "../types/ban";
const mockBans: Ban[] = [
{ ip: "192.168.1.1", jail: "sshd", bannedAt: "2026-02-28T12:00:00Z", expiresAt: null, banCount: 3, country: "DE" },
];
function renderWithProvider(ui: JSX.Element) {
return render(<FluentProvider theme={webLightTheme}>{ui}</FluentProvider>);
}
it("should render a row for each ban", () => {
renderWithProvider(<BanTable bans={mockBans} onUnban={vi.fn()} />);
expect(screen.getByText("192.168.1.1")).toBeInTheDocument();
});
15. Git & Workflow
- Branch naming:
feature/<short-description>,fix/<short-description>,chore/<short-description>. - Commit messages: imperative tense, max 72 chars first line (
Add ban table component,Fix date formatting in dashboard). - Every merge request must pass: ESLint, Prettier, TypeScript compiler, all tests.
- Do not merge with failing CI.
- Keep pull requests small and focused — one feature or fix per PR.
- Review your own diff before requesting review.
16. Quick Reference — Do / Don't
| Do | Don't |
|---|---|
| Type every prop, state, and return value | Use any or leave types implicit |
Keep shared types in types/ |
Duplicate interfaces across files |
| Call API from hooks, not components | Scatter fetch calls across the codebase |
Use import type for type-only imports |
Import types as regular imports |
| One component per file | Export multiple components from one file |
| Destructure props in the signature | Access props.x throughout the body |
| Use Fluent UI components for all interactive UI | Build custom buttons, inputs, dialogs from scratch |
Style with makeStyles + design tokens |
Use inline styles, global CSS, or Tailwind |
Wrap the app in <FluentProvider> |
Render Fluent components outside a provider |
Use @fluentui/react-icons for icons |
Mix multiple icon libraries |
| Use semantic HTML elements | Use <div> for everything |
| Handle loading, error, and empty states | Only handle the happy path |
Name booleans with is/has/should |
Name booleans as plain nouns (loading) |
| Extract constants for magic values | Hard-code numbers and strings |
| Clean up effects (abort, unsub) | Let effects leak resources |
| Format and lint before every commit | Push code that doesn't pass CI |