Skip to content

Instantly share code, notes, and snippets.

@dhruv-m-patel
Last active March 28, 2022 01:57
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dhruv-m-patel/b4eab70d8c8febe80a58e868ce811ea6 to your computer and use it in GitHub Desktop.
Save dhruv-m-patel/b4eab70d8c8febe80a58e868ce811ea6 to your computer and use it in GitHub Desktop.
Understanding Apollo GraphQL (Full stack edition)

Apollo GraphQL

  • Apollo GrpahQL allows a simplified way of building a server with GraphQL. It also allows you do the data binding in various technical stacks using Apollo packages on client side applications.
  • GraphQL follows schema first design requiring to think of data in terms of graphs
  • In a graph, nodes represent objects, where as arrows (edges) represent relationships

Part 1: Basics of full-stack development with Apollo GraphQL

GraphQL Schema:

  • A schema defines objects and their properties.

  • It essentially outlines the contract between the server and client through a common interface for data.

  • Here is an example of a partial graphql schema:

    type SpaceCat {
      age: Int
      missions: [Mission]
      name: String!
    }
  • To add description to your schemas, GraphQL allows you to add single comments using “comment” (comment between double quotes) and multiline comments using comment between triple double quotes i.e. ”””.

  • Following is an example of a schema defining relationships and data query:

    const typeDefs = gql`
      type Query {
            homeTracks: [Track!]
        }
    
        "Author of a complete Track"
        type Author {
            id: ID!
            "Author's first and last name"
            name: String!
            "Author's profile picture url"
            photo: String
        }
    
        "A track is a group of Modules that teaches about a specific topic"
        type Track {
            id: ID!
            "The track's title"
            title: String!
            "The track's main author"
            author: Author!
            "The track's main illustration to display in track card or track page detail"
            thumbnail: String
            "The track's approximate length to complete, in minutes"
            length: Int
            "The number of modules this track contains"
            modulesCount: Int
        }
    `

How GraphQL Server is initialized?

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

const server = new ApolloServer({ typeDefs });
server.listen().then(() => {
    console.log(`
🚀  Server is running!
🔉  Listening on port 4000
    `);
});

To enable mocked data without a backend connection, following example demonstrates the data binding

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

const mocks = {
    Query: () => ({
        homeTracks: () => [...new Array(10)],
    }),
    Track: () => ({
        id: () => "track_01",
        title: () => "Astro Kitty, Space Explorer",
        author: () => {
            return {
                name: "Grumpy Cat",
                photo: "https://res.cloudinary.com/dety84pbu/image/upload/v1606816219/kitty-veyron-sm_mctf3c.jpg",
            };
        },
        thumbnail: () =>
            "https://res.cloudinary.com/dety84pbu/image/upload/v1598465568/nebula_cat_djkt9r.jpg",
        length: () => 1210,
        modulesCount: () => 6,
    }),
};

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

server.listen().then(() => {
    console.log(`
🚀  Server is running!
🔉  Listening on port 4000
    `);
});

How does client initiate connection to Apollo Server’s GraphQL endpoint?

import React from "react";
import ReactDOM from "react-dom";
import App from "./app";
import { ApolloProvider, ApolloClient, InMemoryCache } from "@apollo/client";

const client = new ApolloClient({
    uri: "http://localhost:4000",
    cache: new InMemoryCache(),
});

ReactDOM.render(
    <React.StrictMode>
        <ApolloProvider client={client}>
            <App />
        </ApolloProvider>
    </React.StrictMode>,
    document.getElementById("root")
);

Here, ApolloClient is used to create a client connected to GraphQL endpoint.

ApolloProvider accepts the apollo client connected to GraphQL endpoint as a property and allows children to query GraphQL server for data.

InMemoryCache will cache queried data on client end before performing query to server again.

How does client do the data fetching?

import React from "react";
import { gql, useQuery } from "@apollo/client";

const TRACKS = gql`
    query getTracks {
        homeTracks {
            id
            title
            thumbnail
            length
            modulesCount
            author {
                id
                name
                photo
            }
        }
    }
`;

const App = () => {
    const { loading, error, data } = useQuery(TRACKS);

    if (loading) { 
        return 'Loading...';
    } else if (error) {
        return `ERROR: ${error.message}`;
    } else {
        return JSON.stringify(data);
    }
};

export default App;
  • gql is used to write the data fetching query. Usually it’s a good idea to query data you need through GraphQL query editor such as GraphiQL or GraphQL playground.
  • TRACKS in this example holds a single query that we need for fetching data for tracks. getTracks in query is a placeholder and it can really be anything - however it should be meaningful for your reference to indicate what you are doing with data. homeTracks is actual GraphQL query on server that will return data.
  • useQuery is used to fetch data using TRACKS query. It returns 3 things: loading state indicating whether data is still being fetched. error indicates whether there was an error of not - this is a standard js error object. data represents actual data returned by server, it can be undefined initially and populated later when data becomes available on client.

The Journey of a GraphQL Query

  • Browser issues query to GraphQL server
  • GraphQL server receives request and parses request
  • Server builds an AST (Abstract Syntax Tree) to understand the document tree it needs to fetch
  • Server then validates the request looking at AST for types and fields defined in schema
  • Server will then pass valid request to correct resolver(s) to process it
  • Resolver will fetch data / apply mutation and respond with result
  • Results will be gathered and sent back to response

Part 2: Resolvers & DataSources

What is a Resolver?

  • A resolver is a function whose job is to resolve data for each field given in the query

  • A query consists of fields, each of which uses a resolver to resolve data

  • A resolver function has a standard signature that accepts 4 arguments, i.e.

    (parent, args, context, info) => { 
    		// gather data
    		return data;
    }
    • parent points to the parent field for which data is being fetched. For e.g. for a Track if author information is being fetched, parent would point to Track entity.
    • args are actual arguments needed to resolve data. In tracks example for fetching author information, this would be authorId to resolve author information
    • context holds shared data across all resolvers. This contains state information, database connection, authentication state etc.
    • info contains information about the current graphql operation - this includes field names etc. and could be used to set cache policies at resolver level.

What is a DataSource?

A data source is the mechanism from which you retrieve the data. This can be a database, a REST api, etc.

What are the practices to consider when working with resolvers and datasources?

  • Consider following type of structure to simplify organization of resolvers and data sources:
    src
    |- data-sources
    |-- track-api.js
    |- resolvers.js
    
  • Each data source should be separately defined in src/data-sources
  • If you're calling a REST api to gather data, you could use RESTDataSource from apollo-datasource-rest to create an optimized wrapper over your REST api rather than using node-fetch. Reretrieving same data would be much faster with RESTDataSource extensions.
  • For resolver functions, if an argument is used, you could use _ (underscore) to indicate its unused. e.g. (_, __, { dataSource }) => {} - here you are only using context to access dataSource, except that everything else isnt needed.

How do I replace mocks with Resolver and DataSource?

  • Here is how you could build a wrapper for TrackApi using RESTDataSource from apollo-datasource-rest in src/data-sources/track-api.js:

      const { RESTDataSource } = require("apollo-datasource-rest");
    
      class TrackApi extends RESTDataSource {
          constructor() {
              super();
              this.baseURL = "https://odyssey-lift-off-rest-api.herokuapp.com/";
          }
    
          getTracksForHome() {
              return this.get("tracks"); // i.e. GET https://odyssey-lift-off-rest-api.herokuapp.com/tracks
          }
    
          getAuthor(authorId) {
              return this.get(`author/${authorId}`); // i.e. GET https://odyssey-lift-off-rest-api.herokuapp.com/author/{authorId}
          }
      }
    
      module.exports = TrackApi;
    • As seen above, you can create a wrapper over a REST api by defining a class for TrackApi that extends RESTDataSource
    • baseURL in constructor needs to be set to REST Api host
    • You can define your functions that make calls using REST verbs over this property
    • The first argument accepted here defines the path of the endpoint on the REST api host
  • And here is a sample of src/resolver.js that would use TrackApi to pull data:

    const resolvers = {
      Query: {
          // returns collection of tracks with their authors
          tracksForHome: (_, __, { dataSources }) => {
              return dataSources.trackApi.getTracksForHome();
          },
      },
      Track: {
          author: ({ authorId }, _, { dataSources }) => {
              return dataSources.trackApi.getAuthor(authorId);
          },
      },
    };
    
    module.exports = resolvers;
    • The unused arguments as seen above are replaced with _(s) in relevant order to make them unique
    • The query does the job of resolving data
    • For any nested relationships, to fetch child data, you will need to define the top level field with child field in that entity that uses a resolver function to populate relevant data. i.e. Track entity resolving author information through dataSources using the authorId found in parent Track entity data.
  • Finally, you can now replace mocks returned from server with actual data using resolvers and datasources as follows:

      const { ApolloServer } = require("apollo-server");
      const typeDefs = require("./schema");
      const resolvers = require("./resolvers");
      const TrackApi = require("./data-sources/track-api");
    
      const server = new ApolloServer({
          typeDefs,
          resolvers,
          dataSources: () => ({
              trackApi: new TrackApi(),
          }),
      });
    
      server.listen().then(() => {
          console.log(`
          🚀  Server is running!
          🔉  Listening on port 4000
      `);
      });
    • mocks was removed in its entirety here and instead was replaced with resolvers and dataSources here that query actual data

Reference

If you are a beginner with Apollo GraphQL, I would highly recommend https://odyssey.apollographql.com for good learning experience.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment