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

7.3 KiB
Raw Permalink Blame History

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):

{
  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):

{
  sessionId: string,        // UUID
  questions: GameQuestion[]
}

GameQuestionSchema:

{
  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:

{
  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):

{
  sessionId: string,        // UUID
  questionId: string,       // UUID
  selectedOptionId: number  // 03
}

Response (AnswerResultSchema):

{
  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:

{
  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

{
  type: "lobby:join",
  payload: {
    code: string  // Room code (e.g. "WOLF-42")
  }
}

lobby:leave

{
  type: "lobby:leave",
  payload: {
    code: string
  }
}

lobby:start

{
  type: "lobby:start",
  payload: {
    code: string
  }
}

Only the host can send this. Triggers game start.

game:answer

{
  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

{
  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

{
  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

{
  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

{
  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:

{
  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)