2020 Tech Stack: GraphQL Apollo server with React.js

Wojciech Cichoradzki
12 May 2020

Since 2000, RESTful principles have been the industry standard for building web APIs. While REST solved many problems that previous protocols could not, it has flaws. In modern applications, the data interconnects in complex relations which can create performance issues in product development. This is exactly what GraphQL aims at solving.

GraphQL vs Rest

Based on HTTP, REST is an easy way to communicate between the client application and the server. Data is accessible and modifiable via HTTP methods on specific endpoints. The application is thus decoupled from the server. Custom libraries are also no longer needed. The integration with different platforms is easier. 

In a news website, each article has its title, text content, date, author, and some visual content like images or videos. It may also have user’s comments grouped into threads and links to other related stories. The growing complexity in the data relationship graph shows the limits of the traditional REST approach. Each resource has to be accessed separately, often in sequence since one resource is dependable on the other. GraphQL improves the performance.

GraphQL

In 2015, Facebook engineers introduced GraphQL as a brand-new solution for designing APIs. The core concept of GraphQL is to give the developers more granular control over the resources needed by providing a single ‘smart’ endpoint instead of having a number of different endpoints for each resource. 

The building of GraphQL APIs is organized in terms of types and fields, not endpoints. Data retrieval methods are queries and any manipulation on the data is a mutation in GraphQL terminology. Since the demand in the industry for a solution like GraphQL was already high, it quickly gained popularity and support from developers. 

GraphQL has become popular and offers implementations in all popular programming languages. It has already been adopted by large companies like Twitter, Yelp, The New York Times, Airbnb, and more.

The developers appreciate the robustness and ease of using GraphQL. The 2019 developer survey “State of Javascript” showed that almost 40% of Javascript developers had already tried GraphQL and would use it again (Figure 1). The trend is rising, and we will see more GraphQL adoption in the upcoming years.

Figure 1 The results of State of JS survey on GraphQL experience

Apollo GraphQL 

Apollo is a leading GraphQL implementation that provides a set of tools and libraries that help developers build GraphQL applications. The typical Apollo server consists of a set of GraphQL schema definitions and their resolvers. To showcase the capabilities of the GraphQL, we will be building a simple application that encompasses the Apollo Node.js server and React.js client.

The application will be storing user’s read book reviews. Let’s start by creating the basic structure for the project and build our server.

$ mkdir app app/server app/client && cd app/server
$ yarn init

Next, we have to create a database with two tables: Authors and Books.

CREATE TABLE `authors` (
  `id` INT NOT NULL AUTO_INCREMENT,
  `name` VARCHAR(256) NOT NULL UNIQUE,
  PRIMARY KEY (`id`)
);
CREATE TABLE `books` (
  `id` INT NOT NULL AUTO_INCREMENT,
  `author` INT NOT NULL,
  `title` VARCHAR(512) NOT NULL,
  `image` VARCHAR(512) NOT NULL,
  `review` VARCHAR(2048) NOT NULL,
  PRIMARY KEY (`id`),
  FOREIGN KEY (`author`)
    REFERENCES `authors`(`id`)
    ON UPDATE NO ACTION ON DELETE CASCADE
);

To communicate with the database, we will be using the knex query builder library. Let’s install the required dependencies and set up a basic server.

$ yarn add dotenv knex mysql2 apollo-server
$ touch index.js

Within index.js, we are going to create our schema definitions and resolvers. We will be using two types: Author and Book. Besides, our API will expose two queries for each type, as well as a single mutation that adds a new book.

require('dotenv').config();
const { ApolloServer, gql } = require('apollo-server');


const knex = require('knex')({
  client: 'mysql2',
  connection: {
    host: process.env.DB_HOST || 'localhost',
    user: process.env.DB_USER || 'root',
    password: process.env.DB_PASSWORD || '1234',
    database: process.env.DB_NAME || 'books_database',
  },
});

// The GraphQL schema
const typeDefs = gql`
  type Author {
    id: Int!
    name: String!
  }

  type Book {
    id: Int!
    author: Author!
    title: String!
    image: String!
    review: String!
  }

  type Query {
    books: [Book!]
    authors: [Author!]
  }

  type Mutation {
    addBook(author: String!, title: String!, image: String!, review: String!): Book!
  }
`;

// A map of functions which return data for the schema.
const resolvers = {
  Query: {
    books: async (parent, args, context, resolveInfo) => {
      const result = await knex('books')
        .leftJoin('authors', 'books.author', 'authors.id')
        .select('*')
        .options({ nestTables: true });
        return result.map(({ books, authors }) => ({
        ...books,
        author: authors,
      }));
    },
    authors: async () => knex('authors').select('*'),
  },
  Mutation: {
    addBook: async (parent, {
      author: authorName, title, image, review,
    }) => {
      let author = await knex('authors').first().where({ name: authorName });
      if (!author) {
        const [authorId] = await knex('authors').insert({ name: authorName });
        author = {
          id: authorId,
          name: authorName,
        };
      }
      const newBook = {
        author: author.id,
        title,
        image,
        review,
      };
      const [bookId] = await knex('books').insert(newBook);
      return {
        ...newBook,
        id: bookId,
      };
    },
  },
};

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

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

Now, to start the server and test our API with the knex logs exposed, we have to run Node.js on our index.js file:

DEBUG=knex:query node index.js
🚀 Server ready at http://localhost:4000/ 

Next, if we open http://localhost:4000/, we are welcomed by what is called GraphQL playground. Here, we can easily test our API. Let’s try to retrieve all the books.

 Since we currently don’t have any book stored, the response is just an empty array. We can add books by calling addBook mutation.

When we query books again, we see the newly created book in the response list.

 Our "books" resolver is performing a left join on the ‘authors’ table to retrieve the id and name of the author and pass it to the response. However, when the author field is not requested, the expensive left join operation could be omitted. To achieve such optimization, we can use the graphql-parse-resolve-info package and refactor our resolver, so it checks whether the author field is present in the request.

const {
  parseResolveInfo,
  simplifyParsedResolveInfoFragmentWithType,
} = require('graphql-parse-resolve-info');
…
    books: async (parent, args, context, resolveInfo) => {
      const query = knex('books')
        .select('*')
        .options({ nestTables: true });
      const simplifiedFragment = simplifyParsedResolveInfoFragmentWithType(
        parseResolveInfo(resolveInfo),
        resolveInfo.returnType,
      );
      if (simplifiedFragment.fields.author) {
        query.leftJoin('authors', 'books.author', 'authors.id');
      }
 
      const result = await query;
      return result.map(({ books, authors: author }) => ({
        ...books,
        author,
      }));
    },

Now, if we remove the author field from the query, we see a change in the SQL queries performed:

 Our API is now ready to be used by our client application.

React.js

We initialize the React project by using create-react-app and installing the required dependencies:

cd .. && npx create-react-app client && cd client
yarn add apollo-boost @apollo/react-hooks graphql

Now, when running yarn start, a basic starter project should open. Let’s create our GraphQL client and expose it to the sub-components via a context provider. We need to modify index.js like this:

import React from 'react';
import ReactDOM from 'react-dom';
import { ApolloProvider } from '@apollo/react-hooks';
import ApolloClient from 'apollo-boost';

import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';

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

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

As a consequence, we need to perform a query to our API and render a list of books. Apollo gives a set of useful React hooks that help us do that. In our case, we will use the useQuery hook with our previously used GraphQL books query attached. Let’s modify our App.js so it fetches and renders the stored books.

import React from 'react';
import { useQuery } from '@apollo/react-hooks';
import { gql } from 'apollo-boost';

const BOOK_QUERY = gql`
  {
    books{
      id
      author{
        name
      }
      title
      image
      review
    }
  }
`;

export default function App() {
  const { loading, error, data } = useQuery(BOOK_QUERY);

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

  return (
    <div className="App">
      <h2>My Books</h2>
      { data.books.map(({ id, author, title, image, review }) => (
      <div key={id}>
        <img src={image} alt={title} />
        <p>{ title }</p>
        <p>{ author.name }</p>
        <p>{ review }</p>
      </div>
     ))}
    </div>
  );
}

Now, after the refresh, our app successfully renders a list of books. However, the styling is not too appealing. Let’s add the milligram.css library, extract the Book component to a separate module, and add a bit of CSS magic.

Index.html

<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Roboto:300,300italic,700,700italic">
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/normalize/5.0.0/normalize.css">
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/milligram/1.3.0/milligram.css">

Book.js

export default function Book({ author, title, image, review }: Props) {
  return (
    <div className="book container">
      <div className="row">
        <div className="column-10">
          <img
            src={image}
            alt="book cover"
            className="cover"
          />
        </div>
        <div className="column-90">
          <h4 className="title">{ title }</h4>
          <i>{ author }</i>
          <p>{ review }</p>
        </div>
      </div>
    </div>
  );
}

Index.css

.App {
  margin: 2rem 20%;
}

.book .cover {
  width: 100px;
  height: auto;
  margin-right: 2rem;
}

.book .title {
  margin-bottom: 0;
  font-weight: bold;
}

Now, our page looks much better:

 

Our verdict 

The simplicity and robustness of GraphQL, especially when combined with Apollo and React, is the best to use in an all-purpose 2020 tech stack. In the coming months, we expect to see a wider adoption and further optimizations by the growing community. Interested in learning more? Check out our blog for more articles on development tools.

Do you have any questions about product development? If so, head to our contact page or drop us a line at hello@start-up.house

You may also like...

Development

Remote Design Sprint 101

Now more than ever, being able to work with clients remotely is essential. At Startup House, we are experienced in working with clients from all over the world, and we have conducted multiple re...

Filip Wilczek & Kasia Radziejewska
28 May 2020
About us

The importance of mental health when working from home

2020 has been, so far, a challenging year for many. The last two months have been the hardest for professionals throughout the world. One day you were sitting comfortably in an office chair. The...

Aneta Pałczyńska
26 May 2020