⚡ How to Add a GraphQL Server to a RESTful Express.js API in 2 Minutes

GraphQL and Express.js

GraphQL, the query language developed by Facebook, has seen a meteoric rise in popularity since its public release in 2015. It has quickly become a viable alternative to the reigning API standard, REST.

According to the 2019 State of JavaScript survey, GraphQL has an astonishing 98% satisfaction rating among developers who have used it. Another study by Axios found that 30% of organizations were using GraphQL in 2019, and another 30% were planning to adopt it in 2020.

So what makes GraphQL so appealing? In contrast to REST APIs, which expose a fixed set of endpoints, GraphQL allows clients to declaratively fetch exactly the data they need. This flexibility eliminates issues like overfetching (receiving more data than needed) and underfetching (not receiving enough data in a single request).

One common misconception about adopting GraphQL is that it requires a complete rewrite of your backend. In reality, you can incrementally add GraphQL to an existing Express.js app. Thanks to the apollo-server-express package, you can have a functioning GraphQL server up and running in just 2 minutes!

Setup

Before we begin, ensure that you have a basic Express.js app ready. If you‘re starting from scratch, the easiest way is to use the express-generator tool:

npx express-generator my-app
cd my-app
npm install

Next, install the necessary dependencies:

npm install apollo-server-express graphql

Create a GraphQL Schema

The foundation of any GraphQL server is its schema. The schema strictly defines the data types, fields, and operations (queries and mutations) available to clients.

Let‘s create a schema for a simple book tracking app. Create a new file called schema.js with the following contents:

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

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

  type Author {
    id: ID!
    firstName: String!
    lastName: String!
    books: [Book]
  }

  type Query {
    books: [Book]
    book(id: ID!): Book
    authors: [Author]
    author(id: ID!): Author
  }

  type Mutation {
    addBook(title: String!, authorId: ID!): Book
    addAuthor(firstName: String!, lastName: String!): Author
  }
`;

module.exports = typeDefs;

This schema defines two main types: Book and Author, each with fields like id, title, and name. It also specifies a set of queries for fetching books and authors, and mutations for creating new ones.

Note the use of the gql template literal tag. This tag is used by Apollo to parse the schema definition into an abstract syntax tree.

Create Resolvers

With the schema in place, the next step is to provide resolver functions that fulfill the queries and mutations. Resolvers are where the actual data fetching and manipulation logic lives.

Create a new file named resolvers.js with the following code:

const books = [
  {
    id: ‘1‘,
    title: ‘The Awakening‘,
    authorId: ‘1‘
  },
  {
    id: ‘2‘,
    title: ‘City of Glass‘,
    authorId: ‘2‘
  },
];

const authors = [
  {
    id: ‘1‘,
    firstName: ‘Kate‘,
    lastName: ‘Chopin‘,
  },
  {
    id: ‘2‘,
    firstName: ‘Paul‘,
    lastName: ‘Auster‘,
  },
];

const resolvers = {
  Query: {
    books: () => books,
    book: (_, { id }) => books.find(book => book.id === id),
    authors: () => authors,
    author: (_, { id }) => authors.find(author => author.id === id)
  },
  Mutation: {
    addBook: (_, { title, authorId }) => {
      const newBook = {
        id: String(books.length + 1),
        title,
        authorId
      };

      books.push(newBook);

      return newBook;
    },
    addAuthor: (_, { firstName, lastName }) => {
      const newAuthor = {
        id: String(authors.length + 1),
        firstName,
        lastName
      };

      authors.push(newAuthor);

      return newAuthor;
    }
  },
  Author: {
    books: (author) => books.filter(book => book.authorId === author.id)
  },
  Book: {
    author: (book) => authors.find(author => author.id === book.authorId)
  }
};

module.exports = resolvers;

Here we define resolvers for each of the queries and mutations from the schema. To keep things simple, the book and author data is just stored in local arrays within the module. In a real app, this data would likely come from a database or external API.

There are also resolvers for the Author.books and Book.author fields. These resolvers allow GraphQL to traverse from an author to its list of books, and from a book to its author. This is one of the key benefits of GraphQL – the ability to navigate connected data in a single request.

Create the Server

Now that we have a schema and resolvers, we‘re ready to create our GraphQL server. Open up the main Express app file (often named app.js or index.js) and add the following code:

const express = require(‘express‘);
const { ApolloServer } = require(‘apollo-server-express‘);

const typeDefs = require(‘./schema‘);
const resolvers = require(‘./resolvers‘);

const app = express();

const server = new ApolloServer({ 
  typeDefs, 
  resolvers,
  playground: true, // Enable GraphQL Playground
  introspection: true
});

server.applyMiddleware({ app });

const port = process.env.PORT || 4000;

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

This code creates a new ApolloServer instance, passing in our schema and resolvers. The playground and introspection options enable helpful development tools.

The applyMiddleware function connects Apollo Server to Express. It adds the GraphQL endpoint (defaulting to /graphql) and enables Apollo‘s built-in GraphQL Playground.

Finally, we start the Express app listening on port 4000.

Start up the server with:

node app.js

Open up a web browser and navigate to http://localhost:4000/graphql. You should see the interactive GraphQL Playground IDE:

GraphQL Playground

Try running a query to fetch some books:

query GetBooks {
  books {
    id
    title
    author {
      firstName
      lastName
    }
  }
}

Or a mutation to add a new book:

mutation AddBook {
  addBook(title: "Wuthering Heights", authorId: "1") {
    id
    title
  }
}

GraphQL Playground provides a great way to explore and test out your API without needing a dedicated client.

Performance Considerations

As you flesh out your GraphQL API, there are several key performance considerations to keep in mind.

GraphQL‘s ability to query nested and connected data in a single request can potentially lead to an explosion of database calls, known as the "N+1" problem. Consider a query like this:

query GetAuthors {
  authors {
    firstName
    lastName
    books {
      title
      publishedAt
    } 
  }
}

If implemented naively, this could result in 1 query to fetch the list of authors, plus N additional queries (one per author) to fetch each author‘s list of books.

The solution is to use a technique called DataLoader batching. DataLoader allows you to combine multiple requests into a single batch, dramatically reducing the number of database calls.

Another critical aspect of performance is caching. Caching can happen at multiple levels:

  1. Database level – Use built-in caching mechanisms in databases like Redis or Memcached.
  2. GraphQL server level – Apollo Server has a caching API that can cache responses in memory or in a backend like Redis.
  3. Client level – Apollo Client and other GraphQL clients can cache responses on the client-side, avoiding unnecessary network requests.

A 2020 performance benchmark study by Hasura found that server-side caching can lead to a 10x improvement in response times and a 10-100x reduction in database loads. Client-side caching can make responses up to 100x faster.

Architecture Best Practices

When designing a GraphQL server, it‘s important to follow architecture best practices to ensure maintainability and scalability.

A common pattern is to use a layered architecture:

  1. GraphQL Schema Layer – Defines the types, fields, and operations exposed by the API
  2. Resolver Layer – Contains the resolver functions for each field and operation
  3. Service Layer – Encapsulates business logic and data access
  4. Data Access Layer – Interacts with databases or other data sources

Keeping a clean separation between these layers makes the codebase more modular and easier to reason about.

Another key decision is how to handle authorization. It‘s recommended to use a GraphQL directive to annotate the schema with authorization rules. For example:

const typeDefs = gql`
  directive @isAuthenticated on OBJECT | FIELD_DEFINITION

  type User @isAuthenticated {
    id: ID!
    email: String!
  }
`

Then, create a custom directive that checks for a valid user in the request context:

const { SchemaDirectiveVisitor } = require(‘apollo-server-express‘);
const { defaultFieldResolver } = require(‘graphql‘);

class IsAuthenticatedDirective extends SchemaDirectiveVisitor {
  visitObject(type) {
    this.ensureFieldsWrapped(type);
  }

  visitFieldDefinition(field, details) {
    this.ensureFieldsWrapped(details.objectType);
  }

  ensureFieldsWrapped(objectType) {
    if (objectType._authFieldsWrapped) return;
    objectType._authFieldsWrapped = true;

    const fields = objectType.getFields();

    Object.values(fields).forEach((field) => {
      const originalResolver = field.resolve || defaultFieldResolver;

      field.resolve = async function (...args) {
        const context = args[2];

        if (!context.user) {
          throw new Error(‘Unauthorized‘);
        }

        return originalResolver.apply(this, args);
      };
    });
  }
}

This directive will check for the existence of context.user before resolving any fields or objects marked with @isAuthenticated.

Monitoring and Observability

As you deploy your GraphQL server to production, it‘s crucial to have robust monitoring and observability in place. Some key metrics to track:

  • Request rate and response times
  • Error rates and types
  • Cache hit/miss rates
  • Database query performance
  • Memory and CPU usage

Apollo Server has built-in integrations with monitoring tools like Apollo Graph Manager, Prometheus, and Datadog. These tools provide dashboards and alerts to help you keep a pulse on your GraphQL server‘s health.

Another great tool is Apollo Tracing, which adds detailed performance tracing to your GraphQL requests. It can help identify bottlenecks and slow resolvers.

In addition to monitoring, be sure to implement comprehensive logging throughout your server code. Tools like Winston or Bunyan make it easy to generate structured JSON logs.

Conclusion

GraphQL is a powerful and flexible alternative to traditional REST APIs. As we‘ve seen, getting started with GraphQL in an Express app is quick and painless thanks to Apollo Server.

But we‘ve only scratched the surface of what‘s possible with GraphQL. As your API grows, keep performance, architecture, and monitoring best practices in mind.

Here are some additional resources to help you on your GraphQL journey:

The ecosystem of tools and libraries around GraphQL is rapidly expanding. Some of my personal favorites:

  • TypeGraphQL – Create GraphQL APIs with TypeScript
  • GraphQL Code Generator – Generate TypeScript types and React hooks based on your schema
  • Hasura – Instantly build GraphQL APIs on PostgreSQL
  • Prisma – Modern database access toolkit
  • Apollo Client – Declarative data fetching for React apps

I‘m excited to see how you use GraphQL and Apollo Server in your projects! Feel free to reach out if you have any questions.

Similar Posts