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 groupopenapi/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 withauth.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 anavigator.locksmutex, 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| Mode | Behavior |
|---|---|
localStorage | Default. Survives reload and browser restart. |
sessionStorage | Cleared when the browser session ends. |
cookie | JS-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 groupopenapi/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.txtPublish 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-generatorInstallation
Generated Swift clients are in:
openapi/clients/swift/{group_name}/Add to Xcode Project
- Copy the generated directory to your iOS project
- Add files to your target in Xcode
- 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") # ❌ ValidationErrorBest 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
- Overview - Learn about Django-CFG API Client Generation
- CLI Usage - Generate updated clients
- Group Configuration - Configure API groups
Regenerate After API Changes
After modifying your Django REST Framework API, regenerate clients to get updated types:
python manage.py generate_client