<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Pool Guard</title>
    <description>The latest articles on DEV Community by Pool Guard (@pool_guard_603fe0cb4809a4).</description>
    <link>https://dev.to/pool_guard_603fe0cb4809a4</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4035936%2F88487eb6-b2f4-4845-81b7-5e949bea6ff1.png</url>
      <title>DEV Community: Pool Guard</title>
      <link>https://dev.to/pool_guard_603fe0cb4809a4</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pool_guard_603fe0cb4809a4"/>
    <language>en</language>
    <item>
      <title>Building a Self-Healing GitHub Trending API in One Day</title>
      <dc:creator>Pool Guard</dc:creator>
      <pubDate>Sun, 19 Jul 2026 01:08:36 +0000</pubDate>
      <link>https://dev.to/pool_guard_603fe0cb4809a4/building-a-self-healing-github-trending-api-in-one-day-1h9i</link>
      <guid>https://dev.to/pool_guard_603fe0cb4809a4/building-a-self-healing-github-trending-api-in-one-day-1h9i</guid>
      <description>&lt;p&gt;What happens when you refuse to accept "the source is down" as an answer.&lt;/p&gt;

&lt;p&gt;The problem&lt;br&gt;
GitHub's trending page is where millions of developers find new projects every day. But GitHub has never shipped an official API for it — if you want that data programmatically, you have to scrape the HTML.&lt;/p&gt;

&lt;p&gt;Every existing scraper I found had the same design: one source, one point of failure. Small HTML change from GitHub → your service is 503 for a week. Rate limited from Cloudflare → same result.&lt;/p&gt;

&lt;p&gt;I wanted a trending API that stays up when GitHub's page is down. So I built Hydra, and put it live at hydra9.dev.&lt;/p&gt;

&lt;p&gt;The architecture (in one picture)&lt;br&gt;
             ┌───────────────┐&lt;br&gt;
Request ───▶ │    Router     │&lt;br&gt;
             └───────┬───────┘&lt;br&gt;
                     │ tries in order, breaker-guarded&lt;br&gt;
     ┌───────────────┼────────────────┬─────────────────┐&lt;br&gt;
     ▼               ▼                ▼                 ▼&lt;br&gt;
 ┌────────┐    ┌──────────┐    ┌──────────────┐   ┌──────────────┐&lt;br&gt;
 │Track A │    │ Track B  │    │  Track C     │   │  Track D     │&lt;br&gt;
 │HTML    │    │Community │    │Search API    │   │gitstar-      │&lt;br&gt;
 │scrape  │    │mirror    │    │(created:&amp;gt;7d) │   │ranking.com   │&lt;br&gt;
 └────────┘    └──────────┘    └──────────────┘   └──────────────┘&lt;br&gt;
Any track dead? Circuit breaker opens, next request routes past it.&lt;br&gt;
All tracks dead? Serve most recent Postgres snapshot + fire Prometheus alert.&lt;br&gt;
Four independent sources. A track that fails 3 times in a rolling window gets its breaker opened for 60 seconds — the router jumps to the next track without adding latency to the user's request.&lt;/p&gt;

&lt;p&gt;Why 4 sources and not 2&lt;br&gt;
Two sources feels like enough right up until they share a failure mode. The four I picked are architecturally independent:&lt;/p&gt;

&lt;p&gt;Track A (github.com/trending HTML): freshest data, but the format changes without warning&lt;br&gt;
Track B (ghapi.huchen.dev community mirror): a nice person maintains this, but it can go quiet for hours&lt;br&gt;
Track C (GitHub Search API created:&amp;gt;7d sort:stars): the most stable, but it's reconstructed trending — not what appears on the actual page&lt;br&gt;
Track D (gitstar-ranking.com): different data model entirely (lifetime stars), useful only as a last-resort cross-check&lt;br&gt;
If A breaks because of a GitHub HTML change, B doesn't (it has its own parser). If B breaks because the maintainer's server goes down, C is a completely different infrastructure at GitHub. If C rate-limits you, D is a totally different site.&lt;/p&gt;

&lt;p&gt;Historical data — the thing GitHub won't give you&lt;br&gt;
The official trending page is a snapshot in time. If you refresh, yesterday's list is gone forever.&lt;/p&gt;

&lt;p&gt;Hydra runs a Celery beat task every 15 minutes that stores a fresh snapshot to Postgres. That unlocks four analytics endpoints the official page can't offer:&lt;/p&gt;

&lt;h1&gt;
  
  
  Repos that showed up today but weren't there yesterday
&lt;/h1&gt;

&lt;p&gt;curl &lt;a href="https://hydra9.dev/api/v1/trending/new-stars" rel="noopener noreferrer"&gt;https://hydra9.dev/api/v1/trending/new-stars&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Repos that just fell off the list
&lt;/h1&gt;

&lt;p&gt;curl &lt;a href="https://hydra9.dev/api/v1/trending/dropped" rel="noopener noreferrer"&gt;https://hydra9.dev/api/v1/trending/dropped&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Ranked by stars/hour instead of cumulative fame
&lt;/h1&gt;

&lt;p&gt;curl &lt;a href="https://hydra9.dev/api/v1/trending/velocity" rel="noopener noreferrer"&gt;https://hydra9.dev/api/v1/trending/velocity&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Look at any point in time
&lt;/h1&gt;

&lt;p&gt;curl &lt;a href="https://hydra9.dev/api/v1/trending/history?at=2026-07-14T00:00:00Z" rel="noopener noreferrer"&gt;https://hydra9.dev/api/v1/trending/history?at=2026-07-14T00:00:00Z&lt;/a&gt;&lt;br&gt;
velocity in particular has been my favorite — it surfaces projects that are exploding right now, not projects that were famous 6 months ago and still coasting.&lt;/p&gt;

&lt;p&gt;The stack (deliberately boring)&lt;br&gt;
API: FastAPI (Python 3.11+)&lt;br&gt;
Task queue: Celery + Redis (broker on db 1, results on db 2)&lt;br&gt;
Storage: PostgreSQL 16 + Alembic&lt;br&gt;
HTTP client: httpx (async everywhere)&lt;br&gt;
HTML parser: selectolax (faster than BeautifulSoup)&lt;br&gt;
Observability: Prometheus + Grafana + 7 alert rules&lt;br&gt;
Deploy: Docker Compose (single file)&lt;br&gt;
The whole thing is 52 Python files and 47 tests that run in 6 seconds.&lt;/p&gt;

&lt;p&gt;One lesson: @lru_cache and Pydantic don't mix&lt;br&gt;
The single bug that took me from "green in dev" to "500 in prod" was this innocent-looking function:&lt;/p&gt;

&lt;p&gt;@lru_cache&lt;br&gt;
def &lt;em&gt;track_singletons(settings: Settings) -&amp;gt; dict[TrackId, Track]:&lt;br&gt;
    return {&lt;br&gt;
        TrackId.A: TrackA(settings),&lt;br&gt;
        TrackId.B: TrackB(settings),&lt;br&gt;
        TrackId.C: TrackC(settings),&lt;br&gt;
        TrackId.D: TrackD(settings),&lt;br&gt;
    }&lt;br&gt;
Pydantic's BaseSettings is mutable and doesn't implement __hash&lt;/em&gt;_. lru_cache needs hashable arguments. Every real request threw:&lt;/p&gt;

&lt;p&gt;TypeError: unhashable type: 'Settings'&lt;br&gt;
Fix: since get_settings() is itself an @lru_cache'd singleton, the parameter was redundant. Call it inside:&lt;/p&gt;

&lt;p&gt;@lru_cache&lt;br&gt;
def _track_singletons() -&amp;gt; dict[TrackId, Track]:&lt;br&gt;
    settings = get_settings()&lt;br&gt;
    return {...}&lt;br&gt;
Silly bug. The tests didn't catch it because they patched Settings with a mock that was hashable. Lesson: your test doubles should mirror the real thing's flaws, not paper over them.&lt;/p&gt;

&lt;p&gt;Try it&lt;br&gt;
Live: &lt;a href="https://hydra9.dev" rel="noopener noreferrer"&gt;https://hydra9.dev&lt;/a&gt;&lt;br&gt;
API docs: &lt;a href="https://hydra9.dev/docs" rel="noopener noreferrer"&gt;https://hydra9.dev/docs&lt;/a&gt;&lt;br&gt;
Source (MIT): &lt;a href="https://github.com/loly-baby/Hydra" rel="noopener noreferrer"&gt;https://github.com/loly-baby/Hydra&lt;/a&gt;&lt;br&gt;
Self-host in 4 commands:&lt;br&gt;
git clone &lt;a href="https://github.com/loly-baby/Hydra" rel="noopener noreferrer"&gt;https://github.com/loly-baby/Hydra&lt;/a&gt;&lt;br&gt;
cd Hydra &amp;amp;&amp;amp; cp .env.example .env&lt;br&gt;
docker compose -f infra/docker-compose.prod.yml up -d --build&lt;br&gt;
curl &lt;a href="http://localhost:8000/api/v1/trending" rel="noopener noreferrer"&gt;http://localhost:8000/api/v1/trending&lt;/a&gt;&lt;br&gt;
If it's useful, a ⭐ on the repo goes a long way. If you have a use case where you'd want more analytics (release velocity? topic clustering? language-specific momentum?), open an issue.&lt;/p&gt;

&lt;p&gt;Hydra runs on a single 6GB VPS shared with another project. If the public instance gets hammered, self-hosting is a Docker Compose away.&lt;/p&gt;

</description>
      <category>python</category>
      <category>fastapi</category>
      <category>opensource</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
