Three sources, three different categories of risk

The idea was simple and consistent with the rest of the site: 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 — the same reason it's worth automating a check instead of trusting your own attention, especially on text you've already read too many times to notice the differences.

When a CORS proxy like this makes sense (and when it doesn't)

A lightweight proxy via a Netlify Function makes sense when a third-party public API doesn't include the CORS headers needed to be called directly from the browser, request volume stays modest (no significant added cost on Netlify Functions), and the data isn't sensitive or doesn't require complex authentication to hide.

It doesn't make sense if the third-party API already supports CORS natively — in that case the proxy just adds an unnecessary network hop. It should also be avoided if call volume is high: a centralized proxy with no cache becomes a bottleneck and a single point of failure for multiple site features at once.

The alternative, when possible, is checking whether the provider offers an alternative endpoint with explicit CORS support, often documented separately from the main endpoint.

The most common mistakes in this type of integration: assuming "public and free" means "callable from a browser" (false by definition — CORS is a separate issue from authentication); confusing a CORS problem with an uptime problem on the downstream service, when they're actually two distinct causes even if they surface at the same point in the code; and not periodically checking whether a community-maintained repository or service has been archived, which effectively means the API will no longer receive fixes.

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; and 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 the management dashboard.

Frequently asked questions

Why can a fetch() call to a public API be blocked even if the API works correctly?

Because the block is decided by the visiting browser, not the API's server. If the Access-Control-Allow-Origin header is missing, the browser discards the response before handing it to the code, even if the data arrived fine. An identical request from a terminal works, because CORS only exists in a browser context.

Does a server-side proxy always fix a CORS problem?

It fixes the browser block, since the request originates from the server and isn't subject to the same rules. But if the remote service is slow or unreachable, the proxy will still return an error, just with a different code. It's the correct fix for CORS, not a guarantee of uptime.

How can you tell if a free public API has been abandoned?

Reliable signals: the open source repository behind it is archived or hasn't seen commits in a long time; an official data catalog, if one exists, explicitly states there's no updated data; multiple related endpoints return the same error consistently, not intermittently.

Does a 404 error on an API always mean the service was discontinued?

No, it often just means a specific resource doesn't exist at that moment. It becomes a discontinuation signal when it happens systematically across multiple related endpoints, including fallbacks, and is confirmed by an independent source like official documentation.

Why does FAQPage structured data need to match the visible page text?

Search engines use it to generate rich results and expect it to faithfully represent what a visitor actually sees. Even a small text mismatch can make content ineligible for the rich result — better to check it automatically than by eye.