Any secret exposed to client-side code is readable by every visitor's browser. This lesson covers proxying secret-requiring API calls through your own backend, why CORS doesn't hide client-side secrets, and using exponential backoff for responsible retry behavior.
1Every API Key in Your Bundle Is Public
Any environment variable exposed to client code gets baked directly into the JavaScript bundle shipped to every visitor, readable by anyone with browser developer tools. A secret API key with real privileges must never be exposed to client-side code under any circumstances.
2Proxying Secret-Requiring Calls Through Your Own Backend
If a third-party API requires a secret key, a React app should never call it directly. The client should instead call your own backend, which safely holds the real secret server-side and makes the actual third-party request on the client's behalf, returning the result.
3CORS Isn't a Security Feature for YOUR App
CORS protects a server from unwanted cross-origin requests; it does nothing to hide or protect anything already present in the client's JavaScript bundle. CORS configuration should never be relied upon as a substitute for genuinely keeping secrets off the client.
4Rate Limiting and Retry Discipline
A client retrying failed requests aggressively and immediately, with no backoff, risks triggering an API's rate-limit lockout or resembling a denial-of-service pattern. Exponential backoff for retries, along with respecting any Retry-After header, keeps client behavior responsible.
5Step-by-Step Breakdown
Every API Key in Your Bundle Is Public. Any environment variable exposed to client code (like Vite's VITE_ prefix, covered earlier) gets baked directly into the JavaScript bundle shipped to every visitor — readable by anyone with browser DevTools. A secret API key with real privileges (billing, admin access) should NEVER be exposed to client-side code, full stop.
Proxying Secret-Requiring Calls Through Your Own Backend. If a third-party API needs a secret key, the React app should NEVER call it directly — instead, the client calls YOUR backend, and your backend (which safely holds the real secret, server-side only) makes the actual third-party request and returns the result.
Why should a React app call your own backend instead of a third-party API directly, when that API requires a secret key?
- →It keeps the real secret key on the server, never exposed to the client bundle
- →Proxying is always strictly faster than a direct request
CORS Isn't a Security Feature for YOUR App. A common misconception: CORS (Cross-Origin Resource Sharing) protects a SERVER from unwanted cross-origin requests — it does nothing to protect the requesting browser or hide anything already sitting in the client's JavaScript bundle. Don't rely on CORS configuration as a substitute for actually keeping secrets off the client.
Rate Limiting and Retry Discipline. A client that retries failed requests aggressively and immediately, with no backoff, can accidentally hammer an API into a rate-limit lockout — or worse, look indistinguishable from a denial-of-service attack. Implement exponential backoff for retries, and always respect a Retry-After header if the API sends one.
Mastery Achieved. You now understand secure API consumption: never exposing secret keys to client code, proxying secret-requiring calls through your own backend, correctly understanding what CORS does and doesn't protect, and using exponential backoff for responsible retry behavior. This closes out React Security — next, you'll move into React with AI.
Level Up 🚀
Advanced cheat sheets, SEO tricks, and interview prep for this topic.
Browser Support
Fully supported.
Fully supported.
Fully supported.
Fully supported.
Accessibility (A11y)
1Retry Logic Should Update an Accessible Loading State
During exponential backoff retries, keep the user informed via an accessible loading or status indicator (role='status') rather than leaving a silent, indeterminate wait with no feedback.
SEO Implications
- 1
Exposed API Keys Can Enable Abuse That Harms Reputation and Availability
A leaked API key with real privileges can be abused for spam, scraping, or resource exhaustion, potentially degrading service availability or triggering third-party blocklisting that indirectly affects a site's reputation and reliability.
Best Practices
Audit Every VITE_-Prefixed (or Similarly Exposed) Environment Variable
Periodically review which environment variables are exposed to client code and confirm none of them are secrets with real privileges — public API keys meant for client use (like a Stripe publishable key) are fine; true secrets are not.
Always Implement Exponential Backoff for Client-Side Retries
Never retry a failed request immediately and repeatedly with no delay — exponential backoff (doubling the wait time after each failure) is a standard, responsible retry pattern respected by most APIs.
Frequent Bugs
A secret API key committed to a VITE_-prefixed environment variable ends up visible in the production bundle.
Move the secret-requiring logic to a backend endpoint, and have the client call that backend instead. Never prefix true secrets with VITE_ (or any client-exposure prefix).
A client-side retry loop with no backoff triggers the third-party API's rate limiter, causing cascading failures.
Implement exponential backoff between retry attempts, and respect any Retry-After header the API returns, rather than retrying immediately and repeatedly.
Real-World Examples
Proxying a Weather API Call Requiring a Secret Key
A weather widget needs data from a third-party API that requires a paid, secret API key tied to the account's billing. Instead of calling that API directly from the client (which would expose the key to every visitor), the React app calls a `/api/weather` endpoint on its own backend, which holds the real key server-side and forwards the request.
// Client
const response = await fetch('/api/weather?city=London');
// Server (holds the real secret, never sent to the client)
app.get('/api/weather', async (req, res) => {
const data = await fetch(`https://weatherapi.com/data?key=${process.env.WEATHER_SECRET_KEY}&city=${req.query.city}`);
res.json(await data.json());
});