lila/documentation/ai-context/03-api-contract.md
2026-05-16 01:59:43 +02:00

367 lines
7.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 03 — API Contract
> **Purpose:** REST and WebSocket endpoint reference with exact Zod schemas. Concatenate with 00-project-overview.md and 99-current-task.md.
> **Last updated:** 2026-05-15
> **Depends on:** 00-project-overview.md, 02-data-model.md
---
## REST Endpoints
### Health
```
GET /api/v1/health
```
**Response:** `{ "status": "ok" }`
**Auth:** None (public)
---
### Game — Start Session
```
POST /api/v1/game/start
```
**Request body** (GameRequestSchema):
```typescript
{
source_language: SupportedLanguageCode, // "en" | "it" | "de" | "es" | "fr"
target_language: SupportedLanguageCode,
pos: SupportedPos, // "noun" | "verb" | "adjective" | "adverb"
difficulty: DifficultyLevel, // "easy" | "intermediate" | "hard"
rounds: GameRounds // "3" | "10" (string enum, converted to number in service)
}
```
**Validation rules:**
- `source_language` !== `target_language`
- Both languages in `SUPPORTED_LANGUAGE_CODES`
- `pos` in `SUPPORTED_POS`
- `difficulty` in `DIFFICULTY_LEVELS`
- `rounds` in `GAME_ROUNDS`
**Response** (GameSessionSchema):
```typescript
{
sessionId: string, // UUID
questions: GameQuestion[]
}
```
**GameQuestionSchema:**
```typescript
{
questionId: string, // UUID
prompt: string, // Word in source language
gloss: string | null, // Definition (falls back to English if target lang gloss missing)
options: AnswerOption[] // 4 items, shuffled
}
```
**AnswerOptionSchema:**
```typescript
{
optionId: number, // 03
text: string // Translation in target language
}
```
**Note:** The correct answer is NOT included in the response. The frontend only sees `optionId` and `text`. The server stores `questionId → correctOptionId` mapping in the GameSessionStore.
**Auth:** Required (session middleware)
---
### Game — Submit Answer
```
POST /api/v1/game/answer
```
**Request body** (AnswerSubmissionSchema):
```typescript
{
sessionId: string, // UUID
questionId: string, // UUID
selectedOptionId: number // 03
}
```
**Response** (AnswerResultSchema):
```typescript
{
questionId: string,
isCorrect: boolean,
correctOptionId: number, // 03
selectedOptionId: number // 03
}
```
**Error cases:**
- Session not found → 404 NotFoundError
- Question not in session → 404 NotFoundError
- Invalid optionId → 400 ValidationError
**Auth:** Required
---
### Lobby — Create
```
POST /api/v1/lobbies
```
**Request body:** None (host's auth session determines host_id)
**Response:**
```typescript
{
id: string, // UUID
code: string, // Human-readable room code (e.g. "WOLF-42")
host_id: string,
status: "waiting",
max_players: number,
settings: object | null,
created_at: string
}
```
**Auth:** Required
---
### Lobby — Join
```
POST /api/v1/lobbies/:code/join
```
**Path param:** `code` — room code (e.g. "WOLF-42")
**Response:** Same as create (the lobby object)
**Error cases:**
- Lobby not found → 404
- Lobby full → 400
- Already joined → 200 (idempotent)
**Auth:** Required
---
### Auth
```
ALL /api/auth/* — Better Auth handlers (public)
```
Better Auth mounts its own router at `/api/auth/*`. Handles:
- `/api/auth/signin/social` — initiate social login
- `/api/auth/callback/:provider` — OAuth callback
- `/api/auth/signout` — clear session
- `/api/auth/session` — get current session
**Auth:** Mixed (some public, some require valid session)
---
## WebSocket Protocol
All WS messages are JSON objects with a `type` field. The `type` is a discriminated union — the router validates the payload against the schema for that type.
### Connection
1. Client opens WebSocket to `wss://api.lilastudy.com/ws`
2. Server validates Better Auth session from cookie on upgrade
3. Connection established
### Client → Server Messages
#### `lobby:join`
```typescript
{
type: "lobby:join",
payload: {
code: string // Room code (e.g. "WOLF-42")
}
}
```
#### `lobby:leave`
```typescript
{
type: "lobby:leave",
payload: {
code: string
}
}
```
#### `lobby:start`
```typescript
{
type: "lobby:start",
payload: {
code: string
}
}
```
Only the host can send this. Triggers game start.
#### `game:answer`
```typescript
{
type: "game:answer",
payload: {
code: string,
questionId: string,
optionId: number // 03
}
}
```
Must be sent within the 15-second server timer.
---
### Server → Client Messages
#### `lobby:state`
```typescript
{
type: "lobby:state",
payload: {
code: string,
players: {
id: string,
display_name: string,
is_host: boolean
}[],
status: "waiting" | "in_progress" | "finished",
settings: object | null
}
}
```
Broadcast to all players in the lobby on any membership change.
#### `game:question`
```typescript
{
type: "game:question",
payload: {
questionId: string,
prompt: string,
gloss: string | null,
options: { optionId: number, text: string }[],
timeLimit: number // seconds (15)
}
}
```
Broadcast when the game starts or a new round begins.
#### `game:answer_result`
```typescript
{
type: "game:answer_result",
payload: {
questionId: string,
results: {
playerId: string,
displayName: string,
isCorrect: boolean,
selectedOptionId: number,
score: number
}[]
}
}
```
Broadcast after all players answer or timer expires.
#### `game:finished`
```typescript
{
type: "game:finished",
payload: {
finalScores: {
playerId: string,
displayName: string,
score: number
}[],
winner: {
playerId: string,
displayName: string
} | null // null for ties
}
}
```
Broadcast after all rounds complete.
---
## Zod Schema Locations
All schemas live in `packages/shared/src/schemas/`:
| Schema | File | Used by |
| ---------------------- | ---------- | --------------------------------------- |
| GameRequestSchema | `game.ts` | API controller, frontend GameSetup |
| GameSessionSchema | `game.ts` | API service, frontend quiz flow |
| GameQuestionSchema | `game.ts` | API service, frontend QuestionCard |
| AnswerOptionSchema | `game.ts` | API service, frontend OptionButton |
| AnswerSubmissionSchema | `game.ts` | API controller, frontend submit handler |
| AnswerResultSchema | `game.ts` | API controller, frontend ScoreScreen |
| LobbyCreateSchema | `lobby.ts` | API controller |
| LobbyJoinSchema | `lobby.ts` | API controller |
| LobbyStateSchema | `lobby.ts` | WS handler, frontend lobby UI |
| WebSocketMessageSchema | `lobby.ts` | WS router (discriminated union) |
**Rule:** Never duplicate these schemas. Import from `packages/shared` in both API and frontend.
---
## Error Responses
All errors follow this shape:
```typescript
{
error: string, // Human-readable message
statusCode: number // HTTP status
}
```
**Common status codes:**
- 400 — ValidationError (bad input, schema mismatch)
- 401 — Unauthorized (no valid session)
- 404 — NotFoundError (session, question, or lobby not found)
- 500 — Unknown error (logged, generic message to client)