I wanted to build a page aggregating real-time space data from three free public sources: astronomy photos, the Space Station’s position, upcoming launches. In theory, three fetch() calls and done. In practice each of the three sources broke in a different way, and none of the three ways was what I expected.
Three sources, three different categories of risk
The idea was simple: zero server costs, zero keys to manage where possible, data read straight from the browser. The page needed to show the astronomy picture of the day and Mars rover images, the International Space Station’s real-time position, and upcoming launches with rocket and reusable-booster data — three categories, three different providers, all documented as “free public API, no complicated signup”.
I assumed “public and free” also meant “callable directly from a web page.” That’s not true by definition, and it was the first thing that surfaced once the page went live.
CORS blocked: when the problem isn’t in your code
The launches section returned a console error that was as clear as it was unhelpful at first glance:
No 'Access-Control-Allow-Origin' header is present on the requested resource
The key detail is that this check isn’t done by the API’s server — it’s done by the browser of whoever is visiting the site. If the server doesn’t include that header in the response, the browser discards the response before it ever reaches the JavaScript code, even though the requested data arrived just fine. An identical request made from a terminal works without a hitch, because CORS is a rule that only exists in a browser’s context.
The standard fix is to move the call server-side: a small Netlify Function that queries the third-party API and returns the response with the correct CORS headers pointed at your own domain. The user’s browser only talks to your own function, not to the remote API, so the browser-side CORS check simply no longer applies to that part of the path.
Almost never enough to stop there. I wrote the proxy, deployed it, and the section kept returning errors — just with a different code.
The proxy works, but the response is still a 502
With the proxy in place the CORS block was gone, but requests came back with a 502 Bad Gateway: the function was reaching the remote API correctly, but it wasn’t responding usefully. Before digging further into my own code, I checked the status of the open source project powering that API. The GitHub repository had been archived about a month earlier — read-only, no new commits possible. In the meantime some endpoints had silently migrated to a new version while others stayed on the old one, and the infrastructure behind the API showed signs of broken connectivity, not just slowness.
An archived repository is as strong a signal as a changelog entry: nobody is triaging issues anymore, nobody is shipping fixes. Continuing to debug against a service in that state is time spent chasing a target that might stop existing entirely in the meantime.
Replacing the source, not just the code
The sturdier choice at that point wasn’t another round of patches but switching providers. I found a launch-tracking API that’s professionally and actively maintained, with a different but comparable data shape: the same ability to list future and past launches, the same access to rocket configurations, and — a detail I hadn’t taken for granted — tracking of individual reusable boosters too, with serial number and status, the exact equivalent of what the discontinued API offered.
Before writing a single line of code I verified the data schema with a real live call, instead of trusting the documentation alone: a ten-minute check that avoided another round of “ship it, someone reports the bug, fix it.”
A 404 that actually means “closed for good”
In parallel, the Mars rover photos section had stopped returning results: the main endpoint responded 404, and even the fallback endpoint I’d written for exactly this situation responded 404 too. At that point the suspicion shifted from “malformed request” to “the service no longer exists,” and the confirmation came from a place I hadn’t checked first: the official data catalog linked to that API stated in plain text that the dataset contained no data, maintained by a single external developer, untouched for over a year.
The practical difference between an isolated 404 and a structural one sits right here: a single error on one endpoint, maybe intermittent, is often a temporary hiccup. Multiple related endpoints returning the same error consistently, confirmed by an independent source like an official data catalog, tell a different story — an abandoned service, not a service outage.
A replacement that slightly changes what the page promises
In place of the discontinued service I connected the same agency’s official image library, a different, actively maintained piece of infrastructure — but with a slightly different content nature: from “the latest photo the rover took in the last few hours” to “relevant images from the official archive, filtered by rover and sorted by date.” Same real photos, same institutional source, but no longer a snapshot of the exact moment.
The temptation in these cases is to pretend nothing changed and keep presenting the section as “latest photos.” I chose to state it explicitly next to the selector instead, with a line explaining why these aren’t strictly the most recent photos anymore: an honestly disclosed limitation is far less of a problem than an unexplained result the user has to figure out on their own.
A quieter bug: when structured data doesn’t tell the truth
In the final review pass I found two problems that had nothing to do with the external APIs. The first was simple: the meta description comfortably exceeded the roughly 155-160 characters Google shows before truncating awkwardly in search results. The second was sneakier: three of the six FAQPage structured-data question/answer pairs had slightly different wording than the text actually visible on the page — differences born from writing the two blocks in separate passes, invisible on a quick re-read.
Search engines expect FAQPage structured data to faithfully mirror what a visitor actually sees, not a summary or a paraphrase: even a small mismatch can make the content ineligible for the rich result. I found it with a small script that programmatically compares the two versions word for word, not by eyeballing it.
(While writing this very recap, I made the exact same mistake once — a nice reminder that this class of bug doesn’t announce itself.)
What I take away from this
The most useful lesson isn’t about a single error, but about an order in which to verify things:
- A real call always beats documentation, especially for a community-maintained project.
- A proxy fixes CORS but doesn’t guarantee the uptime of the service downstream — two separate problems even when they show up at the same point in the code.
- An archived repository is already an answer, no need to wait for the API to formally stop responding too.
- Structured data needs to be checked against visible content automatically, because small wording differences almost always slip past a manual read.
If you’re interested in the same server-side proxy pattern applied to a different case, I also wrote about summarizing PDFs with Gemini Flash-Lite via a Netlify Function. And if the topic is more generally about integrating an external API into a vanilla JS project, there’s also the article on the Gemini API chatbot inside a management dashboard.
I originally published this on my site, where you can also try the live Space page this post is about.
Top comments (0)