Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 31 additions & 18 deletions backend/main.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
"""API Directory Backend Serves the catalog of public APIs with search and filters."""
"""API Directory Backend - Serves the catalog of public APIs with search and filters."""

import json
import httpx
import random
from datetime import datetime
from pathlib import Path

from fastapi import FastAPI, Query
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from pydantic import BaseModel
from typing import Optional
from datetime import datetime
import asyncio
import random

app = FastAPI(
title="Public API Directory",
Expand All @@ -28,6 +25,7 @@
# Load catalog
CATALOG_PATH = Path(__file__).parent.parent / "catalog" / "apis.json"


def load_catalog() -> list[dict]:
with open(CATALOG_PATH) as f:
return json.load(f)["apis"]
Expand All @@ -41,7 +39,9 @@ async def get_apis(
search: str = Query("", description="Search in name, description, tags"),
cors: str = Query("", description="Filter: true/false for CORS support"),
min_popularity: int = Query(0, ge=0, le=5, description="Minimum popularity (1-5)"),
sort: str = Query("name", regex="^(name|popularity|category|price)$", description="Sort field"),
sort: str = Query(
"name", pattern="^(name|popularity|category|price)$", description="Sort field"
),
page: int = Query(1, ge=1),
per_page: int = Query(50, ge=1, le=200),
):
Expand Down Expand Up @@ -69,7 +69,8 @@ async def get_apis(
if search:
s = search.lower()
apis = [
a for a in apis
a
for a in apis
if s in a["name"].lower()
or s in a["description"].lower()
or s in a["category"].lower()
Expand Down Expand Up @@ -156,7 +157,11 @@ async def get_stats():
auth_types[auth_simple] = auth_types.get(auth_simple, 0) + 1

# Price
if "free" in a["price"].lower() and "paid" not in a["price"].lower() and "+" not in a["price"]:
if (
"free" in a["price"].lower()
and "paid" not in a["price"].lower()
and "+" not in a["price"]
):
price_tiers["free"] += 1
elif "free" in a["price"].lower():
price_tiers["freemium"] += 1
Expand All @@ -179,7 +184,8 @@ async def get_stats():
}


@app.get("/api/ap/{api_id}")
@app.get("/api/ap/{api_id}", include_in_schema=False)
@app.get("/api/apis/{api_id}")
async def get_api_detail(api_id: str):
"""Get details for a specific API."""
apis = load_catalog()
Expand Down Expand Up @@ -213,15 +219,19 @@ async def compare_apis(ids: str = Query(..., description="Comma-separated API ID
found = [a for a in apis if a["id"] in id_list]

if not found:
return JSONResponse(status_code=404, content={"error": "No matching APIs found"})
return JSONResponse(
status_code=404, content={"error": "No matching APIs found"}
)

return {
"apis": found,
"comparison": {
"free_count": sum(1 for a in found if "free" in a["price"].lower()),
"no_auth_count": sum(1 for a in found if a["auth"] == "None"),
"cors_count": sum(1 for a in found if a["cors"]),
"avg_popularity": round(sum(a["popularity"] for a in found) / len(found), 1),
"avg_popularity": round(
sum(a["popularity"] for a in found) / len(found), 1
),
},
}

Expand All @@ -238,11 +248,13 @@ async def get_suggestions(q: str = Query("", description="Partial query")):

for a in apis:
if q_lower in a["name"].lower() or any(q_lower in t.lower() for t in a["tags"]):
suggestions.append({
"id": a["id"],
"name": a["name"],
"category": a["category"],
})
suggestions.append(
{
"id": a["id"],
"name": a["name"],
"category": a["category"],
}
)

if len(suggestions) >= 10:
break
Expand All @@ -262,4 +274,5 @@ async def health():

if __name__ == "__main__":
import uvicorn

uvicorn.run(app, host="0.0.0.0", port=8000)
26 changes: 26 additions & 0 deletions backend/test_main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import unittest

from fastapi.testclient import TestClient

from backend.main import app


class ApiDetailTest(unittest.TestCase):
def setUp(self):
self.client = TestClient(app)

def test_canonical_detail_route_returns_xquik(self):
response = self.client.get("/api/apis/xquik-api")

self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()["api"]["id"], "xquik-api")

def test_legacy_detail_route_remains_compatible(self):
response = self.client.get("/api/ap/xquik-api")

self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()["api"]["id"], "xquik-api")


if __name__ == "__main__":
unittest.main()
16 changes: 16 additions & 0 deletions catalog/apis.json
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,22 @@
"tags": ["twitter", "tweets", "social", "trending", "analytics"],
"popularity": 5
},
{
"id": "xquik-api",
"name": "Xquik",
"description": "REST API for X data extraction, account monitoring, webhooks, and automation workflows. Includes MCP support and SDKs for multiple languages.",
"category": "Social Media",
"base_url": "https://xquik.com/api/v1",
"docs_url": "https://docs.xquik.com/api-reference/overview",
"auth": "API Key",
"price": "Paid",
"rate_limit": "Plan based",
"endpoint_example": "/x/tweets/search",
"response_format": "JSON",
"cors": false,
"tags": ["x", "twitter", "social-media", "webhooks", "automation", "mcp"],
"popularity": 4
},
{
"id": "telegram-bot-api",
"name": "Telegram Bot API",
Expand Down
27 changes: 17 additions & 10 deletions frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API Hub Discover Public APIs</title>
<title>API Hub - Discover Public APIs</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<style>
Expand Down Expand Up @@ -337,9 +337,9 @@
<input type="text" id="searchInput" placeholder="Search APIs, tags, categories...">
</div>
<div class="header-stats" id="headerStats">
<span><strong id="statTotal"></strong> APIs</span>
<span><strong id="statCats"></strong> categories</span>
<span><strong id="statFree"></strong> free</span>
<span><strong id="statTotal">-</strong> APIs</span>
<span><strong id="statCats">-</strong> categories</span>
<span><strong id="statFree">-</strong> free</span>
</div>
</div>
</header>
Expand All @@ -350,10 +350,10 @@ <h1>Discover <span>Public APIs</span></h1>
</section>

<div class="stats-bar" id="statsBar">
<div class="stat-card"><div class="num" id="sTotal"></div><div class="label">Total APIs</div></div>
<div class="stat-card"><div class="num" id="sFree"></div><div class="label">Free</div></div>
<div class="stat-card"><div class="num" id="sCors"></div><div class="label">CORS Enabled</div></div>
<div class="stat-card"><div class="num" id="sNoAuth"></div><div class="label">No Auth Needed</div></div>
<div class="stat-card"><div class="num" id="sTotal">-</div><div class="label">Total APIs</div></div>
<div class="stat-card"><div class="num" id="sFree">-</div><div class="label">Free</div></div>
<div class="stat-card"><div class="num" id="sCors">-</div><div class="label">CORS Enabled</div></div>
<div class="stat-card"><div class="num" id="sNoAuth">-</div><div class="label">No Auth Needed</div></div>
</div>

<main class="main">
Expand Down Expand Up @@ -524,7 +524,11 @@ <h2 id="sectionTitle">All APIs</h2>
if (currentCategory) apis = apis.filter(a => a.category === currentCategory);
if (currentSearch) {
const s = currentSearch.toLowerCase();
apis = apis.filter(a => s in a.name.toLowerCase() || s in a.description.toLowerCase() || a.tags.some(t => s in t.toLowerCase()));
apis = apis.filter(a =>
a.name.toLowerCase().includes(s)
|| a.description.toLowerCase().includes(s)
|| a.tags.some(t => t.toLowerCase().includes(s))
);
}
if (currentFilter === "free") apis = apis.filter(a => a.price.toLowerCase() === "free");
if (currentFilter === "noauth") apis = apis.filter(a => a.auth === "None");
Expand Down Expand Up @@ -620,7 +624,10 @@ <h2 id="sectionTitle">All APIs</h2>
document.getElementById("modalTitle").textContent = api.name;
document.getElementById("modalCat").textContent = api.category;
document.getElementById("modalPop").innerHTML = starsHTML(api.popularity);
document.getElementById("modalDesc").textContent = api.description;
const independenceNotice = api.id === "xquik-api"
? " Xquik is an independent third-party service. Not affiliated with X Corp."
: "";
document.getElementById("modalDesc").textContent = api.description + independenceNotice;
document.getElementById("modalDocs").href = api.docs_url;

document.getElementById("modalGrid").innerHTML = `
Expand Down