Table of Contents
- I define the feature before looking for an API
- I use a directory to make a shortlist, then open the provider’s documentation
- I decide whether the request belongs in the browser or on my server
- I make one small request and validate the data I will actually use
- I calculate the request budget before deciding that a free tier is enough
- I test the failure states before polishing the successful screen
- I record a go, hold, or reject decision
I choose a public API by checking whether it can support one useful feature: the right data, a workable authentication model, browser compatibility, a realistic request budget, and a clear response when something fails. A successful example request is a starting point. Before I build the interface, I want to know what would make that feature unreliable or unexpectedly difficult to maintain.
For a small web project, I recommend a short shortlist and a concrete acceptance test. This guide describes the workflow I would apply, with a practice endpoint, an illustrative traffic calculation, and a decision card you can reuse. It is a selection method, not a claim that I have operated every API in a directory or measured a provider’s long-term reliability.

I define the feature before looking for an API
I start with one sentence: “A visitor can enter a book title and see matching books with an author, a publication year when available, and a useful empty-result message.” That is a better selection brief than “I want to build something with a books API.” It tells me which fields matter and gives me a way to reject attractive but unsuitable services.
Next, I divide the data into required and optional fields. For that book finder, a stable identifier and a usable title may be required. A cover image can be optional because I can show a neutral placeholder. If the idea depends on complete, current pricing, however, missing prices could invalidate the entire feature. The same response can be good enough for one project and inadequate for another.
I also decide who the project serves. A practice exercise can use fictional records. A public directory needs appropriate real data. A tool that people rely on for important decisions needs stronger evidence about freshness, accuracy, support, and availability. I do not quietly promote a practice dependency into a production promise.
I use a directory to make a shortlist, then open the provider’s documentation
For discovery, I recommend browsing public-api.org when you need ideas across different API categories. Its directory presents discovery information such as authentication, HTTPS, and CORS labels. I would use those signals to find two or three plausible candidates, then follow each candidate to its own documentation before making a choice.
I treat a directory entry as a lead. A health indicator cannot tell me whether a particular endpoint returns the fields my feature needs, whether my browser origin is allowed, or whether my intended use fits the provider’s terms. An “unknown” label means I still need evidence; it is neither an automatic rejection nor permission to assume the best.
My shortlist note contains the exact endpoint, its documentation page, and the question it still needs to answer. “Can this return a publication year?” is actionable. “Looks interesting” is not. I stop adding candidates when I have enough to compare against the same requirements. Collecting twenty alternatives makes the decision harder without improving the feature.
Before writing integration code, I look for data-use terms, attribution requirements, caching rules, commercial-use restrictions, and any separate rights attached to images or other returned content. Public access alone does not establish those permissions. If a material rule is unclear, I put the candidate on hold and seek clarification or choose a better-documented alternative.
I decide whether the request belongs in the browser or on my server
The simplest viable architecture depends on the provider’s contract. For a small, read-only learning project, a public endpoint that permits browser requests and requires no secret can keep the first version straightforward. A provider that requires a confidential credential introduces a server-side responsibility. I make that decision before writing the interface around a particular URL.
| What I find | My next step |
|---|---|
| No secret is required, and the documented request works from my app’s origin | Consider a direct browser request for the initial feature. |
| The provider requires a confidential API key | Keep it on a server I control and expose only the operation the app needs. |
| The provider explicitly supplies a browser-safe key | Follow its restrictions and quota guidance; do not treat the key as confidential. |
| The example works in a terminal but fails in my browser | Inspect the browser’s network and console evidence before changing architecture. |
| The provider’s authentication or browser support is unclear | Hold the candidate until the missing requirement is resolved. |
For confidential credentials, placing a value in a frontend environment variable does not make it private if the build includes that value in downloadable JavaScript. I trace where the credential ends up. A server-side route also needs its own request controls; hiding the upstream key does not prevent someone from repeatedly calling my route.
CORS is a separate check. Browsers apply cross-origin access rules that a terminal client does not enforce in the same way. I test the actual method and headers from the origin that will host the app. Opening an endpoint in a tab, or receiving JSON in Postman, does not prove browser JavaScript can read it.
I do not use mode: "no-cors" as a JSON-reading fix: it gives the caller an opaque response. I also avoid routing a project through an arbitrary public CORS proxy. If a backend is needed, I want to own that dependency and its limits rather than inherit another unexplained failure point.
I make one small request and validate the data I will actually use
For the request-handling example, I use JSONPlaceholder’s fictional todo data. The official JSONPlaceholder guide is a helpful place to explore the practice endpoints and their behavior. Its simulated write operations do not persist changes, so I would not use a successful write example as evidence that an application now has durable storage.
The following function reads one record, applies an eight-second deadline, checks the HTTP response and content type, and validates the fields the interface expects. The deadline is an example of my application’s waiting budget, not a claim about the provider’s normal response time. For a different API, I would replace both the endpoint and the field checks.
async function readExampleTodo() {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 8000);
try {
const response = await fetch(
"https://jsonplaceholder.typicode.com/todos/1",
{ signal: controller.signal }
);
if (!response.ok) {
throw new Error(`Request failed: HTTP ${response.status}`);
}
const mime = response.headers.get("content-type") || "";
if (!mime.toLowerCase().includes("application/json")) {
throw new Error("The endpoint did not return JSON");
}
const record = await response.json();
if (
!record ||
!Number.isInteger(record.id) ||
typeof record.title !== "string" ||
typeof record.completed !== "boolean"
) {
throw new Error("The response is missing expected fields");
}
return {
id: record.id,
title: record.title,
completed: record.completed
};
} finally {
clearTimeout(timer);
}
}
try {
const todo = await readExampleTodo();
console.log(todo);
} catch (error) {
console.error("Could not load the example:", error.message);
}
The final try/catch can run in a browser console or a JavaScript module. In a page, I would connect it to a loading indicator and an error message. If I display a returned title, I use a text-rendering API such as textContent, rather than treating the string as trusted HTML.
The MDN guide to using Fetch explains an easy-to-miss detail: a response such as HTTP 404 does not, by itself, reject the fetch promise. That is why I check response.ok. Reading the body can fail separately, including when a response claims to be JSON but contains invalid JSON.
I keep this first test narrow. It is not a complete SDK, a load test, or a retry system. It answers whether one documented request can produce a usable record in the environment where the feature will run. A passing result still leaves terms, quotas, data coverage, and failure behavior to check.
I calculate the request budget before deciding that a free tier is enough
I count requests generated by the interface, not just visitors. One page can call several endpoints, fetch more pages of results, refresh automatically, and make additional requests when the user changes a filter. Development sessions also consume requests. Those details can matter more than the number of people who visit the project.
Here is a deliberately hypothetical planning example. Suppose I expect 200 visits per day, four API requests per loading round, and three loading rounds per visit. My baseline would be:
200 visits × 4 requests × 3 rounds = 2,400 requests per day.
If I reserve another 300 requests for development and debugging, my planning total becomes 2,700 per day. These are invented project assumptions to demonstrate the calculation, not measurements or any provider’s published allowance. I would compare that result with the actual plan’s limits and leave room for uncertainty.
I check bursts separately. Eight simultaneous visits making four requests each could create 32 requests close together even when the daily total looks comfortable. I therefore look for how the provider measures limits: per second, minute, day, API key, account, or IP address. Moving calls behind one server can concentrate traffic into a shared limit.
My first savings usually come from avoiding unnecessary requests: search after a deliberate action, cancel obsolete work when appropriate, avoid duplicate loads, and paginate intentionally. If the provider permits caching, I define how long cached data remains acceptable and how the page identifies stale results. I do not cache indiscriminately simply because it reduces a counter.
When a service returns HTTP 429, I treat it as a signal to slow down. A Retry-After header may describe when to try again, but I do not assume it is always present or accessible to browser code. I keep retries bounded and follow the provider’s instructions. Repeatedly retrying a throttled request can increase the problem.
I test the failure states before polishing the successful screen
A dependency is easier to accept when I can explain what a visitor sees when it fails. I write that behavior down before adding animations or a more elaborate layout. For a search interface, “no matches” and “could not load results” must stay distinct; an outage should not silently become evidence that the requested item does not exist.
| Case | How I check it | Useful behavior |
|---|---|---|
| Valid response | One documented example request | Show the expected fields and finish loading. |
| Empty result | A local fixture with no matching records | Explain that the search returned no matches. |
| Missing field or invalid JSON | An intentionally altered fixture | Show a data error or an explicitly allowed fallback. |
| HTTP 429 or a server error | A mocked response | End loading and offer an appropriate, bounded next action. |
| Slow or unavailable network | A delayed mock or browser network controls | Stop waiting at the chosen deadline and make retry deliberate. |
I simulate rate limits locally instead of sending enough traffic to provoke them on somebody else’s service. A mock proves how my code handles that response; it does not establish the provider’s actual quota or outage behavior. Keeping those two kinds of evidence separate makes the test results more useful.
I also check a second search that finishes before the first. If the older request completes later, it must not overwrite the newer result. Cancellation or a request identifier can help enforce that rule. This is especially useful for search boxes, where a technically successful response may already be irrelevant to the visitor.
I record a go, hold, or reject decision
Before committing to a candidate, I fill in a short card. I keep the unanswered questions visible because they tell me what to investigate next. A blank answer should not quietly turn into an assumption while I am busy building the page.
- Feature: the user action and the result I promise.
- Data: required fields, optional fields, empty results, and acceptable freshness.
- Request: exact endpoint, method, authentication, and browser or server placement.
- Usage: estimated daily requests, expected bursts, and the provider’s actual limits.
- Permissions: intended use, attribution, and any storage or caching conditions.
- Failure: timeout, error message, retry policy, and a clearly labeled demo fallback if needed.
- Decision: go, hold, or reject, with the reason and the date I checked it.
I choose go when the essential evidence supports the specific feature. I choose hold when a material question, such as caching permission or browser access, still needs an answer. I choose reject when the API cannot meet a core requirement, even if it is popular or easy to call. Optional cover art should not disqualify a useful book finder; missing the required book records should.
For a portfolio demo, I may also keep a small permitted fixture so I can demonstrate the interface when the live service is unavailable. I label that mode as sample data and never present it as a current live result. The feature remains understandable, while its limitations remain visible.
My next step after the card is a single vertical slice: one user action, one useful result, and a working failure state. Once that behaves correctly, I can expand the project with better evidence about the dependency I have chosen. The payoff is a smaller, clearer build whose API requirements I can explain before they become expensive to change.