How to Actually Secure API Keys in React Applications


Join thousands of professionals and get the latest insight on Compliance & Cybersecurity.
You've set up a React application and need to integrate with external APIs. You create a .env file, add your API key with the proper REACT_APP_ prefix, and feel like you've done your due diligence. But then that nagging thought hits you: "Is my API key actually secure?"
If you've ever inspected your production bundle and found your supposedly "hidden" API keys in plain text, you're not alone. It's a problem that leaves many developers feeling, as one put it, "kinda mind boggling."
The hard truth: You can never hide your API keys on the client side. If your key ends up in the frontend JavaScript bundle, it's not a secret anymore.
This article will show you the architectural pattern that professional developers use to actually secure API keys: the Backend-for-Frontend (BFF) pattern. Let's solve this problem once and for all.
The Illusion of Frontend Security: Why .env Files Aren't Enough


Environment variables are external configuration values that can be accessed by your application. In React applications built with Create React App, the most common approach is using .env files with the dotenv package.
Here's the typical setup:


- Install
dotenv:npm install dotenv --save - Create a
.envfile in your project root:# .env REACT_APP_API_KEY=A1234567890B0987654321C - Access the key in your code:
const apiKey = process.env.REACT_APP_API_KEY; // Example API call fetch('https://api.example.com/data', { headers: { 'Authorization': `Bearer ${process.env.REACT_APP_API_KEY}` } }); - Add
.envto your.gitignore:# .gitignore .env
This approach has a critical flaw: during the build process, Create React App performs a find-and-replace operation. Every instance of process.env.REACT_APP_API_KEY gets replaced with the actual string value "A1234567890B0987654321C" in your compiled JavaScript.
The .env file's purpose is to keep secrets out of version control—not to keep them out of your user's browser. As Smashing Magazine points out, this exposes you to risks like:
- Unauthorized API usage at your expense
- Financial liabilities if pay-per-use services are abused
- Data theft or manipulation
- Potential compliance violations
The Real Solution: The Backend-for-Frontend (BFF) Pattern
The Backend-for-Frontend pattern is a dedicated server that sits between your frontend (React SPA) and your downstream APIs. Its primary purpose is to serve the specific needs of the frontend client while keeping sensitive information secure.
Understanding Public vs. Confidential Clients
To understand why the BFF pattern works, we need to introduce a concept from OAuth 2.0:
- Public Clients: Applications that cannot securely store secrets (like SPAs, mobile apps). The browser environment is inherently insecure.
- Confidential Clients: Applications that can maintain the confidentiality of credentials (like server-side apps).
As explained by Auth0, a React SPA is a public client because any JavaScript running in the browser can be inspected. But a BFF is a confidential client because it runs in a controlled server environment.
How the BFF Solves the API Key Problem
Here's how the secure flow works:
- The React app makes a request to an endpoint on the BFF (e.g.,
/api/weather). It does not send any secret keys. - The BFF server receives this request and securely accesses the real API key from its environment variables.
- The BFF makes the request to the external API, attaching the secret key.
- The external API responds to the BFF.
- The BFF forwards the safe, non-sensitive data back to the React app.
The key security benefit:


The API key never leaves the server environment. It is never exposed to the browser, completely mitigating the risk of client-side inspection. As Nestenius.se explains, this significantly reduces the attack surface of your application.


Practical Guide: Building a Simple BFF Proxy Server
Let's implement a basic BFF proxy server using Express.js:
Step 1: Install Server Dependencies
npm install express cors axios nodemon dotenv
Step 2: Create the Server
Create a server.js file in your project root:
// server.js
const express = require('express');
const cors = require('cors');
const axios = require('axios');
require('dotenv').config();
const app = express();
const PORT = process.env.PORT || 8000;
// IMPORTANT: In production, restrict this to your frontend's domain
app.use(cors());
app.get('/api/data', async (req, res) => {
try {
const options = {
method: 'GET',
url: 'https://api.third-party.com/data', // The actual API URL
headers: {
'X-Api-Key': process.env.API_KEY // The secret key
}
};
const response = await axios.request(options);
res.json(response.data);
} catch (error) {
console.error(error);
res.status(500).json({ error: 'An error occurred' });
}
});
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
Create a server-specific .env file:
# .env
API_KEY=A1234567890B0987654321C
PORT=8000
Step 3: Update Your React Component
Modify your React component to call your BFF instead of the third-party API directly:
// MyComponent.jsx
import React, { useState, useEffect } from 'react';
function MyComponent() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
// Request is made to the BFF, not directly to the third-party API
const response = await fetch('http://localhost:8000/api/data');
const result = await response.json();
setData(result);
} catch (error) {
console.error("Error fetching data:", error);
setError(error);
} finally {
setLoading(false);
}
};
fetchData();
}, []);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error fetching data</div>;
return (
<div>
{/* Render your data here */}
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
);
}
export default MyComponent;
Hardening Your BFF: Authentication and Session Management
While a basic proxy hides your API keys, a production-grade BFF should also manage user sessions to protect the proxy endpoint itself. Otherwise, anyone could use your BFF to make API calls at your expense.
The key component is session management via secure cookies, as detailed in Auth0's BFF guide:
- When a user logs in, the BFF establishes a session and sends a cookie to the browser.
- This cookie must be configured with security attributes:
HttpOnly: Prevents JavaScript from accessing the cookie, mitigating XSS attacks.Secure: Ensures the cookie is only sent over HTTPS.SameSite=Strict(orLax): Defends against Cross-Site Request Forgery (CSRF) attacks.
The React app then sends requests to the BFF with this secure session cookie attached. The BFF validates the session before calling the downstream API with your secret key.
Advanced Security: Managing Keys with a Secrets Vault
For larger teams and production systems, hardcoding keys in .env files (even on the server) is not ideal. This is especially true when you need to address the challenges of:
- Preventing interns or junior developers from accessing sensitive API keys
- Managing the rotating of keys, which one developer described as "a pain in the ass"
The solution is a Secrets Vault - a dedicated service for secure storage and management of sensitive credentials. Leading providers include:
These services offer critical benefits:


- Centralized Management: A single source of truth for all secrets.
- Fine-Grained Access Control: Only authorized services and personnel can access specific secrets.
- Automated Rotation: Many services can automatically rotate API keys without downtime.
- Auditing: Provides a log of who accessed which secret and when.
With a secrets vault, your BFF server would fetch the API key from the vault rather than from process.env, adding an additional layer of security and management.
Conclusion
The fundamental rule of API key security is simple but absolute: Never expose API keys to the client.
Environment variables in React applications are useful for configuration but offer no security for API keys. The Backend-for-Frontend pattern creates a secure intermediary that keeps your keys safe on the server while serving your React application's needs.
By implementing a BFF with proper session management and potentially a secrets vault, you can build React applications that securely interact with external APIs without compromising your keys.
Remember: If an API key ends up in the frontend, it's not a secret anymore. Keep your keys on the server, and your applications will be significantly more secure.

