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 modelNever 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:
| Preset | Use Case | Key Settings |
|---|---|---|
staticDataConfig | Brands, categories | revalidateOnFocus: false, dedupingInterval: 60000 |
detailPageConfig | Item detail | revalidateOnFocus: false, no polling |
liveListConfig | Live data lists | refreshInterval: 300000 (5 min) |
statsConfig | Dashboard stats | dedupingInterval: 30000 |
Default config: revalidateOnFocus: true, keepPreviousData: true, errorRetryCount: 3, no retry on 4xx.
CFG API vs Custom API
Two tiers of API clients:
| Tier | Package | Scope |
|---|---|---|
| CFG API | @djangocfg/api | Universal: accounts, auth, JWT, 2FA |
| Custom API | generated/ per app | App-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 setscreateBaseNextConfig({ dpop: true }), the client holds a non-extractable Web Crypto key (IndexedDB) and signs a per-requestDPoPproof. A stolen token can’t be replayed.dpopEnabledis exported from@djangocfg/api. The token is still readable viaauth.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/setLogLevelfrom@djangocfg/api).AuthProviderapplies it automatically: admins (and dev) get debug logs, regular users in production get a clean console (errors only). Errors still reach@djangocfg/monitorregardless.
Rules
- Never edit
generated/directories — regenerate withmake api - Import types from the context’s
types.ts, never fromgenerated/directly - Use the dual export pattern (
export const+export type) for enums intypes.ts - Create one
BaseClientsingleton per domain — useLocalStorageAdapteron client,MemoryStorageAdapteron server - Use SWR config presets from
SWRProvider.tsx— do not setrefreshIntervalordedupingIntervalinline - Pass
nullas SWR key to conditionally skip fetching — never call hooks inside conditionals