---
title: Data Layer
sidebar_position: 2
keywords:
  - generated api client
  - openapi
  - zod validation
  - swr hooks
  - baseclient
description: Generated API clients from Django ViewSets, BaseClient singleton, Zod runtime validation, SWR hooks, and type re-export patterns.
---

# Data Layer

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

<Callout type="info">
For Django ViewSet patterns see [App Design Guide — ViewSets](../app-design/viewsets). For SSR data fetching see [SSR & Hydration](./ssr-hydration).
</Callout>

## 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
```

<Callout type="warning">
Never edit `generated/` manually. Regenerate with `make api`.
</Callout>

## BaseClient Singleton

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

```typescript
// 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`:

```typescript
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:

```typescript
// 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:

```typescript
// 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:

```typescript
// 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](./recipes/extensions).

## 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
