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:

  1. Install dotenv in your project directory.
    npm install dotenv --save
    
  2. Create a file named .env at your project’s root, outside the src folder.
    A screenshot with a highlighted env file in the project root directory
    (Large preview)
  3. Store your key in the format of key-value pairs.
    // for CRA applications
    REACT_APP_API_KEY = A1234567890B0987654321C ------ correct
    
    // for Vite applications
    VITE_SOME_KEY = 12345GATGAT34562CDRSCEEG3T  ------ correct
    
    — note the placeholder indentation represents the file’s content structure.
  4. Save the file and never commit it to version control.
  5. 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
    
  6. 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:

  1. Install dependencies. You’ll need express for the server, cors for cross-origin requests, axios for making the API calls, and nodemon for auto-restarting during development.
    npm install express cors axios nodemon
    
  2. 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.
    A screenshot with a highlighted server.js file in the project root directory
    (Large preview)
  3. Initialize and configure. Require the installed packages, define a port, and create an endpoint that performs a GET request 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);
        });
    }
    
  4. Add a start script. Open your package.json and add a script entry that runs the server file.
    A screenshot with a script tag in a package.json file
    (Large preview)
  5. Launch the proxy. Start the server, then visit localhost:5000 to confirm the response renders.
    npm run start:backend
    
  6. 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:

  1. Scan the codebase for any hardcoded keys, tokens, or passwords. Identify every location that needs to be moved to a secure store.
  2. Replace hardcoded values with environment variables and add .env to your .gitignore file. This avoids future exposure in version control.
  3. 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.