Why Front-End Secrets Don’t Stay Secret
APIs are the connective tissue of modern applications, letting one service request data from another. But every API interaction is gated by a credential, and if that credential ships to the browser, it isn’t really a secret at all. Anyone can inspect your JavaScript bundle and extract hardcoded keys, which opens the door to unauthorized requests that may cost you money or expose sensitive data.
API keys are long-lived tokens with no built-in expiration. If one leaks, the compromise is permanent until you manually rotate the key. Protecting them matters for three concrete reasons:
- Unauthorized access: A stolen key lets attackers make requests against the API, potentially reaching data they shouldn’t see.
- Cost overruns: Paid APIs bill per request or per unit. An attacker hammering your key can generate charges that land on your account.
- Data integrity: With the right permissions, a malicious actor can steal, modify, or delete data associated with your account.
Level 1: Environment Variables and .gitignore
The first line of defense is keeping keys out of your source code using environment variables. The dotenv package is a common way to load these into a React project.
Setup is straightforward:
- Install
dotenvin your project directory.npm install dotenv --save - Create a file named
.envat your project’s root, outside thesrcfolder.(Large preview) - Store your key in the format of key-value pairs.
— note the placeholder indentation represents the file’s content structure.// for CRA applications REACT_APP_API_KEY = A1234567890B0987654321C ------ correct // for Vite applications VITE_SOME_KEY = 12345GATGAT34562CDRSCEEG3T ------ correct - Save the file and never commit it to version control.
- Reference the variable in your code via
process.env.YOUR_KEY_NAME.// for CRA applications 'X-RapidAPI-Key':process.env.REACT_APP_API_KEY // for Vite applications 'X-RapidAPI-Key':import.meta.env.VITE_SOME_KEY - Restart your dev server so it picks up the new environment variables.
Since .env files are frequently pushed to GitHub by accident, add them to a .gitignore file at your repository’s root. This instructs Git to skip those files when staging and committing.
// .gitignore
# dependencies
/node_modules
/.pnp
.pnp.js
# api keys
.env
When you later deploy to platforms like Vercel or Netlify, you’ll need to re-enter your environment variables in that platform’s project settings, then redeploy the app.
Important caveat: environment variables are not bulletproof. In a client-side React app, your bundler will still inline environment variables into the JavaScript that ships to the browser. They are only *hidden from your source control*, not from a determined inspector.
Level 2: Back-End Proxy Server
If a key must reach a browser, it can be stolen. The robust solution is to remove the key from the client bundle entirely by routing API calls through your own back end.
A back-end proxy server acts as the intermediary. The front end contacts your server; your server attaches the API key and forwards the request to the third-party API. The response returns to your server, which strips any sensitive data before relaying it to the front end. The browser never sees the key, so the attack surface disappears.
To build a simple proxy with a React app:
- Install dependencies. You’ll need
expressfor the server,corsfor cross-origin requests,axiosfor making the API calls, andnodemonfor auto-restarting during development.npm install express cors axios nodemon - Create the server file. At your project’s root, outside
src, make a JavaScript file (e.g.,server.js) that holds all outbound API requests.(Large preview) - Initialize and configure. Require the installed packages, define a port, and create an endpoint that performs a
GETrequest to the third-party service before returning the data.// defining the server port const port = 5000 // initializing installed dependencies const express = require('express') require('dotenv').config() const axios = require('axios') const app = express() const cors = require('cors') app.use(cors()) // listening for port 5000 app.listen(5000, ()=> console.log(`Server is running on ${port}` )) // API request app.get('/', (req,res)=>{ const options = { method: 'GET', url: 'https://wft-geo-db.p.rapidapi.com/v1/geo/adminDivisions', headers: { 'X-RapidAPI-Key':process.env.REACT_APP_API_KEY, 'X-RapidAPI-Host': 'wft-geo-db.p.rapidapi.com' } }; axios.request(options).then(function (response) { res.json(response.data); }).catch(function (error) { console.error(error); }); } - Add a start script. Open your
package.jsonand add a script entry that runs the server file.(Large preview) - Launch the proxy. Start the server, then visit
localhost:5000to confirm the response renders.npm run start:backend - Point the front end at your proxy. Replace any direct calls to the API with requests to
http://localhost:5000/. The key stays on the server side.import axios from "axios"; import {useState, useEffect} from "react" function App() { const [data, setData] = useState(null) useEffect(()=>{ const options = { method: 'GET', url: "http://localhost:5000", } axios.request(options) .then(function (response) { setData(response.data.data) }) .catch(function (error) { console.error(error); }) }, []) console.log(data) return ( <main className="App"> <h1>How to Create a Backend Proxy Server for Your API Keys</h1> {data && data.map((result)=>( <section key ={result.id}> <h4>Name:{result.name}</h4> <p>Population:{result.population}</p> <p>Region:{result.region}</p> <p>Latitude:{result.latitude}</p> <p>Longitude:{result.longitude}</p> </section> ))} </main> ) } export default App;
Level 3: Key Management Services
Even if your keys are server-side, local machine theft is still a possibility. That is where encryption-based secret storage comes in.
Key management services (KMS) let you encrypt, store, rotate, and retrieve keys programmatically. They add a layer of protection that plain environment variables cannot provide, especially if your laptop is shared or lost. Three common choices are:
- AWS Secrets Manager: A fully managed Amazon service for storing database credentials, API keys, and other secrets, retrievable by API calls.
- Google Cloud Secret Manager: Google’s managed secrets platform, designed to integrate with other Google Cloud back-end services.
- Azure Key Vault: Microsoft’s cloud offering for storing passwords, keys, connection strings, and other confidential data in one centralized vault.
For advanced security needs, a KMS is the right architectural choice. For most projects, a combination of the other two techniques is sufficient.
Auditing Existing Projects
If you have already shipped React code with leaked secrets, you can remediate existing projects with a quick audit:
- Scan the codebase for any hardcoded keys, tokens, or passwords. Identify every location that needs to be moved to a secure store.
- Replace hardcoded values with environment variables and add
.envto your.gitignorefile. This avoids future exposure in version control. - For production-grade protection, route calls through a back-end proxy. For maximum security around particularly sensitive credentials, adopt a key management service.
Each layer removes another avenue for an attacker to discover your credentials. Start by keeping them out of Git. Move them to the server when you can. And finally, encrypt them with a dedicated management service. The combination, build for your threat model, keeps your API keys out of public view.



