惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园 - 【当耐特】
Latest news
Latest news
IT之家
IT之家
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
L
LangChain Blog
腾讯CDC
J
Java Code Geeks
GbyAI
GbyAI
美团技术团队
V
Visual Studio Blog
Apple Machine Learning Research
Apple Machine Learning Research
Recorded Future
Recorded Future
U
Unit 42
Jina AI
Jina AI
月光博客
月光博客
罗磊的独立博客
I
InfoQ
有赞技术团队
有赞技术团队
B
Blog RSS Feed
The Register - Security
The Register - Security
WordPress大学
WordPress大学
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
MongoDB | Blog
MongoDB | Blog
NISL@THU
NISL@THU
S
Security Archives - TechRepublic
雷峰网
雷峰网
O
OpenAI News
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Y
Y Combinator Blog
G
GRAHAM CLULEY
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
L
LINUX DO - 热门话题
H
Help Net Security
www.infosecurity-magazine.com
www.infosecurity-magazine.com
S
Securelist
P
Proofpoint News Feed
C
Cybersecurity and Infrastructure Security Agency CISA
博客园 - 叶小钗
Security Latest
Security Latest
A
About on SuperTechFans
G
Google Developers Blog
T
Troy Hunt's Blog
小众软件
小众软件
H
Hacker News: Front Page
C
Cisco Blogs
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
L
LINUX DO - 最新话题
大猫的无限游戏
大猫的无限游戏
Webroot Blog
Webroot Blog

The Practical Developer

The Libuv Thread Pool Trap: Why Node.js Async APIs Stall Under Load Postgres Covering Indexes with INCLUDE: Eliminate Heap Fetches on Read-Heavy Workloads Postgres DISTINCT ON: The Fastest Way to Get the Latest Row Per Group Postgres Transaction Isolation: The Anomalies Your App Actually Faces in Production Linux TCP Tuning for Node.js Microservices: The Kernel Settings That Stop Silent Connection Drops Under Load Postgres HOT Updates and Fillfactor: Why Not All Writes Are Created Equal Database Connection Pool Leaks: Finding the Promise That Never Returns Its Seat Linux OOM Killer in Production: Why Your Node.js Containers Die Without a Stack Trace Postgres Materialized Views: Refresh Strategies That Do Not Lock Your Dashboards API Dependency Health Checks: Why /health Is Not Enough Authorization with Zanzibar Tuples: How Google Manages Permissions and How To Build the Same Check in Node.js Postgres Advisory Locks: The 20-Character Primitive That Replaces Redis for Coordination Dead Letter Queues: The Message Queue Pattern That Saves You at 2 a.m. File Descriptor Exhaustion: The Kernel Limit That Silently Drops Node.js Connections Graceful Degradation: The Pattern That Turns Total Outages into Partial Success PostgreSQL Full-Text Search: Dropping Elasticsearch for 90% of Use Cases S3 Presigned Multipart Uploads: Stop Your API Server from Being a File Upload Bottleneck MessagePack vs JSON: The Binary Serialization Switch That Cut Our Internal RPC Overhead by 40% DNS Caching in Node.js: The Silent Cause of Production Latency Spikes Reliable Cron Jobs: The Pattern That Stops Double Runs, Missed Executions, And The 2 AM Page GraphQL Query Complexity: Stop the OOM Query Before It Reaches Your Resolver Node.js Event Loop Lag: The Hidden Metric Behind Random Latency Spikes API Request Validation with Zod: The Schema That Catches Bad Input Before It Corrupts Your Database Load Shedding in Node.js: How to Reject Traffic Before You Drown Request Hedging: Cut Tail Latency In Half Without Overprovisioning Git Bisect: The Automated Binary Search That Finds Breaking Commits in Minutes Node.js Garbage Collection Tuning: Stop Letting V8 Pause Your Event Loop Node.js Server Timeouts: The Settings That Stop Slow Clients from Holding Sockets Hostage Postgres BRIN Indexes: The Time-Series Secret That Shrinks Indexes by 99% Event Sourcing with PostgreSQL: The Pragmatic 80% Solution Node.js Cluster Mode: Scaling the Event Loop Across CPU Cores Postgres Partial Indexes: Stopping Soft Deletes from Ruining Your Query Performance Request Coalescing with the Singleflight Pattern: Stop Drowning Your Database on Every Cache Miss The Bulkhead Pattern: Why One Slow Endpoint Should Not Drown Your Whole Service Node.js AsyncLocalStorage: End-to-End Request Context Without the Propagation Hell Postgres Deadlocks: Logging the Victim, Reproducing the Race, and Fixing the Lock Order Your Node.js HTTP Client Is the Bottleneck: Connection Pool Tuning That Works Optimistic Locking in Postgres: Stop Losing Data to Race Conditions Postgres Read Replicas: Stop Serving Stale Data to Your Users Cursor Pagination: Why Offset Queries Explode at Scale and How to Fix Them Node.js Worker Threads: 60 Lines That Stop a CSV Upload from Timing Out Every Other Request Reliable Webhook Delivery: Architecture for Outbound HTTP You Can Trust Request Timeouts and Deadline Propagation: Stop the Chain of Slowness Advanced Security Practices in Node.js Graceful Shutdown in Node.js: The 40 Lines That Stop 502s During Deploys Finding Node.js Memory Leaks with Heap Snapshots Idempotency Keys in 30 Lines: Stop Your Webhook From Charging Customers Twice Backpressure In Node.js: The Fix For Slow-Motion Queue Meltdowns Retries Done Right: Jitter, Budgets, and the Stampede You Did Not See Coming The Cache Stampede: Why Your "Just Add Redis" Layer Crashes Postgres at 3 a.m. Postgres SKIP LOCKED: An 80-Line Job Queue You Can Run Without Redis Stop Doing Work Nobody Wants: AbortController in Node.js, Done Right The N+1 Query Problem: We Found 23 In One Codebase And Killed Every One I Tried 5 AI Coding Tools for a Month. Here Is What I Actually Use CI/CD From Zero to Production in 30 Minutes With GitHub Actions Node.js vs Bun vs Deno: Which Runtime Should You Pick in 2025? Kubernetes Resource Requests And Limits: The Numbers That Decide If Your Cluster Is Stable The Three Pillars of Observability Are A Myth: What Actually Matters In Production pnpm Vs npm Vs yarn Vs Bun For Monorepos: Which One Earns The Migration In 2024 JSONB Indexing In Postgres: GIN Vs Expression Indexes, And When Each Is The Right Choice A Code Review Checklist That Ends The Same Three Arguments Every Sprint gRPC Vs REST In 2024: When The Switch Pays For Itself React Suspense For Data Fetching: The Pattern That Replaces Half Your Loading State Code The Five-Stage Rollout: How To Ship A Risky Change Without Holding Your Breath GitHub Actions In A Monorepo: Caching, Path Filters, And Secret Boundaries That Actually Work The Blameless Postmortem That Actually Improves Things: A Template And Six Hard-Won Rules Recursive CTEs In Postgres: How To Query A Tree Without N Round Trips Node.js Streams: When They Actually Help, And When They Just Add Complexity Playwright Vs Cypress In 2024: The Honest Comparison Of Which One Earns The Test Time React Server Components: The Mental Model That Makes The "use client" Boundary Obvious Pod Disruption Budgets: The K8s Object That Keeps Your Service Up During Cluster Maintenance Postgres LISTEN/NOTIFY: The Pub/Sub You Already Have And Are Not Using Chaos Engineering Starter Kit: The Five Drills That Don't Need Netflix-Scale Spec-Driven API Development With OpenAPI: How To Stop Drifting From Your Docs Kubernetes Autoscaling Beyond CPU: The Custom-Metric HPA Pattern That Actually Works Postgres Partitioning For Time-Series: The Boring Setup That Saves Your Database Distributed Locks With Redis: An Honest Look At Redlock And When You Don't Need It HTTP/2 vs HTTP/3: What Actually Changes For Your App, And What Doesn't Image Optimization For The Web In 2023: srcset, AVIF, And The Lighthouse Score You Actually Want Kafka vs RabbitMQ: A Decision Tree That Doesn't Hate You UUID vs Bigint Primary Keys In Postgres: The Index Math That Decides For You Flame Graphs: How To Find The Slow Function In 30 Seconds Without Profiling Theatre Postgres Streaming Vs. Logical Replication: Which One Solves Your Actual Problem ESLint Rules That Earn Their Keep: The Twelve I Enable On Every Project Pre-Commit Hooks That Pay For Themselves: Husky, lint-staged, And The Five Rules That Stick Zero-Downtime Database Migrations: The Six-Step Pattern That Rules Them All Circuit Breakers In Node.js: 50 Lines That Stop A Failing Dependency From Taking Down Your Service Postgres VACUUM Is Not Magic: How Your Hot Table Bloats To 80GB And How To Fix It Kubernetes Liveness And Readiness Probes: The Difference That Causes Half Your Outages Rate Limiting In Production: A Token Bucket In 30 Lines Of Redis The Outbox Pattern: How To Stop Losing Events When Postgres And Kafka Disagree Load Testing With k6: The Three Scenarios That Find Real Bugs (Not Synthetic Numbers) Postgres Row-Level Security For Multi-Tenant Apps: The Pattern That Stops You From Leaking Data Rebase vs. Merge: The Team Policy That Ends The Argument Forever OpenTelemetry in Node.js: Distributed Tracing That Actually Helps During an Incident Feature Flags That Pay Rent: The 4 Flag Types And When To Delete Each ETag, Last-Modified, and the Caching Headers Most APIs Get Wrong Connection Pooling Without the Cargo Cult: pgbouncer in 100 Lines of Config JSONB Is Not a Schema: When To Reach For It in Postgres, And When To Stop Bash Strict Mode: The Three Lines That Stop Your Deploy Script From Lying To You
The Backend for Frontend (BFF) Pattern: Stop Forcing Your Mobile App to Parse a Desktop-First API
The Practica · 2026-06-10 · via The Practical Developer

Your mobile team just shipped version 2.0. The feature set matches the web app. The screens look beautiful. And every single screen takes 2.4 seconds to load on 4G because the API returns 180 KB of JSON per page and the phone has to parse four extra fields, join data from two endpoints, and compute a derived value that the server already computed for the desktop view.

You have a single API serving two completely different clients. The web app needs lots of navigational links, embedded user profile cards, and rich text markdown. The mobile app needs the same data in a flatter structure with only the fields it renders, pre-joined so it does not have to make three round trips. Your monolithic API cannot be both things without being horrible for one of them.

The fix is the Backend for Frontend (BFF) pattern: a dedicated server-side layer per client that transforms, aggregates, and trims backend data into exactly what that client needs. One BFF per client type. Zero client-side joins. No field the mobile app does not render.

This post walks through the pattern with a real Node.js implementation, the routes where it helps most, the line between a BFF and a gateway, and the rules that keep your BFF from turning into a second monolith.

What a BFF actually is

A BFF is a thin server that sits between your clients and your backend services. Each client type (mobile, web, smart TV, CLI) has its own BFF. The BFF knows exactly what that client renders and fetches only the data the client needs, in the shape the client wants.

This is not an API gateway. An API gateway routes, rate-limits, and authenticates. That is infrastructure. A BFF is application logic. It aggregates data from multiple downstream services, transforms response shapes, formats dates for the client’s locale, and strips out fields the client does not display. It exists because the client team knows what the client needs, and that is not the same as what the backend team exposes.

Sam Newman coined the term in 2015 in a blog post about microservices. The core observation was: a general-purpose backend API is the wrong abstraction when your clients have fundamentally different rendering patterns, network characteristics, and data requirements. A mobile app on 4G is not a desktop browser on WiFi. Treating them the same makes both worse.

The problem in code

Here is a typical API response from a backend service that serves both a desktop web app and a mobile app:

{
  "id": "order_9k3m2",
  "status": "shipped",
  "created_at": "2026-06-08T14:23:11Z",
  "items": [
    {
      "product_id": "prod_101",
      "sku": "WIDGET-BLUE-L",
      "name": "Blue Widget, Large",
      "description": "A large blue widget made from recycled materials with a 12-month warranty and a happy-satisfaction guarantee. Suitable for indoor and outdoor use.",
      "specs": {
        "weight": "1.2kg",
        "dimensions": "30x20x15cm",
        "material": "recycled polymer",
        "color": "blue",
        "max_load": "15kg"
      },
      "price": 29.99,
      "currency": "USD",
      "inventory_count": 143,
      "supplier": "Acme Corp",
      "supplier_id": "su_442",
      "supplier_contact": "orders@acme-corp.example.com",
      "warehouse_location": "Aisle 12, Bay 4"
    }
  ],
  "shipping_address": {
    "street": "123 Main St",
    "city": "Portland",
    "state": "OR",
    "zip": "97201",
    "country": "US",
    "latitude": 45.5152,
    "longitude": -122.6784
  },
  "payment": {
    "method": "visa",
    "last_four": "4242",
    "billing_zip": "97201"
  },
  "estimated_delivery": "2026-06-12",
  "tracking_url": "https://track.example.com/9k3m2"
}

Now look at what the mobile order-detail screen actually renders:

  • Order number (just the ID)
  • Status badge (mapped to a color and icon)
  • Item name and price (not description, specs, or supplier info)
  • Shipping city and state (not lat/lng or full address)
  • Estimated delivery date

That is about 15% of the payload. The mobile app downloads the other 85% just to discard it. Every field that is not rendered is wasted bytes on a mobile connection, wasted CPU cycles parsing JSON on a device with limited memory, and wasted time decoding a response that could have been 18 KB instead of 180 KB.

A BFF for the mobile app returns exactly this:

{
  "id": "order_9k3m2",
  "status": "shipped",
  "items": [
    {
      "name": "Blue Widget, Large",
      "price": 29.99,
      "image_url": "https://cdn.example.com/widget-blue-l-thumb.jpg"
    }
  ],
  "shipping_city": "Portland",
  "shipping_state": "OR",
  "estimated_delivery": "2026-06-12"
}

That is 100% useful bytes. The mobile app parses it in one pass, renders the screen, and moves on.

Building a BFF in Node.js

A BFF should be a separate service with its own deployment, its own scaling characteristics, and its own API contract per client. Here is a minimal but production-ready structure for a mobile BFF in Node.js with Express.

// mobile-bff/src/server.ts
import express from 'express';
import { orderRoutes } from './routes/orders';
import { profileRoutes } from './routes/profile';
import { errorHandler } from './middleware/error-handler';

const app = express();

// Mobile BFF knows exactly which routes it supports.
// Every route is scoped to what the mobile app renders.
app.use('/api/v2/mobile', orderRoutes);
app.use('/api/v2/mobile', profileRoutes);

app.use(errorHandler);

app.listen(3001, () => {
  console.log('Mobile BFF listening on 3001');
});
// mobile-bff/src/routes/orders.ts
import { Router } from 'express';
import { getOrderById } from '../services/order-service';
import { OrderMobileDTO } from '../dto/order-mobile';

const router = Router();

router.get('/orders/:id', async (req, res, next) => {
  try {
    // The BFF orchestrates calls to multiple backend services
    const [order, tracking] = await Promise.all([
      getOrderById(req.params.id),
      getTrackingInfo(req.params.id),
    ]);

    // The DTO is explicit about what the mobile app gets
    res.json(new OrderMobileDTO(order, tracking));
  } catch (err) {
    next(err);
  }
});

The key design decision is that the DTO (Data Transfer Object) lives in the BFF, not in the backend service. The backend service does not know what the mobile app needs. The BFF does.

// mobile-bff/src/dto/order-mobile.ts

interface RawOrder {
  id: string;
  status: string;
  created_at: string;
  items: RawOrderItem[];
  shipping_address: RawAddress;
  payment: RawPayment;
  estimated_delivery: string;
  tracking_url: string;
  // ... 20 more fields
}

interface RawTracking {
  estimated_arrival: string;
  current_status: string;
  carrier: string;
}

export class OrderMobileDTO {
  id: string;
  status_label: string;
  items: Array<{ name: string; price: number; image_url: string }>;
  shipping_city: string;
  shipping_state: string;
  estimated_delivery: string;

  constructor(order: RawOrder, tracking: RawTracking) {
    this.id = order.id;
    this.status_label = this.mapStatus(order.status);
    this.items = order.items.map((item) => ({
      name: item.name,
      price: item.price,
      image_url: this.buildImageUrl(item.product_id),
    }));
    this.shipping_city = order.shipping_address.city;
    this.shipping_state = order.shipping_address.state;
    this.estimated_delivery = tracking.estimated_arrival;
  }

  private mapStatus(status: string): string {
    const labels: Record<string, string> = {
      pending: 'Preparing',
      confirmed: 'Confirmed',
      shipped: 'On the way',
      delivered: 'Delivered',
      cancelled: 'Cancelled',
    };
    return labels[status] ?? status;
  }

  private buildImageUrl(productId: string): string {
    return `https://cdn.example.com/${productId}-thumb.jpg`;
  }
}

The DTO has three responsibilities:

  1. Select exactly the fields the mobile screen renders. Nothing else.
  2. Transform data into the shape the mobile client expects (status strings, not status enums; image URLs, not product IDs).
  3. Join data from multiple backend calls into a single response so the mobile app makes one HTTP request per screen instead of three.

Where the BFF earns its keep

Not every route needs a BFF. The pattern pays for itself on these three types of endpoints:

Detail screens with high data density. Order details, user profiles, product pages. These screens pull from three to five backend services and render a small subset of each. Without a BFF, the client either makes N round trips or downloads N times more data than it renders.

List screens with computed fields. A timeline feed, a notification list, a home screen. These screens need data from multiple sources aggregated into a single sorted list with computed values (relative timestamps, status labels, unread counts). The BFF computes these server-side and the mobile app just renders the list.

Screens with device-specific logic. A mobile home screen might show a quick-order button that requires pre-computed pricing, inventory, and delivery estimates. A web home screen might show an editorial carousel that pulls from a CMS. The BFF encapsulates the difference so the shared backend does not have to model “home screen behavior.”

The web BFF is different

The mobile BFF above is one BFF. The web app should have a separate one, because the web app needs different things:

// web-bff/src/dto/order-web.ts

interface CMSContent {
  promotion_text: string;
  related_articles: Array<{ title: string; url: string }>;
}

export class OrderWebDTO {
  id: string;
  status: string;
  items: Array<{
    name: string;
    description: string;      // Web renders markdown descriptions
    specs: Record<string, string>;  // Web shows a spec table
    price: number;
    supplier_name: string;    // Web shows supplier attribution
  }>;
  shipping: {
    street: string;           // Web shows full address for editing
    city: string;
    state: string;
    zip: string;
  };
  promotion: string | null;   // Web shows CMS-driven upsells
  related_content: typeof CMSContent.related_articles;

  constructor(order: RawOrder, cms: CMSContent) {
    // Web DTO includes rich fields the mobile BFF intentionally drops
    this.items = order.items.map((item) => ({
      name: item.name,
      description: item.description,
      specs: item.specs,
      price: item.price,
      supplier_name: item.supplier,
    }));
    this.shipping = {
      street: order.shipping_address.street,
      city: order.shipping_address.city,
      state: order.shipping_address.state,
      zip: order.shipping_address.zip,
    };
    this.promotion = cms.promotion_text ?? null;
    this.related_content = cms.related_articles;
  }
}

Same backend. Same order. Two BFFs. Each returns exactly what its client renders. The backend never has to know.

BFF vs API Gateway vs GraphQL

Teams often ask whether a BFF replaces an API gateway or GraphQL. It does neither.

LayerOwned byConcern
API GatewayPlatform/InfraRouting, auth, rate limiting, TLS
BFFClient teamData shaping, aggregation, transforms
Backend APIBackend teamBusiness logic, persistence, domain

A gateway routes traffic to the right BFF. The BFF aggregates data from backend APIs. GraphQL can exist inside a BFF or at the backend API layer, but BFFs do not need GraphQL to do their job. A BFF that returns hand-crafted DTOs over REST is simpler to reason about, easier to profiler, and harder to accidentally N+1.

If you already use GraphQL, a BFF can sit in front of it: the BFF runs a single well-known query against your GraphQL backend and transforms the result into the mobile DTO. You get GraphQL’s flexibility at the backend and the BFF’s payload guarantees at the client. This is common in production.

The rules that keep a BFF from rotting

A BFF is a thin layer. It stays thin only if you enforce these rules:

Rule 1: No business logic. The BFF does not validate inventory, calculate prices, enforce permissions, or decide shipping dates. It fetches data from backend services that do these things and transforms the result. If you find an if statement that checks whether something is allowed, move it to the backend.

Rule 2: One BFF per client type. Do not build one BFF that serves both mobile and web by branching on a header. That is just the monolith with extra steps. Two BFFs that share a library of downstream client helpers (call timeout configs, retry wrappers) are fine. Two BFFs that share route handlers are not.

Rule 3: The BFF owns its schema. The backend team can add fields. The BFF decides whether to pass them through or drop them. The frontend team owns the BFF code and deploys it independently. This is the organizational win: the mobile team can ship a new screen without a backend PR.

Rule 4: Keep the BFF stateless. A BFF should be horizontally scalable without sticky sessions. Session state belongs in a session store that the BFF reads from. The BFF itself is just stateless transformation pipelines.

Rule 5: Cache aggressively. Since the BFF returns pre-shaped data that changes less often than the backend’s raw tables, it is an excellent place to add HTTP caching layers. A mobile BFF returning order DTOs can set Cache-Control: private, max-age=60 and reduce backend load by an order of magnitude for repeat views.

When you do not need a BFF

The BFF pattern adds a deployable service, a CI pipeline, monitoring, and on-call rotation. That is overhead. Skip it when:

  • You have one client type (internal admin dashboard, single-page app, no mobile).
  • Your backend API already returns flat DTOs per client via a GraphQL layer with persisted queries.
  • Your team is three people and your “backend” is a single Node.js service. Add a BFF when the API team is separate from the frontend team or when mobile performance becomes a measurable problem.

The right time to add a BFF is not before you need it. It is the day your mobile team opens a PR that adds a ?client=mobile query parameter to every backend endpoint and the backend team says “this is getting out of hand.”

The takeaway

A general-purpose API is a compromise that makes every client slightly unhappy. The BFF pattern replaces compromise with precision: one backend per client, each returning exactly the data its client renders, in the shape its client expects, without client-side joins or discarded fields.

The implementation is simple: a thin Node.js service per client type with explicit DTOs, orchestrating calls to backend services and transforming results. The organizational structure is where the pattern really matters: the mobile team owns the mobile BFF, the web team owns the web BFF, and the backend team owns the core business logic. Each team ships independently.

Start with one BFF for your worst-performing client. Put it behind your existing API gateway. Measure the payload reduction and the time-to-render. The numbers will tell you whether to build the next one.


A note from Yojji

The BFF pattern is a classic example of architecture that follows organizational boundaries: giving each client team ownership of its own data-shaping layer instead of forcing a single backend API to satisfy contradictory requirements. Getting the deployment boundaries, the DTO structure, and the caching strategy right is the difference between a BFF that accelerates your team and one that becomes just another service to maintain.

Yojji is an international custom software development company founded in 2016, with offices in Europe, the US, and the UK. Their teams specialize in the JavaScript ecosystem (React, Node.js, TypeScript), cloud platforms (AWS, Azure, Google Cloud), and full-cycle product engineering from discovery through DevOps. If your team is weighing API architecture decisions and wants to move faster without accumulating portable complexity, Yojji is worth a conversation.