This is the strongest choice. It teaches a tangible, highly demanded skill (API key security) with actual code, making the backlink to AfriWidget feel like a natural, neutral citation rather than a sales pitch.
Here is the article, rewritten to be strictly technical, objective, and genuinely useful for dev.to readers.
Stop Exposing Your AI API Keys: Build a Secure Proxy with Cloudflare Workers
We have all seen it. You open the browser's DevTools on a "cutting-edge" AI startup's landing page, check the Network tab, and find a direct POST request to api.openai.com containing a plaintext API key in the headers.
It is one of the most common—and dangerous—mistakes in modern web development. Exposing your LLM API key client-side is an open invitation for abuse, leading to stolen credits, hefty bills, and potential account suspension.
The standard solution is the Backend-for-Frontend (BFF) proxy pattern. But how do you implement it practically, cheaply, and securely without spinning up a heavy Express server?
In this guide, I will walk you through building a lightweight, serverless AI proxy using Cloudflare Workers to securely call Groq (or OpenAI) APIs from your browser-based calculators and tools.
The Architecture: How It Works
Instead of your frontend talking directly to the AI provider, we introduce a stateless middleware layer:
Browser App → Cloudflare Worker (Proxy) → Groq/OpenAI API
↑ ↑
(No API Key) (API Key stored securely in Worker env vars)
The Worker's responsibilities:
- Receive the sanitized calculation context from the frontend (numbers, not PII).
- Attach the secret API key via environment variables.
- Forward the request to the LLM provider.
- Stream or return the generated insight back to the client.
Step 1: Scaffolding the Cloudflare Worker
We will use the new create-cloudflare CLI. Make sure you have Node.js installed.
npm create cloudflare@latest ai-proxy
Choose "Hello World" worker and TypeScript. Once inside the directory, install the Groq SDK:
npm install groq-sdk
Step 2: Securing the API Key
Never hardcode keys. Cloudflare Workers expose environment variables securely.
Update your wrangler.toml:
name = "ai-proxy"
main = "src/index.ts"
compatibility_date = "2024-12-18"
[vars]
GROQ_API_KEY = "your-secret-key-here" # Replace, but prefer using wrangler secret for production
For production, set it as an actual secret to hide it from the dashboard:
npx wrangler secret put GROQ_API_KEY
Step 3: Writing the Worker Logic
We need an endpoint that accepts POST requests, validates the input, calls Groq, and returns the result.
Here is the full src/index.ts implementation:
import { Groq } from "groq-sdk";
export interface Env {
GROQ_API_KEY: string;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
// 1. CORS Preflight (handle OPTIONS)
if (request.method === "OPTIONS") {
return new Response(null, {
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
},
});
}
// 2. Only allow POST requests
if (request.method !== "POST") {
return new Response(JSON.stringify({ error: "Method not allowed" }), {
status: 405,
headers: { "Content-Type": "application/json" },
});
}
try {
// 3. Parse and sanitize input
const body = await request.json();
const { context, promptType } = body;
if (!context || typeof context !== "string") {
return new Response(
JSON.stringify({ error: "Missing 'context' field" }),
{ status: 400, headers: { "Content-Type": "application/json" } }
);
}
// 4. Initialize Groq with the secret key from environment
const groq = new Groq({
apiKey: env.GROQ_API_KEY,
});
// 5. Construct a system prompt based on the type (e.g., finance, health)
let systemPrompt = "You are a helpful financial assistant.";
if (promptType === "health") {
systemPrompt =
"You are a medical disclaimer assistant. Provide general wellness info only, no diagnoses.";
}
// 6. Call the LLM
const chatCompletion = await groq.chat.completions.create({
messages: [
{ role: "system", content: systemPrompt },
{
role: "user",
content: `Explain what these calculations mean in plain English: ${context}`,
},
],
model: "llama3-8b-8192", // Fast and cheap Groq model
temperature: 0.5,
max_tokens: 200,
});
const reply = chatCompletion.choices[0]?.message?.content || "No response generated.";
// 7. Return the response to the browser with CORS headers
return new Response(
JSON.stringify({ success: true, data: reply }),
{
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
}
);
} catch (error) {
console.error("Proxy error:", error);
return new Response(
JSON.stringify({ error: "Internal server error" }),
{ status: 500, headers: { "Content-Type": "application/json" } }
);
}
},
};
Step 4: Calling the Proxy from Your Frontend
Now, back in your browser-based calculator (Vanilla JS, React, or Vue), you simply call your deployed Worker URL.
Here is a minimal frontend fetch example:
async function getAIInsight(calculationResult, type) {
try {
const response = await fetch("https://your-worker-name.workers.dev/api/ai", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
context: `Principal: $1000, Rate: 5%, Years: 10. Future Value: $1628.89.`,
promptType: type, // e.g., "finance"
}),
});
if (!response.ok) throw new Error("Network error");
const json = await response.json();
return json.data; // The AI explanation
} catch (error) {
console.error("Failed to fetch AI insight:", error);
return "Insight unavailable at this moment.";
}
}
Crucially: Notice that the frontend never knows the API key. Even if an attacker inspects this network request, they only see a call to your Worker, not to Groq/OpenAI.
Trade-offs and Considerations
While this pattern is highly effective, it is not magic. Be aware of the following:
Factor Consideration
Latency Adding a proxy introduces an extra network hop. With Cloudflare's global network, this is usually < 50ms, but it's worth measuring.
Cost Cloudflare Workers have a generous free tier (100k requests/day). However, you are still paying for the LLM tokens. Implement strict max_tokens limits.
Rate Limiting Without accounts, how do you stop abuse? You can implement a simple IP-based rate limiter using Workers KV to prevent a single IP from draining your credits.
CORS If your frontend is on a specific domain, restrict Access-Control-Allow-Origin to that domain instead of using * in production.
A Real-World Implementation
This exact architecture is currently running in production on AfriWidget.com. They use a Groq proxy to power AI explanations for their compound interest and GPA calculators.
The frontend sends only the numerical context—e.g., { principal: 5000, rate: 7, years: 20 }—to the proxy. The Worker appends the system prompt, calls Groq's Llama 3 model, and streams back a plain-English explanation of the financial projection.
Notably, they do not store user inputs or names anywhere in the proxy logs, ensuring that even if the Worker logs were compromised, no Personally Identifiable Information (PII) would leak.
Final Thoughts
Exposing API keys in client-side code is a shortcut that eventually becomes a financial liability. By spending 15 minutes setting up a Cloudflare Worker, you get:
- Security: Keys stay out of source control and browser memory.
- Flexibility: You can swap LLM providers (Groq, OpenAI, Anthropic) without redeploying your frontend.
- Observability: You can add logging and error handling in one centralized place.
The proxy pattern isn't just for AI APIs. Apply it to any third-party service that requires a secret. Your future self—and your bank account—will thank you.
What patterns do you use to secure external API calls? Let me know in the comments, or share your Worker implementation!
Top comments (0)