The backend that powers my GeoGuess App, serving fresh Mapillary street-view shots, tagging them with country data, and keeping the global leaderboard flowing with minimal lag. Live at geo.api.oof2510.space
- Why This Exists
- Feature Highlights
- How the Image Pipeline Works
- How the App Uses It
- Environment Setup
- Run It Locally
- API Endpoints
- Tech Stack
- Contributing
- License
Guessing places should feel fast, global, and fair. The API makes that happen by:
- Pulling Mapillary imagery from a big list of cities + land-heavy regions.
- Keeping a rolling cache warm so the app doesn’t wait on live Mapillary calls.
- Limiting the cache to two shots per country so the game stays interesting.
- Double-checking country data with both OpenStreetMap’s Nominatim and BigDataCloud.
- Curated city fallbacks: Hundreds of cities across North/South America, Europe, Africa, Asia, Oceania, and the Caribbean mean Mapillary almost always has coverage.
- Parallel cache fills: Up to four fetches run at once, and a single background task keeps the cache stocked.
- Country diversity cap: No more than two cached images from the same ISO code, so players see new places.
- Geocoding do-over: If Nominatim says “unknown”, BigDataCloud steps in and we still return a country name + ISO code when possible.
- Fast empty-cache flow: When the cache is dry, the API kicks off a 15-image refill and immediately grabs one live Mapillary image so the user isn’t waiting.
- Clean JSON: Every response returns
imageUrl,coordinates,countryName, andcountryCode. - Rate-limit friendly: Built-in throttling keeps Mapillary requests comfortably under the 1k/min quota.
- Leaderboard support: Manages session signing, score submissions, and the public top list that the app displays.
flowchart TD
Start["Request /getImage"] -->|Cache hit| Pop["Pop cached entry"]
Pop --> RespondCached["Return cached image"]
RespondCached --> Refill["Trigger background refill if cache < 5"]
Start -->|Cache empty| Warmup["Fire fillCache(15) in background"]
Start -->|Cache empty| LiveFetch["getRandomMapillaryImage"]
LiveFetch --> LiveGeo["Reverse geocode<br/>(Nominatim -> BigDataCloud)"]
LiveGeo --> RespondLive["Return live image"]
Refill --> WorkerPool["Parallel Mapillary fetches<br/>(max 4)"]
WorkerPool --> CheckCountry["Skip if country<br/>already has 2 entries"]
CheckCountry --> Enrich["Reverse geocode<br/>for cache entry"]
Enrich --> CachePush["Push into cache"]
- fillCache(15) tries up to
15 * 5fetches so the country cap doesn’t freeze progress. - getRandomMapillaryImage rotates through city fallbacks, land regions, and fully random boxes.
- reverseGeocodeCountry runs at zoom levels 3 → 5 → 10 before falling back to BigDataCloud.
The React Native client plugs straight into these endpoints:
GET /getImage→ new round image + country info.POST /game/start→ creates a one-hour session with a unique seed.POST /game/submit→ saves scores once a session is finished.GET /leaderboard/top→ grabs the public leaderboard.
Firebase App Check tokens are required for the POST endpoints—the mobile app takes care of attaching them in the X-Firebase-AppCheck header.
-
Install packages
yarn install
-
Drop a
.envnext toindex.jsMAP_API_KEY=your_mapillary_token MONGO_URI=mongodb+srv://... FIREBASE_APP_ID=your_firebase_app_id FIREBASE_SERVICE_ACCOUNT_KEY_BASE64=base64_encoded_service_account_json PORT=8080 # optional
- Use
FIREBASE_SERVICE_ACCOUNT_KEYif you prefer raw JSON instead of Base64. - Need Corepack?
corepack enableand you’re good.
- Use
node index.js- Default port:
8080 - Health check:
http://localhost:8080/health - First boot tries to pre-fill 15 images—make sure
MAP_API_KEYis set.
- Cache hit → instant response.
- Cache miss → kicks off
fillCache(15)and also serves a live Mapillary image right away. - Response sample:
{ "imageUrl": "https://images.mapillary.com/...", "coordinates": { "lat": 47.5079, "lon": -18.8792 }, "countryName": "Madagascar", "countryCode": "MG" }
{
"gameSessionId": "6651fb7e5a842e0e4d816f17",
"seed": "vj5o4l1x0r8",
"expiresAt": "2024-06-08T19:52:31.824Z"
}{
"gameSessionId": "6651fb7e5a842e0e4d816f17",
"score": 9400,
"metadata": { "rounds": 10 }
}- Query param
limitranges from 1–100.
- Returns
{ "status": "ok", "timestamp": "..." }
- Node.js + Express — core API
- MongoDB — sessions + leaderboard
- Mapillary Graph API — imagery source
- OpenStreetMap Nominatim & BigDataCloud — reverse geocoding
- Firebase Admin + App Check — security layer
- Axios — HTTP client with retry + keep-alive
Ideas, fixes, PRs—always welcome. If you tweak the cache or geocoding logic, shout out how it affects API latency or accuracy so we can test it properly.
Licensed under MPL-2.0.
API status? Check the live health endpoint.
Made with ❤️ by oof2510 | API Status