Skip to Content
FeaturesAPI GenerationGenerated Clients

Using Generated Clients

After generating clients with python manage.py generate_client, you’ll have type-safe TypeScript, Python, Go, and Swift clients ready to use in your applications.

TypeScript Clients

Installation

Generated TypeScript clients are organized by group:

openapi/clients/typescript/{group_name}/

Example:

  • openapi/clients/typescript/core/ - Core API group
  • openapi/clients/typescript/shop/ - Shop API group

Group Organization

Each group defined in OpenAPIConfig.groups gets its own client directory with all generated files (client, models, enums, fetchers, hooks, schemas).

Copy to Your Project

# Copy specific group cp -r openapi/clients/typescript/core src/api/ # Or copy all groups cp -r openapi/clients/typescript/* src/api/

Install as NPM Package

Create a package:

// package.json { "name": "@myorg/api-client", "version": "1.0.0", "main": "index.ts", "types": "index.ts" }

Then install locally or publish to NPM.

Basic Usage

import { APIClient } from './api/core'; // Initialize client const client = new APIClient({ baseUrl: 'https://api.example.com', token: 'access-token' }); // Make API calls (all methods are fully typed) const users = await client.users.list(); const user = await client.users.retrieve({ id: 1 }); const newUser = await client.users.create({ email: '[email protected]', name: 'John Doe' });

Authentication

Every generated TypeScript client ships a global auth store (helpers/auth.ts) that is wired into the HTTP client automatically. It is self-sufficient out of the box:

  • Auto-refresh — when the target’s API exposes the SimpleJWT refresh endpoint (/cfg/accounts/token/refresh/), the client registers a default refresh handler at import time. A 401 transparently refreshes and retries — no app wiring needed. Override with auth.setRefreshHandler(...) if your backend differs.
  • Cross-store safety — several generated clients in one app (a local client + @djangocfg/api + others) share the same storage keys. Refreshes are serialized through a navigator.locks mutex, so token rotation (ROTATE_REFRESH_TOKENS) never races between stores.
  • Reactive session — the store owns “am I logged in” (JWT exp-aware) and pushes changes on token writes, refresh results, expiry, and cross-tab / cross-store events.

Session (login / logout)

import { auth } from './generated'; // The ONE write path after any login flow (atomic pair + notification): auth.setSession({ access, refresh }); // Logout: auth.clearSession(); // exp-aware check (expired access + live refresh still counts as logged in): auth.isAuthenticated();

Storage modes

Tokens are stored in one place, selected at bootstrap:

// StorageMode = 'localStorage' | 'sessionStorage' | 'cookie' auth.setStorageMode('cookie'); // call ONCE at startup, before any setSession
ModeBehavior
localStorageDefault. Survives reload and browser restart.
sessionStorageCleared when the browser session ends.
cookieJS-readable (not HttpOnly) — for SSR cookies() access or subdomain sharing.

setStorageMode() is a one-time bootstrap relocation, not a per-login switch — the mode is module state that resets on reload, so set it before the first setSession. Public clients support only 'localStorage' | 'cookie'.

Storage mode is where the token lives, not how long the session lasts. Session length is a server decision — see Persistent Session (remember_me). There is no persist/remember parameter on the client store.

React — subscribe to the session

import { useSyncExternalStore } from 'react'; import { auth } from './generated'; const session = useSyncExternalStore( auth.subscribe, auth.getSnapshot, auth.getServerSnapshot, ); const isAuthenticated = session.status === 'authenticated'; // (@djangocfg/api exports this as the `useSession()` hook.)

Terminal 401 (session died)

// Fires ONLY when the refresh path is exhausted. The store has already // cleared the session — the handler just routes to login. Returns an // unsubscribe function; multiple handlers compose. const unsubscribe = auth.onSessionExpired(() => { window.location.replace('/auth'); });

API Key

// Set API key (sent as X-API-Key; falls back to NEXT_PUBLIC_API_KEY) auth.setApiKey('your-api-key');

Public clients (no auth) — automatic

If a target’s sliced OpenAPI spec contains zero secured operations (for example a public ingest API), the generator automatically emits a token-free minimal store instead: no Authorization header, no token storage, no refresh machinery. This also prevents a logged-in user’s Bearer token from leaking onto public endpoints. No configuration needed — the mode is derived from the spec’s security requirements.

Multiple Groups

Use clients from different groups together:

// Import clients from different groups import { APIClient as CoreClient } from './api/core'; import { APIClient as TradingClient } from './api/trading'; import { APIClient as MarketClient } from './api/market'; // Initialize with shared authentication const token = 'access-token'; const baseUrl = 'https://api.cryptoplatform.com'; const core = new CoreClient({ baseUrl, token }); const trading = new TradingClient({ baseUrl, token }); const market = new MarketClient({ baseUrl, token }); // Use each client independently const user = await core.users.retrieve({ id: 1 }); const wallets = await trading.wallets.list(); const prices = await market.prices.list({ symbols: ['BTC', 'ETH'] });

React Integration

import { useState, useEffect } from 'react'; import { APIClient } from './api/trading'; function WalletList() { const [wallets, setWallets] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { const client = new APIClient({ baseUrl: process.env.REACT_APP_API_URL, token: localStorage.getItem('access_token') }); client.wallets.list() .then(data => { setWallets(data.results); setLoading(false); }) .catch(error => { console.error('Failed to load wallets:', error); setLoading(false); }); }, []); if (loading) return <div>Loading...</div>; return ( <ul> {wallets.map(wallet => ( <li key={wallet.id}>{wallet.currency}: {wallet.balance}</li> ))} </ul> ); }

Vue Integration

<template> <div> <div v-if="loading">Loading...</div> <ul v-else> <li v-for="wallet in wallets" :key="wallet.id"> {{ wallet.currency }}: {{ wallet.balance }} </li> </ul> </div> </template> <script> import { APIClient } from './api/trading'; export default { data() { return { wallets: [], loading: true, }; }, async mounted() { const client = new APIClient({ baseUrl: process.env.VUE_APP_API_URL, token: localStorage.getItem('access_token') }); try { const data = await client.wallets.list(); this.wallets = data.results; } catch (error) { console.error('Failed to load wallets:', error); } finally { this.loading = false; } }, }; </script>

Error Handling

try { const order = await client.orders.create({ symbol: 'BTC/USDT', side: 'buy', amount: 0.1, price: 45000 }); console.log('Order created:', order.id); } catch (error) { if (error.response) { // API error response console.error('Status:', error.response.status); console.error('Data:', error.response.data); } else if (error.request) { // No response received console.error('No response from server'); } else { // Other errors console.error('Error:', error.message); } }

Python Clients

Installation

Generated Python clients are organized by group:

openapi/clients/python/{group_name}/

Example:

  • openapi/clients/python/core/ - Core API group
  • openapi/clients/python/trading/ - Trading API group

Group Organization

Each group defined in OpenAPIConfig.groups gets its own Python package with models, subclients, and async support.

Install Locally

# Install specific group pip install -e openapi/clients/python/core # Or add to requirements.txt echo "-e openapi/clients/python/core" >> requirements.txt echo "-e openapi/clients/python/trading" >> requirements.txt

Publish to PyPI

Package and publish:

cd openapi/clients/python/trading python -m build python -m twine upload dist/*

Basic Usage

from trading import Client from trading.api.default import ( list_wallets, get_wallet, create_order ) from trading.models import OrderCreate # Initialize client client = Client(base_url="https://api.cryptoplatform.com") client = client.with_headers({ "Authorization": "Bearer access-token" }) # Synchronous API calls wallets = list_wallets.sync(client=client) print(f"Found {len(wallets.results)} wallets") wallet = get_wallet.sync(id="btc-wallet-123", client=client) print(f"Wallet: {wallet.currency} - {wallet.balance}") # Create order order_data = OrderCreate( symbol="BTC/USDT", side="buy", amount=0.1, price=45000 ) new_order = create_order.sync( client=client, json_body=order_data ) print(f"Created order: {new_order.id}")

Async Usage

import asyncio from trading import Client from trading.api.default import ( list_wallets, create_order ) async def main(): client = Client(base_url="https://api.cryptoplatform.com") client = client.with_headers({ "Authorization": "Bearer access-token" }) # Async API calls wallets = await list_wallets.asyncio(client=client) print(f"Found {len(wallets.results)} wallets") new_order = await create_order.asyncio( client=client, json_body={"symbol": "ETH/USDT", "side": "buy", "amount": 1} ) print(f"Created: {new_order.id}") # Run async code asyncio.run(main())

Authentication

# Bearer token client = client.with_headers({ "Authorization": f"Bearer {access_token}" }) # API key client = client.with_headers({ "X-API-Key": "your-api-key" }) # Custom headers client = client.with_headers({ "X-Custom-Header": "value", "X-Request-ID": "unique-id" })

Multiple Groups

# Import clients from different groups from core import Client as CoreClient from trading import Client as TradingClient from market import Client as MarketClient # Initialize clients with shared authentication base_url = "https://api.cryptoplatform.com" headers = {"Authorization": "Bearer access-token"} core = CoreClient(base_url=base_url).with_headers(headers) trading = TradingClient(base_url=base_url).with_headers(headers) market = MarketClient(base_url=base_url).with_headers(headers) # Use each client from core.api.default import list_users, get_user from trading.api.default import list_wallets, list_orders from market.api.default import get_prices users = list_users.sync(client=core) wallets = list_wallets.sync(client=trading) orders = list_orders.sync(client=trading, status="open") prices = get_prices.sync(client=market, symbols=["BTC", "ETH"])

Django Integration

# views.py from django.http import JsonResponse from trading import Client from trading.api.default import list_wallets def get_user_wallets(request): """Get wallets for current user""" # Create API client client = Client(base_url="https://api.cryptoplatform.com") client = client.with_headers({ "Authorization": f"Bearer {request.user.access_token}" }) # Fetch wallets try: wallets = list_wallets.sync( client=client, user_id=request.user.id ) return JsonResponse({ 'wallets': [wallet.dict() for wallet in wallets.results] }) except Exception as e: return JsonResponse({'error': str(e)}, status=500)

FastAPI Integration

from fastapi import FastAPI, Depends from trading import Client from trading.api.default import list_wallets, list_orders app = FastAPI() def get_api_client(): """Dependency to get API client""" client = Client(base_url="https://api.cryptoplatform.com") return client.with_headers({ "Authorization": "Bearer service-token" }) @app.get("/wallets") async def get_wallets(client: Client = Depends(get_api_client)): """Get all wallets""" wallets = await list_wallets.asyncio(client=client) return {"wallets": [wallet.dict() for wallet in wallets.results]} @app.get("/orders") async def get_orders(status: str = "open", client: Client = Depends(get_api_client)): """Get orders by status""" orders = await list_orders.asyncio(client=client, status=status) return {"orders": [order.dict() for order in orders.results]}

Error Handling

from httpx import HTTPStatusError try: order = create_order.sync( client=client, json_body={"symbol": "BTC/USDT", "side": "buy", "amount": 0.1} ) except HTTPStatusError as e: print(f"HTTP {e.response.status_code}: {e.response.text}") except Exception as e: print(f"Error: {e}")

Swift Clients (iOS/macOS)

Django-CFG generates Swift clients using Apple’s official swift-openapi-generator. This produces native Swift code with async/await support.

External Generator

Swift generation requires swift-openapi-generator to be installed:

brew install swift-openapi-generator

Installation

Generated Swift clients are in:

openapi/clients/swift/{group_name}/

Add to Xcode Project

  1. Copy the generated directory to your iOS project
  2. Add files to your target in Xcode
  3. Install OpenAPIRuntime via SPM:
// Package.swift or Xcode SPM dependencies: [ .package(url: "https://github.com/apple/swift-openapi-runtime", from: "1.0.0"), .package(url: "https://github.com/apple/swift-openapi-urlsession", from: "1.0.0"), ]

Basic Usage

import OpenAPIRuntime import OpenAPIURLSession import TradingAPI // Generated client // Initialize client let client = Client( serverURL: URL(string: "https://api.cryptoplatform.com")!, transport: URLSessionTransport() ) // List wallets (async/await) Task { do { let response = try await client.walletsList() switch response { case .ok(let okResponse): let wallets = try okResponse.body.json for wallet in wallets.results { print("\(wallet.currency): \(wallet.balance)") } case .undocumented(let statusCode, _): print("Unexpected status: \(statusCode)") } } catch { print("Error: \(error)") } }

Authentication

// Add Bearer token via middleware struct AuthMiddleware: ClientMiddleware { let token: String func intercept( _ request: HTTPRequest, body: HTTPBody?, baseURL: URL, operationID: String, next: (HTTPRequest, HTTPBody?, URL) async throws -> (HTTPResponse, HTTPBody?) ) async throws -> (HTTPResponse, HTTPBody?) { var modifiedRequest = request modifiedRequest.headerFields[.authorization] = "Bearer \(token)" return try await next(modifiedRequest, body, baseURL) } } // Use with client let client = Client( serverURL: URL(string: "https://api.cryptoplatform.com")!, transport: URLSessionTransport(), middlewares: [AuthMiddleware(token: accessToken)] )

SwiftUI Integration

import SwiftUI struct WalletListView: View { @State private var wallets: [Wallet] = [] @State private var isLoading = true @State private var error: Error? private let client = Client( serverURL: URL(string: "https://api.cryptoplatform.com")!, transport: URLSessionTransport() ) var body: some View { Group { if isLoading { ProgressView() } else if let error = error { Text("Error: \(error.localizedDescription)") } else { List(wallets, id: \.id) { wallet in HStack { Text(wallet.currency) Spacer() Text(wallet.balance) } } } } .task { await loadWallets() } } private func loadWallets() async { do { let response = try await client.walletsList() if case .ok(let okResponse) = response { wallets = try okResponse.body.json.results } isLoading = false } catch { self.error = error isLoading = false } } }

Error Handling

do { let response = try await client.ordersCreate( body: .json(.init( symbol: "BTC/USDT", side: .buy, amount: 0.1, price: 45000 )) ) switch response { case .ok(let okResponse): let order = try okResponse.body.json print("Order created: \(order.id)") case .badRequest(let errorResponse): let error = try errorResponse.body.json print("Validation error: \(error.detail)") case .unauthorized: print("Not authenticated") case .undocumented(let statusCode, _): print("Unexpected status: \(statusCode)") } } catch { print("Network error: \(error)") }

Features

  • Native Swift types - Structs, enums, optionals
  • Async/await - Modern concurrency support
  • Type-safe responses - Exhaustive switch on response types
  • Middleware support - Authentication, logging, retry
  • Combine compatible - Works with publishers
  • SwiftUI ready - Use with @State, @StateObject

Type Safety

TypeScript Types

All request and response types are automatically generated:

// Types are inferred automatically const ticket = await api.getTicket({ id: '123' }); // ticket.id: string // ticket.title: string // ticket.description: string // ticket.priority: 'low' | 'medium' | 'high' | 'critical' // ticket.status: 'open' | 'in_progress' | 'resolved' | 'closed' // ticket.created_at: string // TypeScript will catch errors const newTicket = await api.createTicket({ title: 'Test', description: 'Test ticket', priority: 'invalid' // ❌ Type error: not assignable });

Python Types

Generated Python clients use attrs classes for type safety:

from cfg_support.models import Ticket, TicketCreate # Type hints work in IDEs def process_ticket(ticket: Ticket) -> None: print(ticket.id) # ✓ Valid print(ticket.title) # ✓ Valid print(ticket.invalid) # ❌ IDE error # Pydantic-style validation new_ticket = TicketCreate( title="Test", description="Test ticket", priority="high" ) # Invalid data raises validation error invalid = TicketCreate(priority="invalid") # ❌ ValidationError

Best Practices

1. Centralize Client Configuration

Create a client factory:

// api/client.ts import SupportAPI from './cfg_support'; import AccountsAPI from './cfg_accounts'; export class APIClient { private baseUrl: string; private token: string | null = null; constructor(baseUrl: string) { this.baseUrl = baseUrl; } setToken(token: string) { this.token = token; } get support() { const client = new SupportAPI(this.baseUrl); if (this.token) client.setToken(this.token); return client; } get accounts() { const client = new AccountsAPI(this.baseUrl); if (this.token) client.setToken(this.token); return client; } } // Usage const api = new APIClient(process.env.API_URL); api.setToken(localStorage.getItem('token')); const tickets = await api.support.listTickets();

2. Handle Token Refresh

class AuthenticatedAPI { private api: API; private refreshToken: string | null = null; constructor(baseUrl: string) { this.api = new API(baseUrl); } async call<T>(fn: () => Promise<T>): Promise<T> { try { return await fn(); } catch (error) { if (error.response?.status === 401 && this.refreshToken) { // Refresh token and retry await this.refresh(); return await fn(); } throw error; } } async refresh() { const newToken = await this.api.refreshToken({ refresh: this.refreshToken }); this.api.setToken(newToken.access); } }

3. Environment-Specific Configuration

// config.ts const API_URLS = { development: 'http://localhost:8000', staging: 'https://staging.api.example.com', production: 'https://api.example.com' }; export const getApiUrl = () => { const env = process.env.NODE_ENV || 'development'; return API_URLS[env]; }; // Usage const api = new API(getApiUrl());

4. Retry Logic

import backoff from httpx import HTTPStatusError @backoff.on_exception( backoff.expo, HTTPStatusError, max_tries=3, giveup=lambda e: e.response.status_code < 500 ) def fetch_tickets(client): """Fetch tickets with retry logic""" return list_tickets.sync(client=client)

Next Steps

Regenerate After API Changes

After modifying your Django REST Framework API, regenerate clients to get updated types:

python manage.py generate_client
Last updated on