跪拜 Guibai
← Back to the summary

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:

"/api marks 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 /api and 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:


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.