In the world of web development, efficiency is key. That’s why when we decided to switch a feature in our Node.js and React project to GraphQL, we were aiming for a more efficient data fetching process. Let’s dive into some real code examples to illustrate how we made the switch on both the backend and frontend.

GraphQL

The Challenge with REST

Before diving into the GraphQL world, let’s set the stage by discussing the initial setup. Our application, like many others, relied heavily on REST APIs for data fetching. This approach worked well enough in the early days, but as our application grew in complexity, we started facing several challenges:

  • Over-fetching: Fetching more data than needed, because REST endpoints returned fixed data structures.
  • Under-fetching: The need to make additional requests to fetch related data, leading to the dreaded “waterfall” of HTTP requests.
  • Slow performance: Both over-fetching and under-fetching contributed to slower application performance, especially noticeable in mobile networks.

These challenges were particularly evident in a feature we developed to display a dashboard of user analytics. The dashboard needed to aggregate data from multiple sources, and the RESTful approach resulted in multiple round trips to the server, significantly impacting the load time.

The Switch to GraphQL

After much deliberation, we decided to give GraphQL a try for this feature. GraphQL, with its powerful query language, promised to address many of our REST-related challenges by allowing clients to specify exactly what data they need in a single request.

Implementing GraphQL

Backend: Setting Up GraphQL with Node.js

On the backend, we used Apollo Server, a popular GraphQL server that integrates well with Node.js. Here’s a simplified version of how we set up our GraphQL schema and resolvers in Node.js:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
const { ApolloServer, gql } = require('apollo-server');

// GraphQL schema definition
const typeDefs = gql`
  type Query {
    user(id: ID!): User
  }

  type User {
    id: ID!
    name: String
    analytics: Analytics
  }

  type Analytics {
    sessions: SessionData
    pageViews: PageViewData
  }

  type SessionData {
    totalCount: Int
    averageDuration: Float
  }

  type PageViewData {
    totalCount: Int
  }
`;

// resolver functions
const resolvers = {
  Query: {
    user: (parent, args, context, info) => {
      // Fetch the user by id from your data source
      return context.dataSources.userAPI.getUserById(args.id);
    },
  },
  User: {
    analytics: (user) => {
      return getAnalyticsForUser(user.id);
    },
  },
};

// The Apollo Server setup
const server = new ApolloServer({
  typeDefs,
  resolvers,
  context: () => ({
    dataSources: {
      userAPI: new UserAPI(), // data source
    },
  }),
});

// Server startup
server.listen().then(({ url }) => {
  console.log(`Server ready at ${url}`);
});

This code sets up a GraphQL server that can respond to queries for user data, including nested analytics information. The context function is used to provide data sources to the resolvers, which can be anything from a REST API to a database client

Frontend: Fetching Data with Apollo Client in React

On the frontend, we used Apollo Client to interact with our GraphQL server from our React application. Here’s how we set up Apollo Client and executed a query to fetch user analytics:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import React from 'react';
import { ApolloClient, InMemoryCache, ApolloProvider, useQuery, gql } from '@apollo/client';

// Initialize Apollo Client
const client = new ApolloClient({
  uri: 'http://localhost:4000/graphql',
  cache: new InMemoryCache(),
});

// GraphQL query definition
const GET_USER_ANALYTICS = gql`
  query GetUserAnalytics($userId: ID!) {
    user(id: $userId) {
      id
      name
      analytics {
        sessions {
          totalCount
          averageDuration
        }
        pageViews {
          totalCount
        }
      }
    }
  }
`;

// React component that uses the useQuery hook to fetch data
const UserAnalytics = ({ userId }) => {
  const { loading, error, data } = useQuery(GET_USER_ANALYTICS, {
    variables: { userId },
  });

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error :(</p>;

  // Render user analytics data
  return (
    <div>
      <h3>{data.user.name}'s Analytics</h3>
      <p>Total Sessions: {data.user.analytics.sessions.totalCount}</p>
      <p>Average Session Duration: {data.user.analytics.sessions.averageDuration}</p>
      <p>Total Page Views: {data.user.analytics.pageViews.totalCount}</p>
    </div>
  );
};

// ApolloProvider wrapper component in the App
const App = () => (
  <ApolloProvider client={client}>
    <UserAnalytics userId="1" />
  </ApolloProvider>
);

export default App;

In this React component, we use the useQuery hook provided by Apollo Client to execute the GET_USER_ANALYTICS query. We pass the user ID as a variable to the query and render the fetched analytics data.

Remember, these code examples are simplified for illustration purposes. In a real-world application, you would need to handle more complex data fetching, error handling, and state management. But the core concepts remain the same: define your schema, set up resolvers, and use Apollo Client to fetch data efficiently.

The Results

The impact of switching to GraphQL was immediate and profound:

  • Our frontend could now request exactly the data it needed for the dashboard, nothing more, nothing less. This significantly reduced the amount of data transferred over the network.
  • We could fetch all related data in a single request, thanks to GraphQL’s ability to query nested data structures. This eliminated the need for multiple round trips to the server.
  • The dashboard’s load time improved dramatically, offering a better user experience, especially on mobile devices where network latency is more of an issue.