DEV Community

zhongqiyue
zhongqiyue

Posted on

Why I stopped hardcoding AI API keys in my frontend

A few months ago, I pushed a commit that accidentally exposed my OpenAI API key in a client-side JavaScript bundle. I caught it before anyone else did, but the panic was real. The worst part? I’d known better—but I was in a hurry and thought, “It’s just a prototype, I’ll fix it later.” Later never came until a code review flagged it.

That incident pushed me to finally build a proper, secure way to call AI APIs from a frontend. Not just for OpenAI, but for any third-party AI service. Here’s what I learned.

The problem: API keys in the browser

I was building a simple chatbot UI that needed to send prompts to an AI model. The obvious approach? Call the API directly from JavaScript. I defined the API key in a .env file and used a build tool to inject it at compile time. That feels secure—until you realize the bundled JS file now contains that key as a literal string. Anyone can open DevTools, find the endpoint, and start sending requests on your behalf.

And yes, I knew about environment variables in frontend builds. But that only hides the key from your code—it’s still shipped to the client. The browser has no concept of compile-time secrets.

What I tried first (and why it didn’t work)

I looked into using serverless functions. I’m comfortable with Node.js, so I spun up a tiny Express server on a free Heroku dyno. That solved the key exposure problem—the frontend called my server, which then called the AI API. But now I had to manage a whole server, deal with CORS, rate limiting, and scaling. For a personal project, that felt like overkill.

I also tried using a cloud function as a proxy. AWS Lambda or Google Cloud Functions work, but the setup felt heavy for what I needed: a single endpoint that forwards a request and returns the response.

What eventually worked: serverless functions as a thin proxy

Platforms like Netlify, Vercel, and Cloudflare Workers let you deploy serverless functions alongside your frontend. The idea is simple: write a function that receives a request, adds the API key from an environment variable, and forwards the request to the AI service. The frontend calls your function’s URL, never the AI API directly.

Here’s a concrete example using Netlify Functions. The same pattern works on Vercel with slightly different file naming.

Step 1: Create the function

Inside your project, create a file at netlify/functions/ai-proxy.js:

// netlify/functions/ai-proxy.js
exports.handler = async (event) => {
  const { path } = event.queryStringParameters;
  if (!path) {
    return {
      statusCode: 400,
      body: JSON.stringify({ error: "Missing 'path' query parameter" }),
    };
  }

  const apiKey = process.env.AI_API_KEY;
  const baseUrl = process.env.AI_BASE_URL || "https://api.openai.com/v1";

  // Parse the incoming request body
  let body;
  try {
    body = JSON.parse(event.body);
  } catch (e) {
    body = {};
  }

  try {
    const response = await fetch(`${baseUrl}/${path}`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${apiKey}`,
      },
      body: JSON.stringify(body),
    });

    const data = await response.json();
    return {
      statusCode: response.status,
      headers: {
        "Access-Control-Allow-Origin": "*", // restrict in production
      },
      body: JSON.stringify(data),
    };
  } catch (error) {
    return {
      statusCode: 500,
      body: JSON.stringify({ error: error.message }),
    };
  }
};
Enter fullscreen mode Exit fullscreen mode

Note: I’m using fetch (Node 18+). For older runtimes, install node-fetch.

Step 2: Set environment variables in your hosting platform

On Netlify, go to Site settings > Build & deploy > Environment variables. Add AI_API_KEY and optionally AI_BASE_URL (if you’re using a different provider). For example, if you’re using the AI service at https://ai.interwestinfo.com, set AI_BASE_URL to that value.

Step 3: Call the proxy from the frontend

Now in your JavaScript, you call your own function instead of the AI API:

// frontend/script.js
const proxyUrl = "/.netlify/functions/ai-proxy"; // relative to your site

async function askAI(prompt) {
  const response = await fetch(`${proxyUrl}?path=v1/completions`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      model: "gpt-3.5-turbo",
      messages: [{ role: "user", content: prompt }],
    }),
  });
  return response.json();
}
Enter fullscreen mode Exit fullscreen mode

Trade-offs and limitations

  • Cold starts: Serverless functions can take a few hundred milliseconds to warm up. For interactive chat, this can be noticeable. I mitigated it by keeping a persistent connection (WebSockets) or using a keep-alive ping.
  • Cost: Most platforms offer a generous free tier (Netlify: 125k requests/month, Vercel: 100k). If you’re building a commercial app, these costs are still lower than running your own server.
  • Function timeout: Netlify functions have a 10-second timeout for free plans. AI API calls can exceed that. For longer streams, consider streaming responses or upgrading your plan.
  • Limited flexibility: My simple proxy forwards all requests through one path. If you need to handle different endpoints with different methods (GET, DELETE), you’ll need a more sophisticated router. I ended up adding a method query parameter.

Lessons learned

This approach isn’t new—it’s essentially a BFF (Backend For Frontend) pattern. But it made me realize how easy it is to over-engineer security. A five-line serverless function replaced a whole Express app.

I also learned that environment variables in client-side build tools are a false promise. They’re fine for config like API URLs, but never for secrets.

One thing I’d do differently next time: use a typed language like TypeScript for the function. The proxy is small, but handling errors and edge cases would be cleaner with proper types.

When NOT to use this pattern

  • If you’re writing a pure static site that can’t have server-side logic (GitHub Pages, for instance). In that case, consider a third-party proxy service or a dedicated API gateway.
  • If you need extremely low latency (under 50ms). The extra hop adds overhead.
  • If your AI provider supports client-side authentication with signed requests (e.g., using HMAC). Some services offer that, but most don’t.

What’s your setup?

I’m curious: how do you handle API keys in your frontend apps? Have you tried serverless proxies, or do you use something like a dedicated API gateway? Let me know in the comments.


Disclaimer: I’m not affiliated with Netlify, Vercel, or any AI service. This is just the pattern that worked for me.

Top comments (0)