updating documentation
This commit is contained in:
parent
1ba57c7e9d
commit
7e0311683f
25 changed files with 2660 additions and 226 deletions
367
documentation/ai-context/03-api-contract.md
Normal file
367
documentation/ai-context/03-api-contract.md
Normal file
|
|
@ -0,0 +1,367 @@
|
|||
# 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, // 0–3
|
||||
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 // 0–3
|
||||
}
|
||||
```
|
||||
|
||||
**Response** (AnswerResultSchema):
|
||||
|
||||
```typescript
|
||||
{
|
||||
questionId: string,
|
||||
isCorrect: boolean,
|
||||
correctOptionId: number, // 0–3
|
||||
selectedOptionId: number // 0–3
|
||||
}
|
||||
```
|
||||
|
||||
**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 // 0–3
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue