# Phase 5 — HTTP API (REST) Plain REST/JSON API over HTTP using `github.com/go-chi/chi/v5`. No GraphQL on our end. All handlers live in `api/handler.go`. Server bootstrap in `cmd/server/main.go`. Reference: - Suwayomi API patterns: `/Users/achmad/Documents/Belajar/Web/Suwayomi-Server/server/src/main/kotlin/suwayomi/tachidesk/manga/api/` --- ## 5.1 Server Bootstrap — `cmd/server/main.go` - [ ] Parse config from env vars: - [ ] `DATABASE_URL` (required) - [ ] `PORT` (default `8080`) - [ ] `FLARESOLVERR_URL` (optional; disables CF-dependent sources if empty) - [ ] TTL vars: `MANGA_LIST_TTL_SECONDS`, `MANGA_DETAIL_TTL_SECONDS`, `CHAPTER_LIST_TTL_SECONDS` - [ ] Initialize `pgxpool.Pool` via `db.New()` - [ ] Run DB migrations via `db.Migrate()` - [ ] Blank-import all source packages so their `init()` registers them: ```go import ( _ "tachiyomi-go/sources/all/mangadex" _ "tachiyomi-go/sources/all/kemono" _ "tachiyomi-go/sources/en/allanime" // ... all sources ) ``` - [ ] `UpsertSource` for every registered source on startup (sync source table with compiled sources) - [ ] Mount router and `http.ListenAndServe` - [ ] Graceful shutdown on SIGINT/SIGTERM (drain pool, close server) --- ## 5.2 Router — `api/handler.go` - [ ] `NewRouter(db *pgxpool.Pool) http.Handler` using chi - [ ] Mount all routes under `/api` - [ ] Middleware: - [ ] `chi.Logger` — request logging - [ ] `chi.Recoverer` — panic recovery → 500 JSON error - [ ] `requestID` middleware - [ ] CORS headers (configurable origin, default `*`) --- ## 5.3 Source Routes ### `GET /api/sources` - [ ] Call `registry.All()` - [ ] Return `[]{id, name, lang, supportsLatest, isNsfw}` sorted by ID - [ ] No DB call required (registry is in-memory) ### `GET /api/sources/{id}/popular?page=1` - [ ] Parse `id` as int64, `page` as int (default 1), `refresh` as bool - [ ] Validate source exists in registry - [ ] Check manga list TTL (see Phase 2 cache logic) - [ ] If cache miss or `?refresh=true`: call `source.GetPopularManga(page)`, upsert results - [ ] Return `{mangas: [...], hasNextPage: bool}` ### `GET /api/sources/{id}/latest?page=1` - [ ] Same as popular but calls `source.GetLatestUpdates(page)` - [ ] Return 405 if `source.SupportsLatest()` is false ### `GET /api/sources/{id}/search?q=&page=1` - [ ] Parse `q` (query string), `page`, `refresh` - [ ] Pass `filters` from query params (see Filter parsing below) - [ ] Call `source.GetSearchManga(page, q, filters)` - [ ] Upsert results, return `{mangas: [...], hasNextPage: bool}` ### `GET /api/sources/{id}/filters` - [ ] Call `source.GetFilterList()` - [ ] Return filter descriptors as JSON: `[{type, name, values}]` - [ ] No DB involvement ### Filter Param Parsing - [ ] Parse filter values from query params: `filter_{name}={value}` - [ ] `SelectFilter`: single string value - [ ] `CheckboxFilter`: "true"/"false" → bool - [ ] `TriStateFilter`: "0"/"1"/"2" → int - [ ] `TextFilter`: raw string - [ ] `SortFilter`: `{name}:{asc|desc}` --- ## 5.4 Manga Routes (Source-aware) ### `GET /api/sources/{id}/manga?url={encodedURL}` - [ ] URL-decode `url` param - [ ] Look up manga in DB by `(source_id, url)` — `GetMangaBySourceURL` - [ ] If not found or `initialized=false` or TTL expired or `?refresh=true`: - [ ] Call `source.GetMangaDetails(SManga{URL: url})` - [ ] `UpsertManga` + `UpdateMangaDetails` + set `initialized=true` - [ ] Return full manga JSON (all fields) ### `GET /api/sources/{id}/manga?url={encodedURL}&resource=chapters` OR `GET /api/sources/{id}/chapters?url={encodedURL}` - [ ] Look up manga_id from DB - [ ] Check `chapters_last_fetched_at` TTL - [ ] If miss: call `source.GetChapterList(manga)`, `UpsertChapter` for each - [ ] Return `[{url, name, chapterNumber, dateUpload, scanlator, sourceOrder}]` ### `GET /api/sources/{id}/pages?chapterUrl={encodedURL}` - [ ] Look up chapter by URL - [ ] Check if pages exist in DB for chapter - [ ] If miss: call `source.GetPageList(chapter)`, then optionally `source.GetImageURL(page)` for each - [ ] `UpsertPage` for each; store `image_url` (except sources with expiring URLs) - [ ] Return `[{index, url, imageUrl}]` --- ## 5.5 DB-Backed Routes (no source call) ### `GET /api/manga/{id}` - [ ] `GetMangaByID` from DB - [ ] Return manga JSON or 404 ### `GET /api/manga/{id}/chapters` - [ ] `ListChaptersByManga` from DB - [ ] Return chapter array ordered by `source_order` ### `GET /api/chapters/{id}` - [ ] `GetChapterByID` from DB - [ ] Return chapter JSON or 404 ### `GET /api/chapters/{id}/pages` - [ ] `ListPagesByChapter` from DB - [ ] Return page array ordered by `index` --- ## 5.6 Image Proxy — `GET /api/image?url={encodedURL}&source_id={id}` - [ ] URL-decode `url` param - [ ] Validate `source_id` exists in registry - [ ] Fetch image using source's configured HTTP client (respects source-specific headers/cookies) - [ ] Set `Referer` if required (e.g. Pixiv) - [ ] Use FlareSolverr-cleared cookies if source needs CF bypass - [ ] Stream response bytes directly to client - [ ] Forward `Content-Type` header from upstream - [ ] Set `Cache-Control: public, max-age=86400` for non-expiring images - [ ] **Do not** set long cache for signed/expiring URLs (globalcomix, etc.) - [ ] Return 502 on upstream error --- ## 5.7 Error Handling - [ ] All errors: `{"error": "human-readable message"}` with correct HTTP status - [ ] 400: invalid params (bad ID, missing required param, bad page number) - [ ] 404: source not found, manga not found, chapter not found - [ ] 405: method not supported (e.g. `GetLatestUpdates` on source with `SupportsLatest=false`) - [ ] 500: DB error, unexpected source error - [ ] 502: upstream source returned error (connection refused, non-200, parse failure) - [ ] 503: FlareSolverr unavailable (when source requires it but `FLARESOLVERR_URL` is unset) - [ ] Structured error logging: include `source_id`, `url`, `error` in log fields --- ## 5.8 `?refresh=true` Bypass - [ ] Accepted on all source-facing routes: `/popular`, `/latest`, `/search`, `/manga`, `/chapters`, `/pages` - [ ] Bypasses TTL — always calls source regardless of `last_fetched_at` - [ ] Updates DB with fresh data after fetch - [ ] NOT accepted on DB-only routes (`/api/manga/{id}`, `/api/chapters/{id}`, etc.) --- ## Checklist: Phase 5 Done When - [ ] `GET /api/sources` returns all registered sources - [ ] `GET /api/sources/{heancmsID}/popular?page=1` returns ≥1 manga; rows in `manga` table confirmed via `psql` - [ ] Second call to same URL returns from DB (no source HTTP call — confirm via log) - [ ] `?refresh=true` forces re-fetch and updates `last_fetched_at` in DB - [ ] `GET /api/sources/{madaraID}/pages?chapterUrl=...` resolves image URLs via FlareSolverr - [ ] `GET /api/image?url=...&source_id=...` proxies image bytes with correct `Content-Type` - [ ] 404 response for unknown source ID: `{"error": "source not found"}` - [ ] 405 response when `GetLatestUpdates` called on source with `SupportsLatest=false` - [ ] `go build ./cmd/server/` succeeds - [ ] Server starts, logs migration version, logs registered source count