Stop Exposing API Keys in the Browser: Move LLM Streaming to a BFF Layer
Written in front: Today the teacher explained a concept I deeply resonate with — BFF (Backend For Frontend). In the last lesson, we learned about streaming output, and the frontend directly called DeepSeek's API. As a result, we had to deal with ReadableStream, TextDecoder, while loops... the code was long and messy. The teacher said: "Leave this dirty work to the BFF layer!" The frontend is only responsible for display, and the backend proxy forwarding logic is thrown to the Node.js BFF layer. After hearing this, I shouted: This is real engineering!
1. What is BFF? A backend that "works" for the frontend
1.1 Why do we need BFF?
The teacher said:
"Frontend (Vue/React) → Node (BFF) → Backend (Java)."
Traditional three-tier architecture:
Frontend (Vue/React) → Backend (Java/Go) → Database
Four-tier architecture with BFF:
Frontend (Vue/React) → BFF (Node.js) → Backend (Java/Go) → Database
BFF is a "backend serving the frontend." It does not replace the real backend but adds a "translator" layer between the frontend and backend.
1.2 What is BFF suitable for?
| Suitable for BFF | Not suitable for BFF |
|---|---|
| Proxying LLM requests (hiding API Key) | Connecting to databases, CRUD |
| Handling binary streams for streaming output | Complex business logic |
| Formatting data, adapting to frontend needs | High-security, high-concurrency core business |
| Lightweight forwarding, aggregating multiple interfaces | Transaction management, distributed processing |
The teacher said:
"Senior frontend engineers write common Node services themselves to meet their own needs."
BFF is the moat of the "Big Frontend" — backend tasks that frontend engineers can handle themselves.
2. BFF in Practice: Building a Proxy Server with Express
2.1 Initializing the server
Look at server.mjs:
import * as dotenv from 'dotenv';
import express from 'express';
dotenv.config({
path: ['.env.local', '.env'],
});
const app = express();
const port = 3000;
app.listen(port, () => {
console.log(`Server is running on port ${port}`);
});
The teacher used Express — the most common and simplest development framework for Node.js.
2.2 A normal GET route
app.get('/', (req, res) => {
res.send('Hello World!'); // Send once
});
2.3 A proxy route for streaming output
app.get('/stream', async (req, res) => {
const { prompt } = req.query;
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.VITE_DEEPSEEK_API_KEY}`,
},
body: JSON.stringify({
model: 'deepseek-v4-flash',
stream: true,
messages: [{ role: 'user', content: prompt }]
})
});
console.log(response.body); // ReadableStream
});
BFF does a very important thing here:
The frontend no longer needs to know the API Key. The API Key is placed in the BFF layer. The frontend only sends requests to the BFF, and the BFF attaches the Key to call the LLM.
This is an improvement in security — the API Key is not exposed in the frontend code.
2.4 The complete data flow
Frontend (5173): fetch('/api/stream?prompt=…')
↓
Vite Proxy: proxy configuration in vite.config.js
↓
BFF (3000): /api → /stream route → Forward to DeepSeek API
↓
DeepSeek API: Returns streaming data
↓
BFF processes stream data → Returns to frontend
↓
Frontend displays
The teacher said:
"Frontend fetch → Node (BFF) → LLM server. Frontend business logic is very complex, dealing with binary stream objects, decoding, parsing data — all kinds of situations. Abstract it and put it in the Big Frontend BFF layer, inside Node. The frontend becomes simpler, reducing difficulty."
3. Cross-Origin Issues: Solved by Vite Proxy
3.1 What is cross-origin?
The teacher said:
"Whenever the domain, port, or protocol (http/https) is different, requests like fetch encounter cross-origin restrictions due to the same-origin policy."
The frontend runs on http://localhost:5173, and the BFF runs on http://localhost:3000.
Different ports mean the browser will block cross-origin requests.
3.2 Vite's Proxy solution
Look at vite.config.js:
// Vite project, proxy configuration, proxy the request and forward it
export default defineConfig({
plugins: [vue()],
server: {
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
}
}
}
});
The teacher said:
"
/apimarks a request for a backend API interface. It's no longer cross-origin, but you get a 502 — because the frontend doesn't provide this route. Vite intercepts all requests starting with/apiand proxies them to:3000."
The frontend request path:
Frontend request: /api/stream (no longer cross-origin, as it's same-origin)
↓ Vite recognizes the /api prefix
↓ Automatically forwards
BFF actually receives: /stream (the /api prefix is removed)
Vite Proxy is like a "package transfer station" — you hand the package to the courier station in your own community (5173), and it forwards it to the station in another community (3000) for you.
4. BFF + Streaming Output: How Much Simpler Does the Frontend Code Become?
4.1 Before having a BFF
The frontend had to handle directly:
const response = await fetch('https://api.deepseek.com/v1/chat/completions', {
headers: { 'Authorization': `Bearer ${apiKey}` },
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
// …while loop reading the stream…
4.2 After having a BFF
The frontend only needs:
fetch('/api/stream')
.then(res => res.json())
.then(data => {
console.log(data);
});
Of course, if it's streaming output, the frontend still needs to process the stream. But at least the API Key doesn't need to be exposed, the request address is shorter, and the backend forwarding logic can be managed centrally.
5. Summary: BFF is the "Protection Layer" for the Big Frontend
| Concept | Description |
|---|---|
| BFF | Backend For Frontend, a backend serving the frontend |
| Express | The most common web development framework for Node.js |
| dotenv | Reads environment variables from .env files |
| Cross-Origin | Requests to different ports/domains/protocols are blocked by the browser |
| Vite Proxy | Proxies frontend requests to the BFF backend |
| API Key Protection | The Key is placed in the BFF layer, unknown to the frontend |
The core value of BFF in three sentences:
- Hides sensitive information (API Key).
- Reduces frontend complexity (streaming processing is thrown to the BFF).
- Decouples frontend and backend (the frontend does not call third-party APIs directly).
Written at the end
The biggest takeaway today was understanding the role of BFF in AI projects. Previously, I thought the frontend could just call the API directly. Now I know — processing binary streams for streaming output is too complex, and putting the API Key on the frontend is too dangerous. BFF is there to "stand in front of the frontend" — it does the dirty work and shoulders the security concerns.
Next time an interviewer asks you: "What role does the BFF layer play in an AI project?"
You can calmly say:
"BFF (Backend For Frontend) adds a relay layer between the frontend and backend. In AI projects, it has three core functions: ① Hiding the API Key — the frontend sends a request to the BFF, the BFF attaches the Key to call the LLM, and the Key is not exposed on the browser side; ② Reducing frontend complexity — processing of ReadableStream for streaming output and data parsing is completed in the BFF layer, and the frontend is only responsible for display; ③ Cross-origin proxying — Vite Proxy forwards /api requests to the corresponding port of the BFF, avoiding frontend cross-origin issues. In implementation, you can quickly build a BFF service with Express, and a single line of proxy configuration solves the forwarding problem."
Then, looking at the interviewer's satisfied expression, silently think to yourself: This round, steady again.
All code examples in this article come from classroom learning materials and are genuinely runnable.