Backend Development 14 min read

Practical Experience of Introducing GraphQL as a BFF Layer in Backend Development

This article shares a backend team's practical experience of adopting GraphQL with Express and Apollo Server as a BFF layer, covering motivations, implementation steps, schema merging, proxy configuration, HTTP caching strategies, DataLoader optimization, code generation, and integration testing to improve performance and developer efficiency.

HomeTech
HomeTech
HomeTech
Practical Experience of Introducing GraphQL as a BFF Layer in Backend Development

1. Practice Background As business grew, the front‑end needed to call many RESTful APIs, causing multiple network requests, wasted TCP overhead, unpredictable response order, and exposure of unnecessary fields. To reduce request count and payload size, the team introduced GraphQL as a BFF layer.

2. Practice Process

2.1 Express + Apollo GraphQL The project uses Express together with Apollo Server.

npm install apollo-server-express apollo-server-core express graphql
import { ApolloServer } from "apollo-server-express";
import { ApolloServerPluginDrainHttpServer } from "apollo-server-core";
import express from "express";
import http from "http";

async function startApolloServer(typeDefs, resolvers) {
  const app = express();
  const httpServer = http.createServer(app);
  const server = new ApolloServer({
    typeDefs,
    resolvers,
    cache: "bounded",
    plugins: [ApolloServerPluginDrainHttpServer({ http: httpServer })],
  });
  await server.start();
  server.applyMiddleware({ app, path: "/graphql" });
  await new Promise(resolve => httpServer.listen({ port: 4000 }, resolve));
  console.log(`Server ready at http://localhost:4000/`);
}

Apollo Server can also be used directly or via framework‑specific integration packages.

3. Automatic Schema and Resolver Merging To keep type definitions and resolvers modular, the team uses @graphql-tools/load-files and @graphql-tools/merge to load all .gql / .graphql files under src/schema/graphqls and all resolver files under src/schema/resolvers .

import { loadFilesSync } from "@graphql-tools/load-files";
import { mergeTypeDefs, mergeResolvers } from "@graphql-tools/merge";
import { ApolloServer } from "apollo-server-express";

const typeDefsArray = loadFilesSync("./schema/graphqls", { recursive: true, extensions: ["gql", "graphql"] });
const resolversArray = loadFilesSync("./schema/resolvers", { recursive: true, extensions: ["ts", "js"], exportNames: ["resolvers"] });

const typeDefs = mergeTypeDefs(typeDefsArray);
const resolvers = mergeResolvers(resolversArray);

const server = new ApolloServer({ typeDefs, resolvers });

Alternatively, makeExecutableSchema from @graphql-tools/schema can be used.

4. Proxy Configuration For local debugging or accessing restricted resources, the team configures a Node.js proxy using the global-agent library, which respects HTTP_PROXY and NO_AGENT environment variables.

npm install global-agent
import { ApolloServer, gql } from "apollo-server";
import { bootstrap } from "global-agent";

bootstrap();
const server = new ApolloServer({ typeDefs, resolvers, cache: "bounded" });

Run the project with GLOBAL_AGENT_HTTP_PROXY=http://localhost:3210 to enable the proxy.

5. HTTP Cache The article explains freshness (Cache‑Control, Expires) and validation (Last‑Modified, ETag) headers, and why GraphQL’s default POST method bypasses them. It suggests using persisted queries to enable GET requests without exceeding URL length limits, allowing standard HTTP caching.

6. Backend Requests

6.1 RESTDataSource Automatically caches identical GET requests, reducing duplicate calls.

const source = new RESTDataSource();
const user_1 = await source.get("/user", { id: 1 });
const user_2 = await source.get("/user", { id: 1 }); // same Promise as user_1

6.2 DataLoader Batches and caches requests, e.g., loading series information for many goods in a single call.

const loader = new DataLoader(seriesIds => {
  return this.get("/series", { seriesIds: seriesIds.join(",") });
});

Key points: batch function must preserve order, and custom cache keys may be needed when additional parameters are present.

7. Other Tips

7.1 Code Generation – @graphql-codegen/cli can generate TypeScript types from the schema.

7.2 Integration Testing – Apollo Server provides executeOperation for e2e tests and also supports raw HTTP requests.

import { ApolloServer, gql } from "apollo-server";

describe("sample", () => {
  test("e2e demo", async () => {
    const server = new ApolloServer({
      typeDefs: gql`
        type Query { hello(name: String): String! }
      `,
      resolvers: { Query: { hello: (_, { name }) => `Hello ${name}` } },
    });
    const response = await server.executeOperation({
      query: `query Say($name: String) { hello(name: $name) }`,
      variables: { name: "world" },
    });
    expect(response.errors).toBeUndefined();
    expect(response.data?.hello).toBe("Hello world");
  });
});

8. Summary The team documents why GraphQL was adopted as a BFF, the architectural decisions made (schema splitting, proxy, caching, testing), and practical tips to help other developers facing similar challenges.

Backend DevelopmentNode.jsExpressGraphQLHTTP cacheDataLoaderApollo Server
HomeTech
Written by

HomeTech

HomeTech tech sharing

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.