---
estimated_steps: 35
estimated_files: 6
skills_used: []
---

# T03: Normalize /topics and /videos endpoints to paginated response shape

## Description

Two API endpoints return bare lists instead of the standard `{items, total, offset, limit}` paginated shape. Normalize both backend endpoints and update the frontend consumers.

## Steps

1. **Backend — `/topics`:** In `backend/routers/topics.py`, modify `list_topics()` to return `{"items": topics, "total": len(topics)}` instead of the bare list. Add a `TopicListResponse` schema to `backend/schemas.py` or return a dict. Update the `response_model` accordingly.
2. **Backend — `/videos`:** In `backend/routers/videos.py`, modify `list_videos()` to return `{"items": videos, "total": total, "offset": offset, "limit": limit}` — it already accepts offset/limit params but returns a bare list. Add a `SELECT count(*)` query for the total. Add a `VideoListResponse` schema or use dict.
3. **Frontend — fetchTopics:** In `frontend/src/api/topics.ts` (created by T01), update `fetchTopics()` return type from `Promise<TopicCategory[]>` to `Promise<{items: TopicCategory[], total: number}>` or a typed response interface.
4. **Frontend — TopicsBrowse.tsx:** Update to destructure `.items` from the fetchTopics response.
5. **Frontend — Home.tsx:** Update its fetchTopics consumption to read `.items`.
6. **No frontend consumer for `/videos`** — the admin pipeline page uses a different endpoint (`/admin/pipeline/videos`). Confirm with `rg` that no frontend code calls the public `/videos` endpoint directly.
7. Run backend tests and frontend build to verify.

## Must-Haves

- [ ] `GET /api/v1/topics` returns `{items: [...], total: N}`
- [ ] `GET /api/v1/videos` returns `{items: [...], total: N, offset: M, limit: L}`
- [ ] `TopicsBrowse.tsx` reads `.items` from topics response
- [ ] `Home.tsx` reads `.items` from topics response
- [ ] `npm run build` passes
- [ ] Backend tests pass

## Verification

- `cd frontend && npm run build` exits 0
- `cd backend && python -m pytest tests/ -x -q` passes (run in Docker if needed)
- `rg 'response_model=list' backend/routers/topics.py backend/routers/videos.py` returns no matches (bare lists removed)

## Inputs

- `backend/routers/topics.py` — bare list return
- `backend/routers/videos.py` — bare list return
- `backend/schemas.py` — existing schemas
- `frontend/src/api/topics.ts` — fetchTopics function (from T01)
- `frontend/src/pages/TopicsBrowse.tsx` — topics consumer
- `frontend/src/pages/Home.tsx` — topics consumer

## Expected Output

- `backend/routers/topics.py` — paginated response
- `backend/routers/videos.py` — paginated response
- `backend/schemas.py` — new response schemas
- `frontend/src/api/topics.ts` — updated return type
- `frontend/src/pages/TopicsBrowse.tsx` — reads .items
- `frontend/src/pages/Home.tsx` — reads .items

## Inputs

- `backend/routers/topics.py`
- `backend/routers/videos.py`
- `backend/schemas.py`
- `frontend/src/api/topics.ts`
- `frontend/src/pages/TopicsBrowse.tsx`
- `frontend/src/pages/Home.tsx`

## Expected Output

- `backend/routers/topics.py`
- `backend/routers/videos.py`
- `backend/schemas.py`
- `frontend/src/api/topics.ts`
- `frontend/src/pages/TopicsBrowse.tsx`
- `frontend/src/pages/Home.tsx`

## Verification

cd frontend && npm run build && cd ../backend && python -m pytest tests/ -x -q 2>/dev/null; rg 'response_model=list' backend/routers/topics.py backend/routers/videos.py
