Adetayo Akinsanya unkletayo.dev

Frontend API Layer Architecture: REST, GraphQL, gRPC-Web, and Backend-for-Frontend (BFF) Pattern

Evaluating protocol wire efficiency, schema type safety, Protobuf serialization, and BFF orchestration layers

Part 7 in Series — Catch up on the previous article: State Management Architecture: Normalized Stores, Signals, XState Automata, and Immutability (Part 6) before diving into this post.

When a logistics enterprise launched its new mobile web portal in Latin America, engineering was blindsided by user churn. Over 68% of users abandoned the application during initial loading.

Debugging revealed that rendering the home dashboard required the client to make 14 sequential REST calls across 6 internal microservices: fetching user permissions, active shipments, driver status, notifications, and weather updates. Over high-latency 3G cellular connections with 250ms round-trip times (RTT), the network waterfalls created an 11-second delay before the UI became interactive. Worse, the JSON payloads totalled 4.2 Megabytes—of which the mobile UI consumed less than 5%.

The team had coupled the frontend UI directly to internal backend microservice boundaries.

Resolving network waterfalls and payload bloat requires designing a robust client API architecture: evaluating REST vs GraphQL vs gRPC-Web wire efficiency, and deploying the Backend-for-Frontend (BFF) pattern.


1. Protocol Comparison: REST vs GraphQL vs gRPC-Web vs tRPC

+--------------------------------------------------------------------------------+
|                        Frontend API Protocol Comparison                         |
+-------------------+--------------------+--------------------+------------------+
| Feature           | REST (JSON)        | GraphQL            | gRPC-Web         |
+-------------------+--------------------+--------------------+------------------+
| Wire Format       | Text / JSON        | Text / JSON        | Binary (Protobuf)|
| Transport         | HTTP/1.1 or HTTP/2 | HTTP/1.1 or HTTP/2 | HTTP/2           |
| Over-fetching     | Common             | Zero (Client Query)| Zero (Schema)    |
| Schema Type Safety| Manual (OpenAPI)   | Built-in Schema    | Strict `.proto`  |
| Browser Native    | Yes                | Yes                | Requires Proxy   |
+-------------------+--------------------+--------------------+------------------+

1.1 REST: Simplicity vs Payload Over-Fetching

REST over HTTP/JSON is universally supported and leverages browser HTTP caching (Cache-Control, ETag). However, REST endpoints suffer from:

  • Over-fetching: Returning a 50KB JSON object containing 40 database fields when the mobile card only needs id and title.
  • Under-fetching (N+1 HTTP Requests): Requiring a call to /api/users/123, followed by NN separate calls to /api/orders?userId=123.

1.2 GraphQL: Client-Driven Declarative Data Fetching

GraphQL eliminates over-fetching by allowing the frontend to specify the exact fields required in a single query:

# Client Query: Fetches ONLY required fields across relational graphs in ONE HTTP call
query GetShipmentDashboard($userId: ID!) {
  user(id: $userId) {
    name
    activeShipments(limit: 5) {
      trackingId
      status
      destinationCity
    }
  }
}

Architectural Gotcha: GraphQL queries execute via POST /graphql, bypassing standard browser and CDN HTTP GET caching infrastructure. Caching must be handled via normalized client caches (Apollo Client, Relay).


1.3 gRPC-Web: High-Performance Binary Protocol Buffers

gRPC-Web enables browsers to invoke gRPC services directly using compact Protocol Buffer (Protobuf) binary serialization over HTTP/2.

// Proto3 Schema Contract (shipment_service.proto)
syntax = "proto3";

package shipment;

message ShipmentRequest {
  string user_id = 1;
}

message ShipmentResponse {
  string tracking_id = 1;
  string status = 2;
  string destination_city = 3;
}

service ShipmentService {
  rpc GetActiveShipments (ShipmentRequest) returns (stream ShipmentResponse);
}

Protobuf serializes field keys into compact 1-byte integer tags (1, 2, 3) instead of repetitive JSON key strings, reducing payload sizes by 60% to 80% compared to JSON.


2. The Backend-for-Frontend (BFF) Architectural Pattern

In microservice architectures, forcing the frontend client to query internal backend microservices directly creates severe performance and security flaws:

BAD: Direct Client-to-Microservices Connection
[ Browser Client ] ----+----> [ User Microservice ]
                       +----> [ Order Microservice ]
                       +----> [ Billing Microservice ]

The Backend-for-Frontend (BFF) Pattern introduces a dedicated node service owned and maintained by the frontend team:

OPTIMAL: BFF Architectural Pattern
[ Mobile / Web Client ] 
          | (Single Optimized TLS Connection / Secure Cookie)
          v
[ BFF Layer (Node.js / Edge Worker) ]
          | (High-Speed Internal Data Center Network / gRPC)
          +----> [ User Service ]
          +----> [ Order Service ]
          +----> [ Billing Service ]

2.1 Complete Production Node.js BFF Implementation

// Production BFF Aggregation Endpoint (Node.js Express / Fastify)
import express from "express";
import { fetchUserProfile, fetchActiveShipments } from "./internal-grpc-clients";

const bffRouter = express.Router();

bffRouter.get("/api/dashboard", async (req, res) => {
  try {
    const userId = req.session?.userId;
    if (!userId) {
      return res.status(401).json({ error: "Unauthorized" });
    }

    // Execute internal microservice gRPC calls IN PARALLEL over high-speed datacenter LAN
    const [userProfile, shipments] = await Promise.all([
      fetchUserProfile(userId),
      fetchActiveShipments(userId)
    ]);

    // Shape payload specifically for client UI requirements (Trimming unused backend fields)
    const clientPayload = {
      user: {
        name: userProfile.displayName,
        avatar: userProfile.avatarUrl
      },
      shipments: shipments.map((s) => ({
        id: s.trackingNumber,
        status: s.currentStatus,
        city: s.destination.city
      }))
    };

    return res.json(clientPayload);
  } catch (error) {
    console.error("BFF Aggregation Failure:", error);
    return res.status(500).json({ error: "Internal Gateway Error" });
  }
});

Summary & Key Takeaways

  • REST Protocol: Excellent HTTP caching compatibility, but vulnerable to payload over-fetching and N+1 request waterfalls.
  • GraphQL Protocol: Prevents over-fetching and provides client-driven data fetching, but requires normalized client-side caches (Apollo/Relay) due to POST endpoint limitations.
  • gRPC-Web Protocol: Delivers maximum wire efficiency using compact Protobuf binary serialization over HTTP/2.
  • BFF Pattern: Decouples client UIs from internal microservice churn, aggregates network calls, and securely manages authentication tokens at the network edge.

References & Further Reading

  1. Fowler, M. (2015). Pattern: Backends For Frontends (BFF). MartinFowler.com.
  2. GraphQL Foundation. GraphQL Specification (October 2021 Edition). GraphQL Spec.
  3. gRPC Authors. gRPC-Web Protocol Specification. gRPC GitHub.

Up Next in Series →

Part 8: Frontend Security Architecture: XSS Mitigations, CSRF Defense, Content Security Policies, and OAuth2 PKCE

Continue to Part 8 →