// Sync API contracts between FastAPI backend and React frontend. Triggers on "sync api", "update contracts", "generate types", "оновити контракти", "згенерувати типи", or when backend schemas change.
| name | api-contracts |
| description | Sync API contracts between FastAPI backend and React frontend. Triggers on "sync api", "update contracts", "generate types", "оновити контракти", "згенерувати типи", or when backend schemas change. |
Автоматична синхронізація типів між FastAPI backend та React frontend через OpenAPI schema.
Backend (Pydantic) → openapi.json → Orval → TypeScript types + hooks
# Повна синхронізація (рекомендовано)
just api-sync
# Окремі кроки
just api-export # Експорт OpenAPI з FastAPI
just api-generate # Генерація TypeScript з OpenAPI
Викликай just api-sync коли:
Після модифікації backend/app/schemas/*.py:
just api-sync
Перевір згенеровані файли:
# TypeScript types
ls frontend/src/shared/api/model/
# React Query hooks
ls frontend/src/shared/api/generated/
// Імпорт згенерованих типів
import type { TopicPublic, AtomCreate } from '@/shared/api/model'
// Імпорт згенерованих hooks
import { useListTopicsApiV1TopicsGet } from '@/shared/api/generated/topics/topics'
// Використання hook
const { data, isLoading } = useListTopicsApiV1TopicsGet({ limit: 10 })
contracts/
└── openapi.json # OpenAPI schema (source of truth)
frontend/src/shared/api/
├── model/ # TypeScript interfaces
│ ├── topicPublic.ts
│ ├── atomCreate.ts
│ └── ...
├── generated/ # React Query hooks
│ ├── topics/topics.ts
│ ├── atoms/atoms.ts
│ └── ...
└── lib/api/
└── mutator.ts # Axios wrapper for orval
Orval config: frontend/orval.config.ts
export default defineConfig({
api: {
input: '../contracts/openapi.json',
output: {
mode: 'tags-split',
target: './src/shared/api/generated',
schemas: './src/shared/api/model',
client: 'react-query',
},
},
})
# Перегенерувати з чистого стану
cd frontend && rm -rf src/shared/api/generated src/shared/api/model
just api-sync
# Перевірити що схема оновлена
cat contracts/openapi.json | jq '.paths | keys | length'
# Порівняти з runtime
curl http://localhost/api/v1/openapi.json | jq '.paths | keys | length'
cd frontend && npx tsc --noEmit
customInstance з lib/api/mutator.tsФайл: contracts/openapi.json (~12k рядків, 99 endpoints, 132 schemas)
Для економії контексту рекомендовано витягувати тільки потрібну інформацію через Python.
openapi.json
├── openapi: "3.0.2" # версія специфікації
├── info # метадані API
│ ├── title
│ ├── version
│ └── description
├── paths # 🎯 ENDPOINTS (dict: path → methods)
│ └── /api/v1/topics
│ ├── get
│ │ ├── summary
│ │ ├── tags: []
│ │ ├── parameters: [] # query/path params
│ │ └── responses
│ │ └── 200
│ │ └── content.application/json.schema.$ref
│ └── post
│ ├── requestBody # body schema
│ │ └── content.application/json.schema.$ref
│ └── responses
├── components # 🎯 REUSABLE DEFINITIONS
│ └── schemas # TypeScript types живуть тут
│ └── TopicPublic
│ ├── type: "object"
│ ├── properties # поля
│ │ ├── id: {type: "string"}
│ │ └── name: {type: "string"}
│ └── required: []
└── tags # групування endpoints
└── [{name: "topics", description: "..."}]
spec = json.load(open('contracts/openapi.json'))
# Рівень 1: Корінь
spec.keys() # ['openapi', 'info', 'paths', 'components', 'tags']
# Рівень 2: Endpoints
spec['paths'].keys() # всі шляхи API
# Рівень 3: Methods
spec['paths']['/api/v1/topics'].keys() # ['get', 'post', ...]
# Рівень 4: Деталі endpoint
spec['paths']['/api/v1/topics']['get'].keys() # ['summary', 'tags', 'parameters', 'responses']
# Рівень 2: Schemas
spec['components']['schemas'].keys() # всі типи
# Рівень 3: Schema definition
spec['components']['schemas']['TopicPublic'].keys() # ['type', 'properties', 'required']
import json
spec = json.load(open('contracts/openapi.json'))
# Список всіх endpoints
for p, ms in spec['paths'].items():
for m in ms:
if m in ('get','post','put','patch','delete'):
print(f'{m.upper():6} {p}')
# Список schemas
for name in spec['components']['schemas']: print(name)
# Пошук endpoints по keyword
kw = 'topic'
[print(f'{m.upper():6} {p}') for p,ms in spec['paths'].items() for m in ms if kw in p and m in ('get','post','put','delete')]
# Деталі endpoint
print(json.dumps(spec['paths']['/api/v1/topics']['get'], indent=2))
# Деталі schema
print(json.dumps(spec['components']['schemas']['TopicPublic'], indent=2))
# Тільки поля schema
print(list(spec['components']['schemas']['TopicPublic'].get('properties', {}).keys()))
# Endpoints з певним tag
tag = 'topics'
[print(f"{m.upper():6} {p}") for p,ms in spec['paths'].items() for m,d in ms.items() if m in ('get','post','put','delete') and tag in d.get('tags',[])]