Skip to main content

GraphQL Essentials: From Basics to Federation

Greetings!

Over the past two years, I have been engaging with GraphQL intermittently, not always as part of day-to-day development. However, now feels like the right time to consolidate key ideas into a comprehensive article.

What is GraphQL?

GraphQL is a query language for APIs and a runtime for executing those queries based on your data. It provides a more flexible and efficient alternative to traditional REST APIs by allowing clients to request only the data they need, reducing over-fetching and under-fetching.
Unlike REST, where endpoints are tightly coupled to specific data structures, GraphQL operates on a single endpoint and allows dynamic queries.

A GraphQL server

Key Features of GraphQL

  1. Single Endpoint: All requests are sent to a single endpoint.
  2. Strongly Typed: A schema defines the data structure and enforces types.
  3. Declarative Queries: Clients specify the shape of the response they need.
  4. Real-time Support: Through subscriptions, clients can receive real-time updates.
  5. Introspection: Enables clients to query the schema for metadata.

Architecture and Use Cases

GraphQL can be integrated into various architectural patterns, including:
  1. Backend for Frontend (BFF):
    • Ideal for mobile or web applications requiring tailored data responses.
    • Simplifies aggregating data from multiple sources.
  2. Monolith Migration:
    • Gradual replacement of REST endpoints in a monolithic application.
    • Reduces disruption while adopting GraphQL incrementally.
  3. Microservices:
    • Federated GraphQL architecture enables seamless integration of microservices.
    • A gateway aggregates and resolves queries across multiple subgraphs.

Drawbacks

While GraphQL is powerful, it’s not without challenges:
  • Steep learning curve: Unlike REST, GraphQL requires a strong understanding of concepts like schemas, resolvers, and federation.
  • Complexity in high-level composition: Queries across subgraphs with dependencies are not automatically resolved. Manual invocations are often necessary.
  • Performance concerns: Misusing resolvers or failing to batch queries can lead to performance bottlenecks.
  • Complex schema: It is hard to track when multiple subgraphs contributing to the same type.
  • Complex resolvers: It is hard to understand data resolving when the chain is deep. 

What Does GraphQL Look Like?

Imagine a scenario where we need to fetch books by their id, title, and price. Here’s how a GraphQL client query and its corresponding server schema might look:
Client query:

query GetBooks {

  books {

    id

    title

    price

  }

}

Server schema:

type Query {

  books: [Book]

}


type Book {

  id: ID

  title: String

  price: Float

  genre: [Genre]

}

GraphQL Concepts

Understanding these core concepts is crucial when working with GraphQL:
  • Schema: Defines the types, queries, and mutations that the API supports.
  • Types: GraphQL objects define the shape of the API’s data. Common types include:
    • Root types
      • Query - Read-only operation to fetch data.
      • Mutation - Used to modify server-side data.
      • Subscription - Real-time updates
    • Built-in scalars - ID, String, Int, Float, Boolean
    • Custom types - Book, Author - type Book {}
    • Input types - Defines reusable input objects for arguments in queries/mutations. input BookInput { title: String! authorId: ID! }
    • Enums - Defines a fixed set of values for a field. - enum Genre {}
    • Custom scalars - Define custom types like Date or Email. scalar Date
  • Resolver: A function responsible for fetching the data for a specific field in the schema.
    • Resolver chain - the sequential execution of resolvers to fetch nested or related data for a query.
  • Directives: Annotations for modifying query behavior. Examples: 
    • @deprecated(reason: "Use 'newField' instead.")
    • @include and @skip for conditional fields.
    • @key and @external for federation.
    • Custom directives for additional logic.
  • Arguments: Used to pass data to fields, enabling dynamic responses.
  • Parent: The result of the previous resolver in the chain. Useful for nested data.
  • Context: Provides a shared object across all resolvers during a query.
  • DataLoader: Solves the N+1 query problem by batching and caching requests.
  • Extend: Adds fields to existing types across subgraphs in federated GraphQL. extend type Author
  • Federation: A way to compose multiple GraphQL APIs into a single graph. Key concepts include:
    • @key - Declares a primary key field in the schema to uniquely identify types.
    • @external - Marks a field as resolved by another subgraph.
    • @requires - Specifies dependent fields needed to resolve another field.
    • @provides - Indicates that a resolver provides additional data for another subgraph.
    • __resolveReference - Resolver method to fetch type instances by key from another subgraph.
    • __entities - Federated query for resolving multiple references across subgraphs in a single query. Automatically generated for federated operations.
    • __typename - the name of the object type being resolved in a GraphQL query.
    • @shareable Allows a field to be shared across multiple subgraphs.
    • @override Specifies which subgraph should resolve a field when it’s available in multiple subgraphs.
    • _service - query that provides the subgraph's SDL. _service { sdl }
  • Fragments: Reusable parts of queries to avoid redundancy. fragment BookDetails on Book { title author { name } }
  • Introspection - Queries about the schema itself. { __schema { types { name } } }

How Does the Gateway Work?

In a federated setup, the gateway acts as the orchestrator.
  • Aggregates schemas from multiple subgraphs into a single schema.
  • Resolves queries by delegating them to the appropriate subgraph.
  • Uses _entities and _service queries to fetch federated data efficiently.

query Book_entities($representations: [_Any!]!) {

  _entities(representations: $representations) {

    ... on Book {

      id

      title

      authorId

    }

  }

}


// pass the parameters

{

  "representations": [

    { "__typename": "Book", "id": "1" }

  ]

}

How Does the Gateway Resolve Subgraph Dependencies?

The gateway uses @key and @external directives to link entities across subgraphs. For example, if the Books subgraph has an author and the Authors subgraph provides details about authors, the gateway combines them to resolve a complete query response.

  1. Request books, book has author reference.
  2. Gateway fetches books from book-subgraph.
  3. Books subgraph returns books and author reference.
  4. Gateway invoke author-subgraph's __entities query using author references.
  5. __entities query invoke __resolveReference in a loop.
  6. __resolveReference resolve author entities, uses dataloader for peroformance.
  7. Gateway returns books with author entities.

Standalone Server

As mentioned earlier, we can use GraphQL as a monolithic gateway. In this setup, there is no need to install a dedicated gateway or subgraph dependencies.

npm install @apollo/server graphql graphql-tag dataloader express body-parser cors

The server would look as follows; however, we will not explore this example further as all concepts will be covered using a federated graph.

const { ApolloServer } = require('@apollo/server'); const { expressMiddleware } = require('@apollo/server/express4'); const express = require('express'); const bodyParser = require('body-parser'); const cors = require('cors'); // import schema and resolvers const typeDefs = require('./schema'); const resolvers = require('./resolvers'); const authorLoader = require('./data-loaders'); // Define context function const context = async ({ _req, _res }) => { return { loaders: { authorLoader }, }; }; // Create an instance of ApolloServer const server = new ApolloServer({ typeDefs, resolvers, }); // Start the server const startServer = async () => { await server.start(); const app = express(); app.use(cors()); app.use(bodyParser.json()); app.use('/graphql', expressMiddleware(server, { context // pass the context })); const PORT = 4000; app.listen(PORT, () => { console.log(`🚀 Server ready at http://localhost:${PORT}/graphql`); }); }; startServer();

Let's move onto the federated graph.

Federated Server

In the federated server, we have separated microservices and a gateway to combine them together. We are creating two microservices for books and authors: the books subgraph, the authors subgraph, and the federated gateway.

book-shop

---book-shop-gateway

---books-subgraph

---authors-subgraph

Install dependencies

Subgraphs: Navigate to the respective folders (books-subgraph, authors-subgraph) and install the dependencies.

npm install @apollo/server graphql @apollo/subgraph express graphql-tag dataloader

Gateway: Navigate to the gateway (book-shop-gateway) and install the dependencices.

npm install @apollo/server graphql @apollo/gateway express

Resolvers

A resolver is the glue between the schema and the data. There should be an identical resolver for each query. Resolvers has following definition.

  Query: {

    searchBook: (parent, args, context, info) => {

      // search books from a database based on args/parent

      return books;

    }

  }

Author subgraph

const { ApolloServer } = require('@apollo/server');

const { buildSubgraphSchema } = require('@apollo/subgraph');

const { startStandaloneServer } = require('@apollo/server/standalone');

const { gql } = require('graphql-tag');

const DataLoader = require('dataloader');


// author data

const authors = [

    { id: "1", name: "Author 1" },

    { id: "2", name: "Author 2" },

    { id: "3", name: "Author 3" },

];


// author dataloader

const authorLoader = new DataLoader(async (authorIds) => {

    const authorsMap = new Map(authors.map((author) => [author.id, author]));

    return authorIds.map((id) => authorsMap.get(id) || null);

}, {

    cache: false,

});


// schema

const typeDefs = gql`

    type Query {

        authors: [Author]

    }


    type Author @key(fields: "id") {

        id: ID!

        name: String!

    }

`;


// resovlers

const resolvers = {

    Query: {

        authors: () => authors,

    },

    Author: {

        __resolveReference(author, { authorLoader }) {

            return authorLoader.load(author.id);

        },

    },

};


// start the server

const server = new ApolloServer({

    schema: buildSubgraphSchema({ typeDefs, resolvers }),

});


startStandaloneServer(server, {

    listen: { port: 4003 },

    context: async ({ req }) => {

        return {

            authorLoader

        };

    },

}).then(({ url }) => {

    console.log(`Authors subgraph is running at ${url}`);

});

We can validate author reference resolving by navigating to localhost:4003.

query _entities($representations: [_Any!]!) {

  _entities(representations: $representations) {

    ... on Author {

      id

      name

    }

  }

}


// variables

{

  "representations": [

    { "__typename": "Author", "id": "1" },

    { "__typename": "Author", "id": "2" }

  ]

}

Book subgraph

const { ApolloServer } = require('@apollo/server'); const { buildSubgraphSchema } = require('@apollo/subgraph'); const { startStandaloneServer } = require('@apollo/server/standalone'); const { gql } = require('graphql-tag'); const books = [ { "id": "1", "title": "Book 1", "price": 15.99, "genre": ["Adventure"], "authorId": "1" }, { "id": "2", "title": "Book 2", "price": 12.49, "genre": ["Mystery"], "authorId": "2" }, { "id": "3", "title": "Book 3", "price": 18.75, "genre": ["Biography"], "authorId": "3" }, { "id": "4", "title": "Book 4", "price": 14.99, "genre": ["Adventure", "Mystery"], "authorId": "1" }, { "id": "5", "title": "Book 5", "price": 20.00, "genre": ["Mystery", "Biography"], "authorId": "2" }, ]; const typeDefs = gql` type Query { books: [Book] serachBooks(genre: Genre!): [Book] } type Mutation { addBook(input: AddBookInput!): Book } type Book @key(fields: "id") { id: ID! title: String! price: Float genre: [Genre] author: Author } extend type Author @key(fields: "id") { id: ID! @external } input AddBookInput { title: String! price: Float genre: [Genre]! authorId: ID! } enum Genre { Adventure Biography Mystery } `; const resolvers = { Query: { books: () => books, serachBooks: (_parent, { genre }) => { return books.filter(book => book.genre.includes(genre)); } }, Mutation: { addBook: (_parent, { input }) => { input.id = books.length + 1; books.push(input); return input; } }, Book: { __resolveReference(book) { return books.find(b => b.id === book.id); }, author(book) { return { __typename: "Author", id: book.authorId } } }, }; const server = new ApolloServer({ schema: buildSubgraphSchema({ typeDefs, resolvers }), }); startStandaloneServer(server, { listen: { port: 4001 }, }).then(({ url }) => { console.log(`Books subgraph running at ${url}`); });

Gateway

const { ApolloGateway } = require('@apollo/gateway');

const { ApolloServer } = require('@apollo/server');

const { startStandaloneServer } = require('@apollo/server/standalone');


const gateway = new ApolloGateway({

  serviceList: [

    { name: "books", url: "http://localhost:4001" },

    { name: "authors", url: "http://localhost:4003" },

  ],

});


const server = new ApolloServer({

  gateway,

  introspection: true,

});


startStandaloneServer(server, {

  listen: { port: 4000 },

}).then(({ url }) => {

  console.log(`Gateway running at ${url}`);

});

Client query

Navigate to localhost:4000 where apollo sandox is available. You can query books, authors as below.

query Books($genre: Genre!) {

  serachBooks(genre: $genre) {

    ... BookDetails

  }

}


mutation Mutation($input: AddBookInput!) {

    addBook(input: $input) {

    ... BookDetails

  }

}


fragment BookDetails on Book {

    id

    title

    price

    genre

    author {

      id

      name

    }

}


// variables

{ "genre": "Adventure", "input": { "title": "Book 6", "price": 28.75, "genre": ["Biography"], "authorId": "3" } }

Conclusion

It has been a great journey with GraphQL so far, with more positives than negatives. However, it's important to understand the core concepts thoroughly before diving in.

Happy learning 😊

Comments