GraphQL

#graphql

#api

MU

Michał Uzdowski

11 min read

Harnessing the power of GraphQL in large-scale applications

Welcome back to Binary Brain, dear code wizards! Today, we’re diving into the enigmatic yet powerful realm of GraphQL. If REST is the tried-and-true classic rock of APIs, think of GraphQL as the edgy new band that’s shaking things up. This article will take you through the basics of GraphQL, its advantages over traditional REST APIs, and how to leverage it in large-scale applications. Ready to rock your API world? Let’s get started!

What is GraphQL?

GraphQL is a query language for your API, and a server-side runtime for executing queries by using a type system that you define for your data. Created by Facebook in 2012 and open-sourced in 2015, GraphQL provides a more efficient, powerful, and flexible alternative to REST, which you already know from Building a RESTful API with Node.js and Express: Server Yoga for Your Data . It allows clients to request exactly what they need, nothing more and nothing less.

Why Choose GraphQL Over REST?

1. Flexible Data Retrieval

  • With REST, you often end up over-fetching or under-fetching data. GraphQL lets clients request exactly the data they need.

2. Single Endpoint

  • REST APIs require multiple endpoints for different resources, whereas GraphQL APIs typically have a single endpoint that can handle multiple queries and mutations.

3. Strongly Typed Schema

  • GraphQL APIs are defined by a schema that specifies the capabilities of the API and the types of data that can be queried. This helps in catching errors early in the development cycle.

4. Real-time Data with Subscriptions

  • GraphQL supports subscriptions to get real-time updates, which is a game-changer for applications requiring live data.

Setting Up GraphQL in a Node.js Application

To illustrate the power of GraphQL, let’s set up a simple GraphQL server using Node.js, Express, and Apollo Server. Don’t worry, it’s easier than summoning a Patronus!

Step 1: Initialize Your Project

First, create a new directory and initialize a Node.js project.

mkdir graphql-demo
cd graphql-demo
npm init -y

Step 2: Install Dependencies

Next, install the necessary dependencies.

npm install express apollo-server-express graphql

Step 3: Set Up the Server

Create an index.js file and set up your Express server with Apollo Server.

const express = require("express");
const { ApolloServer, gql } = require("apollo-server-express");

// Sample data
const books = [
  { title: "Harry Potter and the Sorcerer's Stone", author: "J.K. Rowling" },
  { title: "Jurassic Park", author: "Michael Crichton" },
];

// Type definitions
const typeDefs = gql`
  type Book {
    title: String
    author: String
  }

  type Query {
    books: [Book]
  }
`;

// Resolvers
const resolvers = {
  Query: {
    books: () => books,
  },
};

async function startServer() {
  const app = express();
  const server = new ApolloServer({ typeDefs, resolvers });
  await server.start();
  server.applyMiddleware({ app });

  app.listen({ port: 4000 }, () =>
    console.log(`Server ready at http://localhost:4000${server.graphqlPath}`)
  );
}

startServer();

Step 4: Test Your API

With your server running, head over to http://localhost:4000/graphql and run the following query:

{
  books {
    title
    author
  }
}

Voila! You’ve set up a GraphQL server that fetches data from a single endpoint. But wait, there’s more!

Scaling Up: GraphQL in Large-Scale Applications

For large-scale applications, GraphQL truly shines. Here’s how you can harness its power effectively.

1. Modularizing Your Schema

When building a GraphQL server for large-scale applications, keeping your code organized and modular is crucial for maintainability and scalability. Here’s how to break down your GraphQL schema management into digestible parts.

Schema Definition (schema.js)

The schema defines the structure of the data that can be queried by the client and includes types, queries, mutations, and subscriptions.

const { gql } = require("apollo-server-express");

const typeDefs = gql`
  type Book {
    title: String
    author: String
  }

  type Author {
    name: String
    books: [Book]
  }

  type Query {
    books: [Book]
    authors: [Author]
  }
`;

module.exports = typeDefs;
  • Types: Define your data structures. Here, Book and Author are types with fields that GraphQL can query.
  • Query: Defines the read-only fetch operations for your API. You can fetch books or authors.
  • Mutation: Defines write operations to modify data on the server. For example, addBook adds a new book to the collection.
  • Subscription: Allows clients to subscribe to real-time updates. bookAdded notifies subscribers whenever a new book is added.

Resolvers (resolvers.js)

Resolvers provide the instructions for turning a GraphQL operation (a query, mutation, or subscription) into data. They define how to fetch or modify the data described in the schema.

const books = [
  { title: "Harry Potter and the Sorcerer's Stone", author: "J.K. Rowling" },
  { title: "Jurassic Park", author: "Michael Crichton" },
];

const authors = [
  { name: "J.K. Rowling", books: ["Harry Potter and the Sorcerer's Stone"] },
  { name: "Michael Crichton", books: ["Jurassic Park"] },
];

const resolvers = {
  Query: {
    books: () => books,
    authors: () => authors,
  },
  Author: {
    books: (parent) => books.filter((book) => book.author === parent.name),
  },
};

module.exports = resolvers;
  • Data: Sample data arrays simulate database data.
  • Query Resolvers: Functions like books() and authors() that return the necessary data based on the query.
  • Mutation Resolvers: Includes addBook which updates the data store and triggers a subscription event.
  • Subscription Resolvers: Manages real-time data subscriptions.

Server Setup (index.js)

This script initializes and configures your GraphQL server, linking it with your Express app, and setting it up to handle API requests.

const express = require("express");
const { ApolloServer } = require("apollo-server-express");
const typeDefs = require("./schema");
const resolvers = require("./resolvers");

async function startServer() {
  const app = express();
  const server = new ApolloServer({ typeDefs, resolvers });
  await server.start();
  server.applyMiddleware({ app });

  app.listen({ port: 4000 }, () =>
    console.log(`Server ready at http://localhost:4000${server.graphqlPath}`)
  );
}

startServer();
  • Apollo Server Initialization: Creates a new ApolloServer instance, passing the schema definitions and resolvers.
  • Middleware: Integrates Apollo Server with Express, allowing GraphQL API handling via HTTP.
  • Server Activation: Starts the server and listens on port 4000, ready to handle incoming API requests.

By organizing your GraphQL setup into these modular components, you ensure that each part of your API is maintainable and easily understood, making your application robust and scalable.

2. Using Data Loaders

Data Loaders are an essential tool in GraphQL for minimizing unnecessary data fetching and solving the N+1 problem commonly faced in API requests. By batching and caching database requests, Data Loaders can significantly enhance the performance of your GraphQL server, especially when dealing with complex and deeply nested queries.

Installing DataLoader
Before integrating Data Loaders, you need to install the DataLoader library, which is developed by Facebook. It’s designed to handle batching and caching at a granular level.

npm install dataloader

Setting Up Data Loaders (dataloaders.js)
he setup involves creating instances of DataLoader, each tailored to a specific type of data or database resource.

const DataLoader = require("dataloader");

// Simulated book data
const books = [
  { id: 1, title: "Harry Potter and the Sorcerer's Stone", authorId: 1 },
  { id: 2, title: "Jurassic Park", authorId: 2 },
];

// DataLoader for fetching books by author
const booksLoader = new DataLoader(async (authorIds) => {
  // A function that returns a promise which resolves to an array of books for each author
  return authorIds.map((authorId) =>
    books.filter((book) => book.authorId === authorId)
  );
});

module.exports = {
  booksLoader,
};

DataLoader Function: The function passed to DataLoader should fetch an array of values. In this example, it filters books based on authorId. The function is designed to batch requests, so it takes an array of keys (authorIds) and returns an array of results.
Usage: This DataLoader is used to optimize queries that request books belonging to specific authors, preventing repeated individual fetch operations for each author in a single query.

Integrating Data Loaders in Resolvers
Integrating Data Loaders in the resolvers allows your GraphQL server to efficiently fetch data required to resolve fields, particularly when resolving fields that require repetitive access to similar data.

const { booksLoader } = require("./dataloaders");

const resolvers = {
  Query: {
    books: () => books,
  },
  Author: {
    books: (author) => {
      return booksLoader.load(author.id);
    },
  },
};

module.exports = resolvers;

Author Resolver: When resolving the books field in the Author type, the resolver uses booksLoader to fetch books. This DataLoader instance batches multiple book fetches into a single request if multiple authors are queried at once.
Load Method: The load() function of DataLoader is used to queue a fetch operation, which DataLoader may batch or cache depending on whether the data was previously requested.

Benefits of Using Data Loaders
Efficiency: Data Loaders reduce the number of database hits by batching multiple queries into a single query and caching duplicate queries to minimize redundant data fetching.
Performance: By reducing the amount of data fetched and the number of database queries, Data Loaders can significantly improve the performance of GraphQL servers.
Scalability: Data Loaders help GraphQL APIs scale by handling data fetching more intelligently, especially important in large-scale applications with high query loads.

Using Data Loaders is a best practice in GraphQL development for managing data fetching more efficiently. It not only optimizes server performance but also provides a smoother data fetching experience for clients by reducing wait times and resource consumption.

3. Real-time Data with Subscriptions

In this step, we’ll delve into enabling real-time data updates in your GraphQL application using Subscriptions. This feature allows clients to maintain a steady connection to the server, receiving updates as soon as data changes, making it ideal for applications that require instant data sync such as chat apps or live statistic updates.

Installing Necessary Packages for Subscriptions

Before implementing subscriptions, you need to set up your environment to handle real-time WebSocket connections, which are different from typical HTTP requests.

npm install subscriptions-transport-ws graphql-subscriptions

These packages provide the necessary tools to create a WebSocket server and bind it with GraphQL subscriptions.

Setting Up Subscriptions in Your Schema (schema.js)

Define the subscriptions in your GraphQL schema to specify the events that clients can subscribe to.

Schema Modifications:

const { gql } = require("apollo-server-express");

const typeDefs = gql`
  type Book {
    id: ID!
    title: String
    author: String
  }

  type Query {
    books: [Book]
  }

  type Mutation {
    addBook(title: String, author: String): Book
  }

  type Subscription {
    bookAdded: Book
  }
`;

module.exports = typeDefs;
  • Subscription Type: The Subscription type is similar to the Query and Mutation types but is used for defining events that clients can subscribe to. In this example, bookAdded allows clients to receive updates when a new book is added.

Integrating Subscriptions in the Server Setup (index.js)

To set up a WebSocket listener on your server, allowing it to handle subscriptions alongside regular queries and mutations.

const express = require("express");
const { ApolloServer } = require("apollo-server-express");
const { createServer } = require("http");
const { SubscriptionServer } = require("subscriptions-transport-ws");
const { execute, subscribe } = require("graphql");
const { makeExecutableSchema } = require("@graphql-tools/schema");
const typeDefs = require("./schema");
const resolvers = require("./resolvers");

async function startServer() {
  const app = express();
  const httpServer = createServer(app);

  const schema = makeExecutableSchema({ typeDefs, resolvers });

  const server = new ApolloServer({
    schema,
    plugins: [
      {
        async serverWillStart() {
          return {
            async drainServer() {
              subscriptionServer.close();
            },
          };
        },
      },
    ],
  });

  await server.start();
  server.applyMiddleware({ app });

  const subscriptionServer = SubscriptionServer.create(
    { schema, execute, subscribe },
    { server: httpServer, path: server.graphqlPath }
  );

  httpServer.listen({ port: 4000 }, () =>
    console.log(`Server ready at http://localhost:4000${server.graphqlPath}`)
  );
}

startServer();
  • HTTP and WebSocket Server: This setup initializes both an HTTP server (for handling standard GraphQL queries and mutations) and a WebSocket server (for handling GraphQL subscriptions).
  • Subscription Server Creation: The SubscriptionServer.create function binds the WebSocket server to the GraphQL schema, enabling it to listen for subscription events specified in the schema.

Handling Subscriptions in Resolvers (resolvers.js)

Define how the data for each subscription is provided in response to events.

const { PubSub } = require("graphql-subscriptions");
const pubsub = new PubSub();
const BOOK_ADDED = "BOOK_ADDED";

const books = [
  {
    id: 1,
    title: "Harry Potter and the Sorcerer's Stone",
    author: "J.K. Rowling",
  },
  { id: 2, title: "Jurassic Park", author: "Michael Crichton" },
];

const resolvers = {
  Query: {
    books: () => books,
  },
  Mutation: {
    addBook: (_, { title, author }) => {
      const newBook = { id: books.length + 1, title, author };
      books.push(newBook);
      pubsub.publish(BOOK_ADDED, { bookAdded: newBook });
      return newBook;
    },
  },
  Subscription: {
    bookAdded: {
      subscribe: () => pubsub.asyncIterator([BOOK_ADDED]),
    },
  },
};

module.exports = resolvers;
  • Publishing Events: In the addBook mutation, after a new book is added, an event is published using pubsub.publish, which triggers the subscription.
  • Subscription Resolver: The subscribe field in the subscription resolver uses pubsub.asyncIterator to return an iterator that yields values whenever the specified event (BOOK_ADDED) occurs.

By implementing these steps, you enable real-time data functionality in your GraphQL server, allowing clients to subscribe to updates and receive new data instantly as it becomes available. This adds a dynamic component to your applications, enhancing user interaction and responsiveness.

Conclusion

Congratulations! You’ve just navigated through setting up a robust GraphQL server capable of handling large-scale applications with ease. We’ve covered everything from basic API requests to real-time updates with subscriptions. By using GraphQL, you’ve streamlined your data fetching processes, made your APIs more flexible, and hopefully had a bit of fun along the way.

As we wrap up this sonic journey through the realms of GraphQL, remember that the power of GraphQL isn’t just in its ability to fetch data efficiently; it’s also in how it can make your applications more interactive and responsive to user actions in real-time.

Stay tuned to Binary Brain for more adventures in coding where we make complex technologies simple and fun. Until next time, keep coding, keep smiling, and let GraphQL rock your API world!