# API Contracts

> Sync API contracts between FastAPI backend and React frontend. Triggers on "sync api", "update contracts", "generate types", "оновити контракти", "згенерувати типи", or when backend schemas change.

- Skill: `majiayu000/api-contracts` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds add majiayu000/api-contracts`
- Raw SKILL.md: https://api.skillmd.com/api/skills/majiayu000/api-contracts/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: majiayu000 (https://skillmd.com/u/majiayu000)
- Updated: 2026-09-09
- Page: https://skillmd.com/skills/majiayu000/api-contracts

---


# API Contracts

Автоматична синхронізація типів між FastAPI backend та React frontend через OpenAPI schema.

## Overview

```
Backend (Pydantic) → openapi.json → Orval → TypeScript types + hooks
```

## Commands

```bash
# Повна синхронізація (рекомендовано)
just api-sync

# Окремі кроки
just api-export    # Експорт OpenAPI з FastAPI
just api-generate  # Генерація TypeScript з OpenAPI
```

## When to Use

Викликай `just api-sync` коли:
- Змінені Pydantic schemas в backend
- Додані/видалені API endpoints
- Змінені response/request models
- Перед commit після backend змін

## Workflow

### 1. Backend зміни

Після модифікації `backend/app/schemas/*.py`:

```bash
just api-sync
```

### 2. Перевірка

Перевір згенеровані файли:

```bash
# TypeScript types
ls frontend/src/shared/api/model/

# React Query hooks
ls frontend/src/shared/api/generated/
```

### 3. Використання в коді

```typescript
// Імпорт згенерованих типів
import type { TopicPublic, AtomCreate } from '@/shared/api/model'

// Імпорт згенерованих hooks
import { useListTopicsApiV1TopicsGet } from '@/shared/api/generated/topics/topics'

// Використання hook
const { data, isLoading } = useListTopicsApiV1TopicsGet({ limit: 10 })
```

## File Structure

```
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
```

## Configuration

**Orval config:** `frontend/orval.config.ts`

```typescript
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',
    },
  },
})
```

## Troubleshooting

### Import errors after generation

```bash
# Перегенерувати з чистого стану
cd frontend && rm -rf src/shared/api/generated src/shared/api/model
just api-sync
```

### Backend changes not reflected

```bash
# Перевірити що схема оновлена
cat contracts/openapi.json | jq '.paths | keys | length'

# Порівняти з runtime
curl http://localhost/api/v1/openapi.json | jq '.paths | keys | length'
```

### TypeScript errors

```bash
cd frontend && npx tsc --noEmit
```

## Notes

- Згенеровані файли комітяться в git
- Hooks використовують `customInstance` з `lib/api/mutator.ts`
- Query keys автоматично генеруються з endpoint paths

---

## Token-Efficient OpenAPI Access

**Файл:** `contracts/openapi.json` (~12k рядків, 99 endpoints, 132 schemas)

Для економії контексту рекомендовано витягувати тільки потрібну інформацію через Python.

### OpenAPI 3.0 Структура

```
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: "..."}]
```

### Навігація по дереву

```python
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']
```

### Приклади (адаптуй під задачу)

```python
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',[])]
```

