- React Native
- Expo SDK 54
- JavaScript
- react-native-maps
- AsyncStorage
- expo-location
- React Navigation
Six sources, one interface
- 6 APIs behind a single service layer, each with its own cache policy
- 1,000+ Map markers populated from bulk data before any paid call is made
- 4 Overpass mirrors cycled through on failure, with linear backoff
| Source | Purpose | Cache |
|---|---|---|
| NPS | Federal parks and monuments | In-memory |
| OSM Overpass | Trails and city parks | 30 days |
| YouTube | Park and trail video | 7 days |
| Google Places | Ratings, photos, hours | 30 days |
| OpenWeather | Five-day forecast | 3 hours |
| Recreation.gov | Campgrounds and booking | 7 days |
Cache lifetimes are matched to how fast the data actually changes. Weather goes stale in hours; a trail network changes over years. Treating them the same would mean either serving old forecasts or paying to re-fetch footpaths that have not moved since the last ice age.
Keeping the cost down
-
Lazy enrichment
Bulk OpenStreetMap data fills the map with over a thousand markers for nothing. Google Places is called only for the marker a user actually taps, and the result is cached by coordinate. Real cost lands around five cents per click, inside the free tier.
-
Two-phase fallback
Point-of-interest quality varies, so a Places lookup tries text search first and falls back to nearby search. One clean path would leave gaps on exactly the obscure places worth discovering.
-
Strict type filtering
Google Places results are checked against a whitelist of preferred types. Without it a search for a city park returns car parks, which is the kind of failure users blame on the app rather than the data.
-
Budget alerts
Thresholds set on Google Cloud at 50, 90, 100, and 150 per cent. A caching bug should send an email, not a bill.
Making it survive the network
-
Multi-endpoint retry
When an Overpass mirror returns a 504, the client cycles through four fallback servers with linear backoff of one, two, then three seconds, under a thirty-second client-side timeout enforced with AbortController.
-
Partial failure stays partial
Error handling is per source, so trails can fail while parks still load. A single unavailable mirror should cost the user one layer of the map, not the screen.
-
Sequential, not parallel
Heavy Overpass queries run one after another. Firing them in parallel was faster on paper and produced a wall of 504s in practice — the server was the constraint, not the client.
-
Graceful without permission
Distance labels use the Haversine formula with five-minute location caching for battery. If location permission is refused, the feature disappears rather than the app stalling on a prompt.
Some details worth naming
-
Trail length from raw geometry
On-demand OpenStreetMap geometry queries return the full polyline. Path length is the sum of Haversine distances across every node, and a trail is called a loop when its start and end are within fifty metres.
-
A timing race, solved with state
Navigating to a favourite and focusing the map on it meant the navigation parameters arrived before the map had mounted. A pending-focus value held until the map reports ready is the fix.
-
Unified search
One search bar queries parks, trails, and city parks at once, with defensive field normalisation because the three sources disagree about what a name, an identifier, and a coordinate look like.
-
Multi-trip model
A trip list with an active trip, supporting stop reordering, visited tracking with auto-skip routing, and export to multi-stop Google Maps navigation.
How it is built
A service layer owns every network call, cache, and error path. Screens never call fetch directly; they compose components and manage navigation state. Components stay presentational and type-flexible — the photo gallery accepts either an array of strings or an array of objects, because two of the sources disagree on that too.
Each source gets its own service module, which is what makes the caching policies legible: the file that knows how volatile weather is, is the file that sets the three-hour lifetime.
Scope and limits
- Coverage is the United States. The park and campground sources are federal, so the model does not transfer to another country without replacing them.
- There is no server. API keys live in the client, which is fine for a portfolio build and would need a proxy before any public release.
- Caches are on-device only, so a fresh install re-fetches everything and two users of the same app share nothing.
- No automated tests around the cache and retry logic yet, which is exactly where they would be worth the most.
What I took from it
Every failure mode here was a lesson in someone else's constraints. Overpass is free public infrastructure and rate-limits accordingly; Google charges per call and rewards you for asking less. Writing against six of them at once made the caching strategy a design problem rather than an afterthought.











