0:00
/
0:00
Transcript

Building a Scalable GraphQL Gateway with Federation and DataLoader in Node.js

Setting the Stage: Why a Scalable GraphQL Gateway?

Picture this: you’ve got half a dozen microservices—User, Inventory, Orders, Recommendations—each humming along happily in its own little silo. Your frontend team nervously juggles six REST endpoints, transforms payloads, deals with version mismatches, and wrestles with CORS. Enter GraphQL Gateway: a single, unified API endpoint that federates multiple subgraphs into one cohesive schema. It’s like assembling all your LEGO kits into one awesome super-model—no extra bricks required.

On paper, GraphQL Gateway sounds like magic. Practically, however, a naïve implementation easily trips over two pitfalls:

• Schema fragmentation.
• Performance bottlenecks (hello, N+1 queries).

In this post, we’ll walk through building a robust, production-ready GraphQL Gateway in Node.js using Apollo Federation and Facebook’s DataLoader. You’ll learn how to:

  1. Compose a supergraph from federated subgraphs.

  2. Batch and cache requests to eliminate N+1 queries.

  3. Stitch everything together in a maintainable, scalable fashion.

Buckle up—scale is the name of this game!


Enter Federation: Stitching Schemas with Elegance

(Forgo the dad jokes here—just the facts.)

Federation is a specification and set of tools that let you take separate GraphQL services (subgraphs) and merge them into a single supergraph. Rather than one mega-schema maintained by a single team, you distribute ownership. Each service owns its types and fields; they expose those to the gateway, which composes them into one cohesive API.

Key Concepts:

Type Ownership: Service A “owns” User, Service B “owns” Order, etc.
Key Directives: Use @key(fields: "...") to define how entities are identified across services.
Extending Types: One service can extend type User { orders: [Order] } to link entities.

Under the hood, Apollo Federation’s composition engine reads the SDL from each subgraph, validates references, and builds a supergraph schema. The gateway then uses this schema to route incoming queries to the correct subgraphs.


The N+1 Menace and How DataLoader Saves the Day

(Still serious—performance is no joke.)

When you fetch nested fields—say, a user’s orders—GraphQL resolvers may inadvertently issue one request per parent entity. If you ask for 50 users and each resolver naively fetches orders, you end up with 1 (users) + 50 (orders) = 51 calls. At larger scales, this is disastrous.

DataLoader is a simple utility that:

  1. Batches multiple loads into a single request.

  2. Caches within the scope of a GraphQL request.

It works by collecting individual load requests during execution, then dispatching them in a single batch to your underlying service or database.

How it works in three steps:

  1. You instantiate a DataLoader with a batch-loading function (keys) => Promise<results[]>.

  2. Inside resolvers, instead of calling fetchOrders(userId), you call orderLoader.load(userId).

  3. After the resolver phase, DataLoader sends one batched request for all requested user IDs, then distributes the results.

By combining Apollo Federation (for schema composition) with DataLoader (for query performance), you get a scalable GraphQL supergraph gateway.


Putting It All Together: A Node.js Gateway Example

Let’s build a minimal example with:

  • Two subgraphs: users and orders.

  • A federated gateway that composes them and uses DataLoader to batch calls to the orders service.

  1. Subgraph: Users Service (users/index.js)

const { ApolloServer, gql } = require('apollo-server');
const { buildFederatedSchema } = require('@apollo/federation');

const typeDefs = gql`
  type User @key(fields: "id") {
    id: ID!
    name: String
  }

  extend type Query {
    user(id: ID!): User
    users: [User]
  }
`;

const users = [
  { id: "1", name: "Alice" },
  { id: "2", name: "Bob" }
];

const resolvers = {
  Query: {
    user: (_, { id }) => users.find(u => u.id === id),
    users: () => users
  },
  User: {
    __resolveReference(ref) {
      return users.find(u => u.id === ref.id);
    }
  }
};

const server = new ApolloServer({
  schema: buildFederatedSchema([{ typeDefs, resolvers }])
});

server.listen({ port: 4001 }).then(({ url }) => {
  console.log(`🚀 Users subgraph ready at ${url}`);
});
  1. Subgraph: Orders Service (orders/index.js)

const { ApolloServer, gql } = require('apollo-server');
const { buildFederatedSchema } = require('@apollo/federation');

const typeDefs = gql`
  type Order @key(fields: "id") {
    id: ID!
    total: Float
    userId: ID!
  }

  extend type Query {
    ordersByUser(userId: ID!): [Order]
  }

  extend type User @key(fields: "id") {
    id: ID! @external
    orders: [Order]
  }
`;

const orders = [
  { id: "101", total: 199.99, userId: "1" },
  { id: "102", total:  49.50, userId: "1" },
  { id: "103", total: 299.00, userId: "2" }
];

const resolvers = {
  Query: {
    ordersByUser: (_, { userId }) =>
      orders.filter(o => o.userId === userId)
  },
  User: {
    orders(user) {
      return orders.filter(o => o.userId === user.id);
    }
  }
};

const server = new ApolloServer({
  schema: buildFederatedSchema([{ typeDefs, resolvers }])
});

server.listen({ port: 4002 }).then(({ url }) => {
  console.log(`📦 Orders subgraph ready at ${url}`);
});
  1. Gateway with Apollo Federation & DataLoader (gateway/index.js)

const { ApolloServer } = require('apollo-server');
const { ApolloGateway } = require('@apollo/gateway');
const DataLoader = require('dataloader');
const fetch = require('node-fetch');

// Batch function to get orders for multiple user IDs
async function batchOrdersFetch(userIds) {
  // Imagine you have a REST endpoint /ordersByUsers that accepts [ids]
  const resp = await fetch('http://localhost:4002/', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      query: `
        query fetch($ids: [ID!]!) {
          ordersByUsers(userIds: $ids) {
            id
            total
            userId
          }
        }
      `,
      variables: { ids: userIds }
    })
  });
  const { data } = await resp.json();
  // Group results per userId
  return userIds.map(id =>
    data.ordersByUsers.filter(order => order.userId === id)
  );
}

async function startGateway() {
  const gateway = new ApolloGateway({
    serviceList: [
      { name: 'users', url: 'http://localhost:4001' },
      { name: 'orders', url: 'http://localhost:4002' }
    ]
  });

  const server = new ApolloServer({
    gateway,
    subscriptions: false,
    context: () => {
      // Create a new DataLoader per request
      return {
        loaders: {
          ordersByUser: new DataLoader(batchOrdersFetch)
        }
      };
    },
    // Override the default executor to inject our DataLoader for extended User.orders
    buildService({ url }) {
      // Uses default RemoteGraphQLDataSource
      const { RemoteGraphQLDataSource } = require('@apollo/gateway');
      return new RemoteGraphQLDataSource({
        url,
        willSendRequest({ request, context }) {
          // Pass the DataLoader through headers or context if needed
          request.http.headers.set(
            'x-user-id',
            context.userId || ''
          );
        }
      });
    },
    // Custom resolver for federated type extension
    plugins: [
      {
        requestDidResolveContext({ context }) {
          const originalResolveReference =
            context.graphqlExecutionContext.executeOperation
              .operationName;
          // No-op: In real apps, override resolvers via @link directive or schema transforms.
        }
      }
    ]
  });

  server.listen({ port: 4000 }).then(({ url }) => {
    console.log(`🎯 Gateway ready at ${url}`);
  });
}

startGateway();

Now, when a client queries:

query {
  users {
    id
    name
    orders {
      id
      total
    }
  }
}

query { users { id name orders { id total } } }

  • The gateway asks the Users service for users.

  • It collects all orders(user: ID). calls, then uses DataLoader to fetch them in one batch.

  • N+1 problem: vanquished.


References to Libraries and Services

If you want to dive deeper or explore alternative tooling, check out:

• Apollo Federation & Apollo Gateway (https://www.apollographql.com/docs/federation/)
• Facebook DataLoader (https://github.com/graphql/dataloader)
• GraphQL Tools (Schema stitching, transforms) (https://www.graphql-tools.com/)
• Hasura (instant GraphQL + permissions)
• AWS AppSync (managed GraphQL with caching & real-time)
• Prisma (auto-generated GraphQL CRUD)


Closing Stanza

And there you have it—a scalable GraphQL Gateway that gracefully federates subgraphs and smashes the N+1 bottleneck with DataLoader. Whether you’re starting with two services or twenty, this pattern will keep your API performant, maintainable, and delightfully unified.

Until next time, may your resolvers be light and your latency negligible. Swing by tomorrow for more backend wizardry, and don’t forget to hit that follow button—our caffeine-fueled newsletter awaits!

Warmly,
The Backend Developers Team

Discussion about this video