[{"data":1,"prerenderedAt":917},["ShallowReactive",2],{"post-en-pattern-bff":3},{"id":4,"title":5,"body":6,"date":902,"description":17,"excerpt":903,"extension":904,"meta":905,"navigation":150,"path":906,"readTime":160,"seo":907,"slug":908,"stem":909,"tags":910,"__hash__":916},"en_blog/en/blog/pattern-bff.md","The BFF Pattern with FastAPI: Backend-for-Frontend",{"type":7,"value":8,"toc":891},"minimark",[9,14,18,23,39,60,74,78,88,91,95,100,357,361,536,540,655,659,662,800,804,884,887],[10,11,13],"h1",{"id":12},"the-bff-pattern-with-fastapi-putting-a-backend-in-front-of-your-frontend","The BFF Pattern with FastAPI: Putting a Backend in Front of Your Frontend",[15,16,17],"p",{},"The Backend-for-Frontend (BFF) pattern is not new — Netflix, SoundCloud, and others popularised it over a decade ago. Yet it remains underused in Vue.js and FastAPI architectures, where the prevailing tendency is to handle OAuth2 tokens directly in the browser. Here is why that is a risky trade-off, and how the BFF pattern addresses it.",[19,20,22],"h2",{"id":21},"the-problem-with-client-side-tokens","The Problem with Client-Side Tokens",[15,24,25,26,30,31,34,35,38],{},"In a standard Vue.js single-page application with Azure B2C, the OAuth2 flow terminates with an ",[27,28,29],"code",{},"access_token"," stored somewhere in the browser: ",[27,32,33],{},"localStorage",", ",[27,36,37],{},"sessionStorage",", or a cookie. Each of these options carries limitations:",[40,41,42,49,54],"ul",{},[43,44,45,48],"li",{},[46,47,33],"strong",{}," — readable by any JavaScript executing on the domain; directly exposed to XSS attacks",[43,50,51,53],{},[46,52,37],{}," — same vulnerabilities; discarded when the tab is closed",[43,55,56,59],{},[46,57,58],{},"HttpOnly cookie"," — the most defensible client-side option, yet token refresh still requires server-side coordination",[15,61,62,63,66,67,70,71,73],{},"The deeper issue is structural: the Azure B2C ",[27,64,65],{},"client_secret"," cannot be embedded in a single-page application. The OAuth2 code exchange (",[27,68,69],{},"authorization_code"," → ",[27,72,29],{},") must happen server-side. Without a BFF, teams either compromise on security or burden the frontend with PKCE and elaborate workarounds.",[19,75,77],{"id":76},"architecture-overview","Architecture Overview",[79,80,85],"pre",{"className":81,"code":83,"language":84},[82],"language-text","Browser (Vue.js)\n        │\n        │  Session cookie (HttpOnly, Secure)\n        ▼\n  FastAPI BFF\n        │\n        ├─── Redis (encrypted sessions + OAuth2 tokens)\n        │\n        └─── Azure B2C (code exchange, token refresh)\n                │\n                └─── Downstream APIs (access_token as Bearer)\n","text",[27,86,83],{"__ignoreMap":87},"",[15,89,90],{},"The browser never sees an OAuth2 token. It exchanges only an opaque session cookie with the BFF. The BFF holds the tokens and injects them into requests to downstream APIs on behalf of the client.",[19,92,94],{"id":93},"fastapi-implementation","FastAPI Implementation",[96,97,99],"h3",{"id":98},"session-management-with-redis","Session Management with Redis",[79,101,105],{"className":102,"code":103,"language":104,"meta":87,"style":87},"language-python shiki shiki-themes github-dark github-light","import json\nimport secrets\nfrom datetime import timedelta\nfrom cryptography.fernet import Fernet\nimport redis.asyncio as aioredis\nfrom fastapi import Request, Response\n\nclass SessionManager:\n    def __init__(self, redis: aioredis.Redis, secret_key: bytes):\n        self.redis = redis\n        self.fernet = Fernet(secret_key)\n        self.session_ttl = 3600  # 1 hour\n\n    def _session_key(self, session_id: str) -> str:\n        return f\"bff:session:{session_id}\"\n\n    async def create_session(self, response: Response, data: dict) -> str:\n        session_id = secrets.token_urlsafe(32)\n        encrypted = self.fernet.encrypt(json.dumps(data).encode())\n        await self.redis.setex(\n            self._session_key(session_id),\n            self.session_ttl,\n            encrypted\n        )\n        response.set_cookie(\n            key=\"session_id\",\n            value=session_id,\n            httponly=True,\n            secure=True,\n            samesite=\"lax\",\n            max_age=self.session_ttl\n        )\n        return session_id\n\n    async def get_session(self, request: Request) -> dict | None:\n        session_id = request.cookies.get(\"session_id\")\n        if not session_id:\n            return None\n        raw = await self.redis.get(self._session_key(session_id))\n        if not raw:\n            return None\n        return json.loads(self.fernet.decrypt(raw))\n","python",[27,106,107,115,121,127,133,139,145,152,158,164,170,176,182,187,193,199,204,210,216,222,228,234,240,246,252,258,264,270,276,282,288,294,299,305,310,316,322,328,334,340,346,351],{"__ignoreMap":87},[108,109,112],"span",{"class":110,"line":111},"line",1,[108,113,114],{},"import json\n",[108,116,118],{"class":110,"line":117},2,[108,119,120],{},"import secrets\n",[108,122,124],{"class":110,"line":123},3,[108,125,126],{},"from datetime import timedelta\n",[108,128,130],{"class":110,"line":129},4,[108,131,132],{},"from cryptography.fernet import Fernet\n",[108,134,136],{"class":110,"line":135},5,[108,137,138],{},"import redis.asyncio as aioredis\n",[108,140,142],{"class":110,"line":141},6,[108,143,144],{},"from fastapi import Request, Response\n",[108,146,148],{"class":110,"line":147},7,[108,149,151],{"emptyLinePlaceholder":150},true,"\n",[108,153,155],{"class":110,"line":154},8,[108,156,157],{},"class SessionManager:\n",[108,159,161],{"class":110,"line":160},9,[108,162,163],{},"    def __init__(self, redis: aioredis.Redis, secret_key: bytes):\n",[108,165,167],{"class":110,"line":166},10,[108,168,169],{},"        self.redis = redis\n",[108,171,173],{"class":110,"line":172},11,[108,174,175],{},"        self.fernet = Fernet(secret_key)\n",[108,177,179],{"class":110,"line":178},12,[108,180,181],{},"        self.session_ttl = 3600  # 1 hour\n",[108,183,185],{"class":110,"line":184},13,[108,186,151],{"emptyLinePlaceholder":150},[108,188,190],{"class":110,"line":189},14,[108,191,192],{},"    def _session_key(self, session_id: str) -> str:\n",[108,194,196],{"class":110,"line":195},15,[108,197,198],{},"        return f\"bff:session:{session_id}\"\n",[108,200,202],{"class":110,"line":201},16,[108,203,151],{"emptyLinePlaceholder":150},[108,205,207],{"class":110,"line":206},17,[108,208,209],{},"    async def create_session(self, response: Response, data: dict) -> str:\n",[108,211,213],{"class":110,"line":212},18,[108,214,215],{},"        session_id = secrets.token_urlsafe(32)\n",[108,217,219],{"class":110,"line":218},19,[108,220,221],{},"        encrypted = self.fernet.encrypt(json.dumps(data).encode())\n",[108,223,225],{"class":110,"line":224},20,[108,226,227],{},"        await self.redis.setex(\n",[108,229,231],{"class":110,"line":230},21,[108,232,233],{},"            self._session_key(session_id),\n",[108,235,237],{"class":110,"line":236},22,[108,238,239],{},"            self.session_ttl,\n",[108,241,243],{"class":110,"line":242},23,[108,244,245],{},"            encrypted\n",[108,247,249],{"class":110,"line":248},24,[108,250,251],{},"        )\n",[108,253,255],{"class":110,"line":254},25,[108,256,257],{},"        response.set_cookie(\n",[108,259,261],{"class":110,"line":260},26,[108,262,263],{},"            key=\"session_id\",\n",[108,265,267],{"class":110,"line":266},27,[108,268,269],{},"            value=session_id,\n",[108,271,273],{"class":110,"line":272},28,[108,274,275],{},"            httponly=True,\n",[108,277,279],{"class":110,"line":278},29,[108,280,281],{},"            secure=True,\n",[108,283,285],{"class":110,"line":284},30,[108,286,287],{},"            samesite=\"lax\",\n",[108,289,291],{"class":110,"line":290},31,[108,292,293],{},"            max_age=self.session_ttl\n",[108,295,297],{"class":110,"line":296},32,[108,298,251],{},[108,300,302],{"class":110,"line":301},33,[108,303,304],{},"        return session_id\n",[108,306,308],{"class":110,"line":307},34,[108,309,151],{"emptyLinePlaceholder":150},[108,311,313],{"class":110,"line":312},35,[108,314,315],{},"    async def get_session(self, request: Request) -> dict | None:\n",[108,317,319],{"class":110,"line":318},36,[108,320,321],{},"        session_id = request.cookies.get(\"session_id\")\n",[108,323,325],{"class":110,"line":324},37,[108,326,327],{},"        if not session_id:\n",[108,329,331],{"class":110,"line":330},38,[108,332,333],{},"            return None\n",[108,335,337],{"class":110,"line":336},39,[108,338,339],{},"        raw = await self.redis.get(self._session_key(session_id))\n",[108,341,343],{"class":110,"line":342},40,[108,344,345],{},"        if not raw:\n",[108,347,349],{"class":110,"line":348},41,[108,350,333],{},[108,352,354],{"class":110,"line":353},42,[108,355,356],{},"        return json.loads(self.fernet.decrypt(raw))\n",[96,358,360],{"id":359},"azure-b2c-oauth2-callback","Azure B2C OAuth2 Callback",[79,362,364],{"className":102,"code":363,"language":104,"meta":87,"style":87},"from fastapi import APIRouter, Request, Response\nfrom httpx import AsyncClient\n\nrouter = APIRouter()\n\n@router.get(\"/auth/callback\")\nasync def oauth_callback(\n    request: Request,\n    response: Response,\n    code: str,\n    state: str,\n):\n    # Code exchange happens entirely server-side\n    async with AsyncClient() as client:\n        token_response = await client.post(\n            f\"https://{settings.b2c_tenant}.b2clogin.com/\"\n            f\"{settings.b2c_tenant}.onmicrosoft.com/\"\n            f\"{settings.b2c_policy}/oauth2/v2.0/token\",\n            data={\n                \"grant_type\": \"authorization_code\",\n                \"client_id\": settings.client_id,\n                \"client_secret\": settings.client_secret,  # Never exposed to the browser\n                \"code\": code,\n                \"redirect_uri\": settings.redirect_uri,\n            }\n        )\n\n    tokens = token_response.json()\n    await session_manager.create_session(response, {\n        \"access_token\": tokens[\"access_token\"],\n        \"refresh_token\": tokens[\"refresh_token\"],\n        \"expires_at\": time.time() + tokens[\"expires_in\"]\n    })\n\n    return RedirectResponse(url=\"/\")\n",[27,365,366,371,376,380,385,389,394,399,404,409,414,419,424,429,434,439,444,449,454,459,464,469,474,479,484,489,493,497,502,507,512,517,522,527,531],{"__ignoreMap":87},[108,367,368],{"class":110,"line":111},[108,369,370],{},"from fastapi import APIRouter, Request, Response\n",[108,372,373],{"class":110,"line":117},[108,374,375],{},"from httpx import AsyncClient\n",[108,377,378],{"class":110,"line":123},[108,379,151],{"emptyLinePlaceholder":150},[108,381,382],{"class":110,"line":129},[108,383,384],{},"router = APIRouter()\n",[108,386,387],{"class":110,"line":135},[108,388,151],{"emptyLinePlaceholder":150},[108,390,391],{"class":110,"line":141},[108,392,393],{},"@router.get(\"/auth/callback\")\n",[108,395,396],{"class":110,"line":147},[108,397,398],{},"async def oauth_callback(\n",[108,400,401],{"class":110,"line":154},[108,402,403],{},"    request: Request,\n",[108,405,406],{"class":110,"line":160},[108,407,408],{},"    response: Response,\n",[108,410,411],{"class":110,"line":166},[108,412,413],{},"    code: str,\n",[108,415,416],{"class":110,"line":172},[108,417,418],{},"    state: str,\n",[108,420,421],{"class":110,"line":178},[108,422,423],{},"):\n",[108,425,426],{"class":110,"line":184},[108,427,428],{},"    # Code exchange happens entirely server-side\n",[108,430,431],{"class":110,"line":189},[108,432,433],{},"    async with AsyncClient() as client:\n",[108,435,436],{"class":110,"line":195},[108,437,438],{},"        token_response = await client.post(\n",[108,440,441],{"class":110,"line":201},[108,442,443],{},"            f\"https://{settings.b2c_tenant}.b2clogin.com/\"\n",[108,445,446],{"class":110,"line":206},[108,447,448],{},"            f\"{settings.b2c_tenant}.onmicrosoft.com/\"\n",[108,450,451],{"class":110,"line":212},[108,452,453],{},"            f\"{settings.b2c_policy}/oauth2/v2.0/token\",\n",[108,455,456],{"class":110,"line":218},[108,457,458],{},"            data={\n",[108,460,461],{"class":110,"line":224},[108,462,463],{},"                \"grant_type\": \"authorization_code\",\n",[108,465,466],{"class":110,"line":230},[108,467,468],{},"                \"client_id\": settings.client_id,\n",[108,470,471],{"class":110,"line":236},[108,472,473],{},"                \"client_secret\": settings.client_secret,  # Never exposed to the browser\n",[108,475,476],{"class":110,"line":242},[108,477,478],{},"                \"code\": code,\n",[108,480,481],{"class":110,"line":248},[108,482,483],{},"                \"redirect_uri\": settings.redirect_uri,\n",[108,485,486],{"class":110,"line":254},[108,487,488],{},"            }\n",[108,490,491],{"class":110,"line":260},[108,492,251],{},[108,494,495],{"class":110,"line":266},[108,496,151],{"emptyLinePlaceholder":150},[108,498,499],{"class":110,"line":272},[108,500,501],{},"    tokens = token_response.json()\n",[108,503,504],{"class":110,"line":278},[108,505,506],{},"    await session_manager.create_session(response, {\n",[108,508,509],{"class":110,"line":284},[108,510,511],{},"        \"access_token\": tokens[\"access_token\"],\n",[108,513,514],{"class":110,"line":290},[108,515,516],{},"        \"refresh_token\": tokens[\"refresh_token\"],\n",[108,518,519],{"class":110,"line":296},[108,520,521],{},"        \"expires_at\": time.time() + tokens[\"expires_in\"]\n",[108,523,524],{"class":110,"line":301},[108,525,526],{},"    })\n",[108,528,529],{"class":110,"line":307},[108,530,151],{"emptyLinePlaceholder":150},[108,532,533],{"class":110,"line":312},[108,534,535],{},"    return RedirectResponse(url=\"/\")\n",[96,537,539],{"id":538},"proxying-to-downstream-apis","Proxying to Downstream APIs",[79,541,543],{"className":102,"code":542,"language":104,"meta":87,"style":87},"@router.api_route(\"/api/{path:path}\", methods=[\"GET\", \"POST\", \"PUT\", \"DELETE\", \"PATCH\"])\nasync def proxy(request: Request, path: str):\n    session = await session_manager.get_session(request)\n    if not session:\n        raise HTTPException(status_code=401)\n\n    # Refresh the token proactively before it expires\n    if time.time() > session[\"expires_at\"] - 60:\n        session = await token_refresher.refresh(session)\n\n    async with AsyncClient() as client:\n        upstream = await client.request(\n            method=request.method,\n            url=f\"{settings.api_base_url}/{path}\",\n            headers={\"Authorization\": f\"Bearer {session['access_token']}\"},\n            content=await request.body(),\n        )\n\n    return Response(\n        content=upstream.content,\n        status_code=upstream.status_code,\n        media_type=upstream.headers.get(\"content-type\")\n    )\n",[27,544,545,550,555,560,565,570,574,579,584,589,593,597,602,607,612,617,622,626,630,635,640,645,650],{"__ignoreMap":87},[108,546,547],{"class":110,"line":111},[108,548,549],{},"@router.api_route(\"/api/{path:path}\", methods=[\"GET\", \"POST\", \"PUT\", \"DELETE\", \"PATCH\"])\n",[108,551,552],{"class":110,"line":117},[108,553,554],{},"async def proxy(request: Request, path: str):\n",[108,556,557],{"class":110,"line":123},[108,558,559],{},"    session = await session_manager.get_session(request)\n",[108,561,562],{"class":110,"line":129},[108,563,564],{},"    if not session:\n",[108,566,567],{"class":110,"line":135},[108,568,569],{},"        raise HTTPException(status_code=401)\n",[108,571,572],{"class":110,"line":141},[108,573,151],{"emptyLinePlaceholder":150},[108,575,576],{"class":110,"line":147},[108,577,578],{},"    # Refresh the token proactively before it expires\n",[108,580,581],{"class":110,"line":154},[108,582,583],{},"    if time.time() > session[\"expires_at\"] - 60:\n",[108,585,586],{"class":110,"line":160},[108,587,588],{},"        session = await token_refresher.refresh(session)\n",[108,590,591],{"class":110,"line":166},[108,592,151],{"emptyLinePlaceholder":150},[108,594,595],{"class":110,"line":172},[108,596,433],{},[108,598,599],{"class":110,"line":178},[108,600,601],{},"        upstream = await client.request(\n",[108,603,604],{"class":110,"line":184},[108,605,606],{},"            method=request.method,\n",[108,608,609],{"class":110,"line":189},[108,610,611],{},"            url=f\"{settings.api_base_url}/{path}\",\n",[108,613,614],{"class":110,"line":195},[108,615,616],{},"            headers={\"Authorization\": f\"Bearer {session['access_token']}\"},\n",[108,618,619],{"class":110,"line":201},[108,620,621],{},"            content=await request.body(),\n",[108,623,624],{"class":110,"line":206},[108,625,251],{},[108,627,628],{"class":110,"line":212},[108,629,151],{"emptyLinePlaceholder":150},[108,631,632],{"class":110,"line":218},[108,633,634],{},"    return Response(\n",[108,636,637],{"class":110,"line":224},[108,638,639],{},"        content=upstream.content,\n",[108,641,642],{"class":110,"line":230},[108,643,644],{},"        status_code=upstream.status_code,\n",[108,646,647],{"class":110,"line":236},[108,648,649],{},"        media_type=upstream.headers.get(\"content-type\")\n",[108,651,652],{"class":110,"line":242},[108,653,654],{},"    )\n",[19,656,658],{"id":657},"distributed-locking-for-token-refresh","Distributed Locking for Token Refresh",[15,660,661],{},"In a multi-pod environment, several workers may attempt to refresh the same token concurrently. A Redis lock prevents redundant refresh calls and the race conditions they produce:",[79,663,665],{"className":102,"code":664,"language":104,"meta":87,"style":87},"async def refresh(self, session: dict) -> dict:\n    lock_key = f\"bff:lock:refresh:{session['user_id']}\"\n\n    async with self.redis.lock(lock_key, timeout=10, blocking_timeout=8):\n        # Re-read the session — another pod may have already refreshed it\n        fresh = await self.session_manager.get_session_by_user(session[\"user_id\"])\n        if time.time() \u003C fresh[\"expires_at\"] - 60:\n            return fresh  # Already refreshed; nothing to do\n\n        async with AsyncClient() as client:\n            response = await client.post(\n                settings.token_endpoint,\n                data={\n                    \"grant_type\": \"refresh_token\",\n                    \"refresh_token\": fresh[\"refresh_token\"],\n                    \"client_id\": settings.client_id,\n                    \"client_secret\": settings.client_secret,\n                }\n            )\n        new_tokens = response.json()\n        updated = {\n            **fresh,\n            **new_tokens,\n            \"expires_at\": time.time() + new_tokens[\"expires_in\"]\n        }\n        await self.session_manager.update_session(updated)\n        return updated\n",[27,666,667,672,677,681,686,691,696,701,706,710,715,720,725,730,735,740,745,750,755,760,765,770,775,780,785,790,795],{"__ignoreMap":87},[108,668,669],{"class":110,"line":111},[108,670,671],{},"async def refresh(self, session: dict) -> dict:\n",[108,673,674],{"class":110,"line":117},[108,675,676],{},"    lock_key = f\"bff:lock:refresh:{session['user_id']}\"\n",[108,678,679],{"class":110,"line":123},[108,680,151],{"emptyLinePlaceholder":150},[108,682,683],{"class":110,"line":129},[108,684,685],{},"    async with self.redis.lock(lock_key, timeout=10, blocking_timeout=8):\n",[108,687,688],{"class":110,"line":135},[108,689,690],{},"        # Re-read the session — another pod may have already refreshed it\n",[108,692,693],{"class":110,"line":141},[108,694,695],{},"        fresh = await self.session_manager.get_session_by_user(session[\"user_id\"])\n",[108,697,698],{"class":110,"line":147},[108,699,700],{},"        if time.time() \u003C fresh[\"expires_at\"] - 60:\n",[108,702,703],{"class":110,"line":154},[108,704,705],{},"            return fresh  # Already refreshed; nothing to do\n",[108,707,708],{"class":110,"line":160},[108,709,151],{"emptyLinePlaceholder":150},[108,711,712],{"class":110,"line":166},[108,713,714],{},"        async with AsyncClient() as client:\n",[108,716,717],{"class":110,"line":172},[108,718,719],{},"            response = await client.post(\n",[108,721,722],{"class":110,"line":178},[108,723,724],{},"                settings.token_endpoint,\n",[108,726,727],{"class":110,"line":184},[108,728,729],{},"                data={\n",[108,731,732],{"class":110,"line":189},[108,733,734],{},"                    \"grant_type\": \"refresh_token\",\n",[108,736,737],{"class":110,"line":195},[108,738,739],{},"                    \"refresh_token\": fresh[\"refresh_token\"],\n",[108,741,742],{"class":110,"line":201},[108,743,744],{},"                    \"client_id\": settings.client_id,\n",[108,746,747],{"class":110,"line":206},[108,748,749],{},"                    \"client_secret\": settings.client_secret,\n",[108,751,752],{"class":110,"line":212},[108,753,754],{},"                }\n",[108,756,757],{"class":110,"line":218},[108,758,759],{},"            )\n",[108,761,762],{"class":110,"line":224},[108,763,764],{},"        new_tokens = response.json()\n",[108,766,767],{"class":110,"line":230},[108,768,769],{},"        updated = {\n",[108,771,772],{"class":110,"line":236},[108,773,774],{},"            **fresh,\n",[108,776,777],{"class":110,"line":242},[108,778,779],{},"            **new_tokens,\n",[108,781,782],{"class":110,"line":248},[108,783,784],{},"            \"expires_at\": time.time() + new_tokens[\"expires_in\"]\n",[108,786,787],{"class":110,"line":254},[108,788,789],{},"        }\n",[108,791,792],{"class":110,"line":260},[108,793,794],{},"        await self.session_manager.update_session(updated)\n",[108,796,797],{"class":110,"line":266},[108,798,799],{},"        return updated\n",[19,801,803],{"id":802},"trade-off-summary","Trade-off Summary",[805,806,807,823],"table",{},[808,809,810],"thead",{},[811,812,813,817,820],"tr",{},[814,815,816],"th",{},"Aspect",[814,818,819],{},"Without BFF",[814,821,822],{},"With BFF",[824,825,826,838,851,862,873],"tbody",{},[811,827,828,832,835],{},[829,830,831],"td",{},"Tokens in the browser",[829,833,834],{},"Yes (localStorage / cookie)",[829,836,837],{},"Never exposed",[811,839,840,845,848],{},[829,841,842,844],{},[27,843,65],{}," exposure",[829,846,847],{},"Absent — PKCE required",[829,849,850],{},"Server-side only",[811,852,853,856,859],{},[829,854,855],{},"Token refresh",[829,857,858],{},"Frontend-managed",[829,860,861],{},"BFF-managed, with distributed lock",[811,863,864,867,870],{},[829,865,866],{},"XSS attack surface",[829,868,869],{},"Tokens accessible",[829,871,872],{},"Opaque session cookie only",[811,874,875,878,881],{},[829,876,877],{},"Operational complexity",[829,879,880],{},"Simpler",[829,882,883],{},"Additional service to operate",[15,885,886],{},"The BFF pattern is not a universal prescription. For public-facing applications with low-sensitivity data, client-side PKCE is simpler and perfectly adequate. However, for applications managing tokens with elevated privileges, sensitive scopes, or multi-API integrations — the BFF is the most architecturally sound approach available.",[888,889,890],"style",{},"html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":87,"searchDepth":117,"depth":117,"links":892},[893,894,895,900,901],{"id":21,"depth":117,"text":22},{"id":76,"depth":117,"text":77},{"id":93,"depth":117,"text":94,"children":896},[897,898,899],{"id":98,"depth":123,"text":99},{"id":359,"depth":123,"text":360},{"id":538,"depth":123,"text":539},{"id":657,"depth":117,"text":658},{"id":802,"depth":117,"text":803},"2025-03-13",null,"md",{},"/en/blog/pattern-bff",{"title":5,"description":17},"pattern-bff","en/blog/pattern-bff",[911,912,913,914,915],"FastAPI","Vue.js","Azure B2C","Architecture","OAuth2","gpMAUaUWHFwn22F2ddZvQBWwLj0EGKM2PKUsk8TExMY",1774645635810]