API Bouncer

Buy me a coffee

Securing API Keys in Frontend Apps: A Practical Guide

APIs are the backbone of modern web apps. But every time your frontend talks to an API, there are security decisions to make. Get them wrong and you could expose API keys, leak user data, or open your app to attacks. This guide covers the most important security practices for frontend developers working with APIs.

Never expose API keys in client-side code

This is rule number one, and it gets broken constantly. If your JavaScript contains an API key, anyone can see it. Open DevTools, view source, or inspect network requests — the key is right there.

// DON'T DO THIS — the key is visible to everyone fetch('https://api.example.com/data?apikey=sk_live_abc123def456');

Even if you put the key in a .env file and use a bundler to inject it, the key ends up in your compiled JavaScript bundle. Environment variables in frontend frameworks like React or Vue are compiled into the client-side code at build time — they're not secret.

What to do instead

Move the API call to a backend server. Your frontend calls your server, your server calls the API with the key, and the key never touches the browser:

// Frontend — calls YOUR server, no key exposed fetch('/api/data').then(r => r.json()).then(console.log); // Your server (Express example) — holds the key safely app.get('/api/data', async (req, res) => { const response = await fetch('https://api.example.com/data', { headers: { 'Authorization': `Bearer ${process.env.API_KEY}` } }); const data = await response.json(); res.json(data); });

Many of the APIs listed on API Bouncer require no authentication at all, which eliminates this problem entirely. When choosing an API, consider whether you need one that requires a key or if a keyless alternative exists.

Build a simple proxy server

You don't need a complex backend. A minimal Express server with a few proxy endpoints is enough for most projects:

const express = require('express'); const app = express(); // Serve your frontend files app.use(express.static('public')); // Proxy endpoint for the third-party API app.get('/api/weather', async (req, res) => { try { const { city } = req.query; if (!city || typeof city !== 'string' || city.length > 100) { return res.status(400).json({ error: 'Invalid city parameter' }); } const response = await fetch( `https://api.weatherservice.com/forecast?q=${encodeURIComponent(city)}&key=${process.env.WEATHER_KEY}` ); const data = await response.json(); res.json(data); } catch (err) { res.status(500).json({ error: 'Failed to fetch weather data' }); } }); app.listen(3000);

Notice how we validate the input, use encodeURIComponent(), and return generic error messages. These small details matter.

Always use HTTPS

If your app or API uses plain HTTP, anyone on the network (coffee shop Wi-Fi, ISP, corporate proxy) can read every request and response in plain text — including API keys, auth tokens, and user data.

Handle auth tokens safely

If your app has user authentication and you're storing tokens (JWTs, session tokens) in the browser, where you store them matters:

For most apps, HttpOnly cookies are the right choice. If you must use localStorage, make sure your app has strong XSS protections.

Validate input on both sides

Never trust user input, even if you're just passing it through to an API. Validate on the frontend for user experience and on the backend for security.

// Frontend validation — good for UX function validateSearch(query) { if (!query || query.trim().length === 0) return 'Search term is required'; if (query.length > 200) return 'Search term is too long'; return null; // valid } // Backend validation — essential for security app.get('/api/search', (req, res) => { const query = req.query.q; if (!query || typeof query !== 'string') { return res.status(400).json({ error: 'Missing search query' }); } if (query.length > 200) { return res.status(400).json({ error: 'Query too long' }); } // Safe to use query now });

Pay particular attention to values that get interpolated into URLs. Always use encodeURIComponent() to prevent injection.

Rate limiting your proxy

If you build a proxy server, add rate limiting. Without it, someone could abuse your endpoint and burn through your API quota (or rack up charges on paid APIs):

const rateLimit = require('express-rate-limit'); const apiLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // limit each IP to 100 requests per window message: { error: 'Too many requests, please try again later' } }); app.use('/api/', apiLimiter);

CORS configuration on your server

If your backend and frontend are on different origins, you'll need to configure CORS on your server. Be specific about allowed origins in production:

const cors = require('cors'); // Development — allow anything app.use(cors()); // Production — restrict to your domain app.use(cors({ origin: 'https://yourdomain.com' }));

Using * as the allowed origin is fine for public APIs but not for your own backend that handles authenticated requests. For a deeper explanation, see our CORS guide.

Common mistakes to avoid

A security checklist

Before deploying any project that uses APIs, run through this:

Security is not about doing one thing perfectly. It's about layering multiple protections so that no single mistake is catastrophic. Start with the basics above and you'll be ahead of most projects out there.