Is it safe to put an API key in client-side JavaScript?

Any secret that ships to the browser is a public secret. When an API key is written into client-side JavaScript, it is delivered to every visitor, and anyone can read it from the page source, the browser’s Network panel, or a single curl of the bundle. Minifying the code does not change that.

Why this matters

A browser has to receive the code in order to run it, so anything the code contains is readable by the person using the page. Frameworks make this easy to trigger by accident. Next.js inlines environment variables that begin with NEXT_PUBLIC_ into the JavaScript sent to the browser, which is exactly why those variables must never hold a secret; the value ends up in the bundle as plain text. Build tools with their own public prefixes, such as Vite, work the same way. The Web Crypto API does not solve this. It gives a page cryptographic primitives for hashing, signing, and encryption, but those operations run on the visitor’s machine inside JavaScript the visitor controls, so a key passed into them is visible and can be copied. The practical result is that a leaked key lets someone impersonate your application, burn your quota, run up your bill, or read and write whatever data the key is allowed to touch, all from outside your app and without ever seeing your login screen.

How to fix it

Keep the secret on the server. Move the third-party call behind your own endpoint or an edge function, attach the key there, and let the browser call only your proxy. If the browser genuinely must reach a paid or third-party service directly, have your server mint a short-lived, narrowly scoped token for each session instead of embedding a long-lived key. Never give a secret a browser-exposed variable name such as NEXT_PUBLIC_, VITE_, or PUBLIC_, and rotate any key that was ever shipped to a browser, because you must assume it is already known.

// Cloudflare Worker: keep the key on the server, not in the page
export default {
  async fetch(request, env) {
    const upstream = await fetch('https://api.example.com/data', {
      headers: { Authorization: 'Bearer ' + env.API_KEY },
    });
    return new Response(upstream.body, {
      status: upstream.status,
      headers: { 'Content-Type': 'application/json' },
    });
  },
};

How to verify the fix

Search the files the browser actually downloads, not your source tree. Fetch the JavaScript bundle and grep it for the key, the word secret, and the token, then open DevTools, watch the Network panel, and inspect the script responses for the same strings. If the value appears anywhere in a response the browser receives, it is public, regardless of how the source is organised.

# Search the served bundle, not your local source
curl -s https://example.com/static/app.js | findstr /I "api_key apikey secret token"

Automated check

The Auditor does not scan your JavaScript for embedded secrets. It reads the page it is given and that page’s response headers, and it does not download and analyse every bundle for hardcoded keys or tokens. Treat the check above as a manual or CI step, then use the scan for the header, cookie, and CORS issues it does cover.

Scan your app free →

Related issues