Skip to content

Ship Less JavaScript with GraphQL Resolvers

In the past year or two, I’ve noticed a lot of folks online putting an emphasis on the size and amount of code we are shipping as JavaScript bundles in our web applications. And this makes sense. A lean JavaScript bundle is advantageous for many reasons like accessibility, mobile device/network experience, SEO, and overall speed of your web pages and applications.

There are many ways that we can reduce the size of the JavaScript bundles that we ship without sacrificing using the tools and frameworks that we’re familiar with to build high-quality websites and applications. In this article, I want to highlight some ways that we can use GraphQL and tools like Apollo Server to reduce the amount of JavaScript that gets shipped in our bundles.

This article assumes that you have some experience and familiarity with GraphQL. If you need an introduction to these concepts, check out this fantastic post from our blog: Migrating from REST to GraphQL. Otherwise, let's dig right into some different strategies and examples we can use to speed up our websites! For the sake of our examples going forward, we are going to pretend that we are working on an IMDB-like website for movies.

Defining schemas to get the most out of your API

One of the key benefits of GraphQL is the ability to define a schema that specifies the shape of the data that is available through the API. Defining a well-crafted GraphQL schema provides the ability to ship only the data that is actually needed by the client. In a REST API, it is common to fetch entire entities from a database, even if only a small portion of that data is actually needed by the client. With GraphQL, you have granular control over the data that is returned, allowing you to ship only the data that is required.

Here’s a simple example schema to describe our movie API. It includes a query to fetch a list of films, and another to fetch a list of actors. as well as the shape of those entities, that we can request some or all of the fields from, using our GraphQL API.

type Query {
  films(limit: Int): [Film]
  people(limit: Int): [Person]
}

type Film {
  title: String
  releaseDate: String
  actors: [Person]
}

type Person {
  name: String
  films: [Film]
}

Sub-query resolvers

One particularly powerful feature of GraphQL is the ability to define sub-query resolvers. This allows you to specify relationships between different types of data in your schema, allowing the client to request related data in a single query. This can greatly reduce the number of API calls needed to fetch all of the data required by the client, ultimately leading to the shipment of less JavaScript.

Based on our schema above, we can easily write a query to request a list of actors that played in a particular film.

query {
  films(limit: 5) {
    title
    actors {
      name
    }
  }
}

Our Apollo Server sub-query resolver might look something like this:

const resolvers = {
  Film: {
    actors: (film, _, { dataSources }) => dataSources.peopleAPI.getActorsByFilm(film.id),
  },
}

A quality GraphQL schema will provide more granular control to its clients, and will help prevent over-fetching of data. This is an easy way to reduce bytes sent across the wire, which will lead to improved performance in our applications.

Moving client logic ito resolvers

Resolvers are just functions that we write to fetch and aggregate the data that we defined in our schema. A simple concept that provides a powerful layer between our data sources and the contract we have defined with our GraphQL API. They are called by the GraphQL engine and are passed the arguments defined in our queries and a reference to the calling object.

Let's update our films query to accept some additional arguments:

type Query {
  films(limit: Int, title: String, year: Int, offset: Int): [Film]
}

Using the arguments defined on our films query, our client now has more control to only fetch the data they need. The limit and offset arguments that allow them to paginate the results so a single page will need to fetch and render a smaller amount of items at a time.

Here’s what our resolver might look like using these new arguments to fetch a subset of the data that is available:

const resolvers = {
  Query: {
    films: async (_, { limit, title, year, offset }, { dataSources }) => {
        return dataSources.filmAPI.getFilms({ limit, title, year, offset });
    },
  },
}

Magic Fields

Another way to use resolvers is to create "magic fields" that are single-purpose data transformations. For example, we could create a field on the Person type called latestFilm that returns the actor’s most recent film, rather than returning a list of all of the user's posts and having the client filter through them. This can save on both the amount of data shipped, and the amount of processing required by the client.

type Person {
  name: String
  films: [Film]
	latestFilm: Film
}

Transformational Resolvers

Transformational resolvers allow you to perform transformations on the data before it is returned to the client, reducing the need for complex data manipulation logic on the client side.

For example, in your film type, you can use an enum to allow the client to request the release date in different formats, such as SHORT or LONG.

enum DateFormat {
  SHORT
  LONG
}

Then, you can use this format in your film query as a variable and pass it to the releaseDate field:

query Film($id: ID!, $format: DateFormat) {
  film(id: $id) {
    id
    releaseData(format: $format)
  }
}

On the server side, you can define a resolver for the releaseData field that takes the format variable as an argument, and performs the appropriate transformation before returning the data to the client.

const resolvers = {
  Film: {
    releaseData: (film, { format }, { dataSources }) => {
        if (format === DateFormat.SHORT) {
            // Perform short format transformation
            return shortFormatDate(film.releaseDate);
        } else if (format === DateFormat.LONG) {
            // Perform long format transformation
            return longFormatDate(film.releaseDate);
        }
    },
  },
}

This way, the client can request the release date in the format they need, and the server can handle the transformation before returning the data. This reduces the amount of code needed on the client side to manipulate the data and improves the performance of the application.

It's worth noting that in some cases, it may make sense to keep some of the data manipulation logic on the client side. The goal is to find the right balance between server and client-side code, and to make sure that the code you ship to the client only contains the logic that is necessary for the user experience.

Wrapping REST APIs to leverage GraphQL features

In some cases, you may not have the ability to create a new GraphQL API from scratch. Fortunately, it is possible to wrap existing REST APIs with GraphQL using tools such as Apollo Server. This allows you to take advantage of the benefits of GraphQL, even if you don't have control over the underlying API(s).

In our movie application, instead of having our own database of films and actors we might have used the IMDB API instead. Apollo Server specifically provides us with abstractions around the data sources that we use to resolve our data from.

Conclusion

In conclusion, GraphQL is a powerful tool that can be used to ship less JavaScript to the client, resulting in improved performance for your web application. By defining well-crafted schemas, and utilizing resolvers effectively, you can reduce the amount of data shipped over the network and the amount of processing required by the client. If you are working with existing REST APIs already, you can use tools like Apollo Server to wrap them and take advantage of the benefits of GraphQL.

Whether you are building a new application or optimizing an existing one, consider using GraphQL to ship less JavaScript, and improve the performance of your app. If you want to learn more about GraphQL, check out graphql.framework.dev, our open-source library filled with curated GraphQL resources!