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 }),
};
}
};
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();
}
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
methodquery 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)