Skip to Content
FrontendData Layer

Data Layer

The data layer is fully generated from Django ViewSets via OpenAPI. Every API response is validated at runtime with Zod.

For Django ViewSet patterns see App Design Guide — ViewSets. For SSR data fetching see SSR & Hydration.

Generation Pipeline

Django ViewSets → drf-spectacular → OpenAPI schema → python manage.py generate_api → app/_lib/api/generated/<domain>/

Each generated domain produces:

generated/<domain>/ ├── client.ts # Typed API class with all endpoints ├── enums.ts # Python enums → TS enums ├── schema.json # OpenAPI source ├── _utils/ │ ├── fetchers/ # Plain async functions (SSR-safe) │ ├── hooks/ # SWR hooks (client-only) │ └── schemas/ # Zod schemas per model

Never edit generated/ manually. Regenerate with make api.

BaseClient Singleton

Each app creates one singleton API instance per domain in _lib/api/BaseClient.ts:

// Client-side: uses localStorage for auth tokens import { LocalStorageAdapter } from '@djangocfg/api'; class CatalogStorage extends LocalStorageAdapter { keyPrefix = 'catalog_public'; } export const catalogPublicClient = new CatalogPublicAPI( settings.api.baseUrl, { storage: new CatalogStorage() } );

For SSR, _lib/api/server.ts uses MemoryStorageAdapter:

import { MemoryStorageAdapter } from '@djangocfg/api'; export function createCatalogPublicClient(): CatalogPublicAPI { return new CatalogPublicAPI(settings.api.baseUrl, { storage: new CatalogPublicMemoryStorage() }); }

Zod Validation

Generated fetchers validate every response at runtime:

// In generated fetcher const response = await api.catalog_public_vehicles.catalogPublicVehiclesList(params); return PaginatedVehicleListListSchema.parse(response);

On validation failure, a CustomEvent('zod-validation-error', ...) is dispatched to the browser window for dev tooling.

SWR Hooks

Generated hooks wrap fetchers with SWR:

// Auto-generated in _utils/hooks/ export function useCatalogPublicVehiclesList( params?: VehiclesListParams, client?: CatalogPublicAPI ) { return useSWR( params ? ['catalog-vehicles', params] : null, () => getCatalogPublicVehiclesList(params!, client) ); }

Pass null as the key to conditionally skip fetching. Mutation hooks return an async function and auto-mutate related SWR keys.

Type Re-Export Pattern (types.ts)

Components import types from the context’s types.ts, never from generated/ directly:

// contexts/<domain>/types.ts — SINGLE SOURCE OF TRUTH import * as CatalogEnums from '@api/generated/catalog_public/enums'; // Dual export: runtime value + compile-time type export const VehicleSource = CatalogEnums.VehicleListSource; export type VehicleSource = CatalogEnums.VehicleListSource; // Re-export schema types export type { VehicleList } from '@api/generated/catalog_public/_utils/schemas/VehicleList.schema'; export type { VehicleDetail } from '@api/generated/catalog_public/_utils/schemas/VehicleDetail.schema'; // App-specific types export interface FilterState { brand: string | null; source: string | null; minPrice: number | null; maxPrice: number | null; page: number; pageSize: number; }

The dual export pattern (export const + export type for the same name) allows both runtime usage and type-level usage from a single import.

SWR Configuration Presets

Global SWR config in _lib/providers/SWRProvider.tsx with named presets:

PresetUse CaseKey Settings
staticDataConfigBrands, categoriesrevalidateOnFocus: false, dedupingInterval: 60000
detailPageConfigItem detailrevalidateOnFocus: false, no polling
liveListConfigLive data listsrefreshInterval: 300000 (5 min)
statsConfigDashboard statsdedupingInterval: 30000

Default config: revalidateOnFocus: true, keepPreviousData: true, errorRetryCount: 3, no retry on 4xx.

CFG API vs Custom API

Two tiers of API clients:

TierPackageScope
CFG API@djangocfg/apiUniversal: accounts, auth, JWT, 2FA
Custom APIgenerated/ per appApp-specific: catalog, projects, terminals

Extensions use createExtensionAPI() from @djangocfg/ext-base to share auth storage with the core API client. See Extensions recipe.

Auth token, DPoP & logging

The generated client’s auth interceptor (in @djangocfg/api helpers/auth.ts) handles tokens, headers, and refresh. Two behaviors worth knowing:

  • DPoP (sender-constrained tokens). When the backend runs JWTConfig(dpop_enabled=True) and the app sets createBaseNextConfig({ dpop: true }), the client holds a non-extractable Web Crypto key (IndexedDB) and signs a per-request DPoP proof. A stolen token can’t be replayed. dpopEnabled is exported from @djangocfg/api. The token is still readable via auth.getToken() — DPoP protects by binding, not hiding, so no proxy is needed. Details: @djangocfg/nextjs@docs/DPOP.md.
  • Role-aware console logging. All @djangocfg/* loggers share a runtime level (applyRoleLogPolicy / setLogLevel from @djangocfg/api). AuthProvider applies it automatically: admins (and dev) get debug logs, regular users in production get a clean console (errors only). Errors still reach @djangocfg/monitor regardless.

Rules

  1. Never edit generated/ directories — regenerate with make api
  2. Import types from the context’s types.ts, never from generated/ directly
  3. Use the dual export pattern (export const + export type) for enums in types.ts
  4. Create one BaseClient singleton per domain — use LocalStorageAdapter on client, MemoryStorageAdapter on server
  5. Use SWR config presets from SWRProvider.tsx — do not set refreshInterval or dedupingInterval inline
  6. Pass null as SWR key to conditionally skip fetching — never call hooks inside conditionals
Last updated on