tutorial // May 17, 2021

How to Set Up a GraphQL Server with Apollo Server and Express

How to properly configure and handle requests to a GraphQL server using the Apollo Server library in conjunction with an existing Express.js server.

How to Set Up a GraphQL Server with Apollo Server and Express

Getting started

To get started, we're going to rely on the CheatCode Node.js Boilerplate. This will give us an already setup GraphQL server to work with and add context to the explanations below. First, clone the boilerplate via Github:

Terminal

git clone https://github.com/cheatcode/nodejs-server-boilerplate

Next, cd into the cloned nodejs-server-boilerplate directory and install the dependencies:

Terminal

cd nodejs-server-boilerplate && npm install

Next, let's manually add the apollo-server dependency (this is different from the apollo-server-express dependency that's already included in the boilerplate—we'll look at this later):

Terminal

npm i apollo-server

Once this is complete, all of the dependencies you need for the rest of the tutorial will be installed. Now, to start, let's take a look at how to set up a basic GraphQL server with Apollo Server.

Setting up the base server

To get started, we need to import two things as named exports from apollo-server, the ApolloServer constructor and the gql function.

/api/graphql/server.js

import { ApolloServer, gql } from "apollo-server";

// We'll set up our server here.

To create a server, next, we create a new instance of ApolloServer with new ApolloServer():

/api/graphql/server.js

import { ApolloServer, gql } from "apollo-server";

const server = new ApolloServer({
  playground: true,
  typeDefs: gql`
    type Example {
      message: String
    }

    type Query {
      queryExample: Example
    }

    type Mutation {
      mutationExample: Example
    }
  `,
  resolvers: {
    Query: {
      queryExample: (parent, args, context) => {
        return {
          message: "This is the message from the query resolver.",
        };
      },
    },
    Mutation: {
      mutationExample: (parent, args, context) => {
        console.log("Perform mutation here before responding.");

        return {
          message: "This is the message from the mutation resolver.",
        };
      },
    },
  },
});

We've added a lot here, so let's step through it. First, we create a variable server and set it equal to the return value of calling new ApolloServer(). This is our Apollo Server instance. As an argument to that constructor to configure our server, we pass an object with three properties: playground, typeDefs, and resolvers.

Here, playground is assigned a boolean true value that tells Apollo Server to enable the GraphQL Playground GUI at /graphql when the server is running. This is a handy tool for testing and debugging your GraphQL API without having to write a bunch of front-end code. Typically, it's good to limit usage of the playground to only your development NODE_ENV. To do that, you can set playground here to process.env.NODE_ENV === 'development'.

xlbxUcXAdng1zzev/stBrsnRkIAZOBuaC.0
Accessing the GraphQL playground via the browser.

Next, the typeDefs and resolvers properties here, together, describe the schema for your GraphQL server. The former, typeDefs is the part of your schema where you define the possible types, queries, and mutations that the server can handle. In GraphQL, there are two root types Query and Mutation which can be defined alongside your custom types (which describe the shape of the data returned by your queries and mutations) like type Pizza {}.

Above, we've spec'd out a full example schema. First, notice that we've assigned our typeDefs value equal to gql`` where gql() is a function that expects a single argument as a string. The syntax here (without parentheses following the gql) is a built-in feature of JavaScript that allows you to simultaneously invoke a function and pass it a string value at the same time. To be clear, the above is equivalent to gql(´´). Using this syntax requires that the string value passed is done as a template literal (meaning, a string defined using backticks as opposed to single or double quotes).

The gql´´ function itself is responsible for taking a string containing code written in the GraphQL DSL (domain-specific language). DSL, here, refers to the unique syntax of the GraphQL language. When it comes to defining our schema, we have the option of writing it in the GraphQL DSL. The gql`` function takes in that string and converts it from the DSL into an abstract syntax tree (AST) which as an object that describes the schema in a format GraphQL can understand.

Inside the string we pass to gql(), first, we've include a data type as type Example which defines a custom type (not the built-in Query or Mutation types) which describes an object containing a message field whose value should be a String. Next, we define the root Query type and Mutation type. On the root Query type, we define a field queryExample (which we expect to pair with a resolver function next) which we expect to return data in the shape of the type Example we just defined. Next, we do the same for our root Mutation type, by adding mutationExample and also expecting a return value in the shape of type Example.

In order for this to work, we need to implement resolver functions in the resolvers object (passed to our ApolloServer constructor). Notice that here, inside of resolvers we've defined a Query property and a Mutation property. These intentionally mimic the structure of type Query and type Mutation above. The idea here is that the function resolvers.Query.queryExample will be called whenever a query is run on the queryExample field from a client (browser or native app), fulfilling or resolving the query.

The same exact thing is taking place at resolvers.Mutation.mutationExample, however here, we're defining a mutation (meaning, we expect this code to change some data in our data source, not just return some data from our data source). Notice that the shape of the object returned from both the queryExample resolver and mutationExample resolver match the shape of the type Example we defined earlier. This is done because, in our root Query and root Mutation, we've specified that the value returned from those resolvers will be in the shape of the type Example.

/api/graphql/server.js

import { ApolloServer, gql } from "apollo-server";

const server = new ApolloServer({
  playground: true,
  typeDefs: gql`...`,
  resolvers: { ... },
});

server.listen({ port: 3000 }).then(({ url }) => {
  console.log(`Server running at ${url}`);
});

export default () => {};

Finally, with our typeDefs and resolvers defined, we put our server to use. To do it, we take the server variable we stored our Apollo Server in earlier and call it's listen() method which returns a JavaScript Promise (hence the .then() syntax is being chained on the end). Passed to listen(), we provide an options object with a single property port equal to 3000. This instructs Apollo Server to listen for inbound connections at localhost:3000.

With this, we should have a functioning Apollo Server up and running. Of note, because we're overwriting the included /api/graphql/server.js file in the Node.js boilerplate we started from, we've added an export default () => {}, exporting an empty function to fulfill the expectations of the existing Express.js server (we'll learn how to connect the Apollo Server with this Express server later in the tutorial).

To give this a test, from the root of the boilerplate, run npm run dev to start up the server. Fair warning, because we're starting two separate servers with this command (the Apollo Server we just implemented above and the existing Express server included in the boilerplate), you will see two statements logged telling you the server is running on different ports:

Terminal

Server running at http://localhost:5001
Server running at http://localhost:3000/

Before we move on to combining this new Apollo Server with the existing Express server in the boilerplate, let's look at how to set a custom context for resolvers.

Setting the resolver context

While we technically have a functioning GraphQL server right now (you can verify this by visiting http://localhost:3000/graphql in your browser), it's good to be aware of how to set a custom resolver context as this plays into user authentication when using GraphQL as your main data layer.

/api/graphql/server.js

import { ApolloServer, gql } from "apollo-server";

const server = new ApolloServer({
  playground: true,
  context: async ({ req, res }) => {
    const token = req?.cookies["jwt_token"];

    const context = {
      req,
      res,
      user: {},
    };

    const user = token ? await authenticationMethod({ token }) : null;

    if (!user?.error) {
      context.user = user;
    }

    return context;
  },
  typeDefs: gql`...`,
  resolvers: { ... },
});

server.listen({ port: 3000 }).then(({ url }) => {
  console.log(`Server running at ${url}`);
});

export default () => {};

In GraphQL, whether you're performing a query or mutation, your resolver functions are passed a context object as their final argument. This object contains the current "context" for the request being made to the GraphQL server. For example, if a user is logged in to your app and performs a GraphQL request, we may want to include the user's account information in the context to help us resolve the query or mutation (e.g., verifying that the logged in user has the proper permissions to access that query or mutation).

Here, alongside the playground, typeDefs, and resolvers properties we added earlier, we've added context set to a function. This function is automatically called by Apollo Server whenever a request comes into the server. It's passed an options object as an argument containing the server request req and response res objects (what Apollo Server uses internally to respond to the HTTP request made to the GraphQL server).

From that function, we want to return an object representing the context argument that we want available in all of our resolvers. Above, we've come up with a hypothetical example where we anticipate an HTTP cookie being passed to the server (along with the GraphQL request) and using that to authenticate a user. Note: this is pseudo code and will not return a user in its current state.

To assign the user to the context object, we define a base context object first, which contains the req and res from the options object passed to the context function via Apollo Server and combine that with an empty object representing our user. Next, we attempt to authenticate our user using the assumed jwt_token cookie. Again, hypothetically, if this function existed, we would expect us to return a user object (e.g., containing an email address, username, and other user-identifying data).

Finally, from the context: () => {} function, we return the context object we defined (with the req, res, and user) values.

/api/graphql/server.js

import * as apolloServer from "apollo-server";
const { ApolloServer, gql } = apolloServer.default;

const server = new ApolloServer({
  playground: true,
  context: async ({ req, res }) => {
    [...]

    return context;
  },
  typeDefs: gql`...`,
  resolvers: {
    Query: {
      queryExample: (parent, args, context) => {
        console.log(context.user);
        return {
          message: "This is the message from the query resolver.",
        };
      },
    },
    Mutation: {
      mutationExample: (parent, args, context) => {
        console.log(context.user);
        console.log("Perform mutation here before responding.");

        return {
          message: "This is the message from the mutation resolver.",
        };
      },
    },
  },
});

server.listen({ port: 3000 }).then(({ url }) => {
  console.log(`Server running at ${url}`);
});

Showcasing how to put the context to use, here, inside of our queryExample and mutationExample resolvers, we've logged out the context.user value we set above.

Attaching the GraphQL server to an existing Express server

Up until this point we've been setting up our Apollo Server to be a standalone GraphQL server (meaning, we're not attaching it to an existing server). Though this works, it limits our server to only having a /graphql endpoint. To get around this, we have the option of "attaching" our Apollo Server to an existing HTTP server.

What we're going to do now is paste back in the original source of the /api/graphql/server.js file that we overwrote above with our standalone GraphQL server:

/api/graphql/server.js

import { ApolloServer } from "apollo-server-express";
import schema from "./schema";
import { isDevelopment } from "../../.app/environment";
import loginWithToken from "../users/token";
import { configuration as corsConfiguration } from "../../middleware/cors";

export default (app) => {
  const server = new ApolloServer({
    ...schema,
    introspection: isDevelopment,
    playground: isDevelopment,
    context: async ({ req, res }) => {
      const token = req?.cookies["app_login_token"];

      const context = {
        req,
        res,
        user: {},
      };

      const user = token ? await loginWithToken({ token }) : null;

      if (!user?.error) {
        context.user = user;
      }

      return context;
    },
  });

  server.applyMiddleware({
    cors: corsConfiguration,
    app,
    path: "/api/graphql",
  });
};

Some of this should look familiar. First, notice that instead of calling to new ApolloServer() directly within the body of our /api/graphql/server.js file, we've wrapped that call in a function expecting app as an argument. Here, app represents the existing Express.js server set up at /index.js in the Node.js boilerplate we've been using throughout this tutorial.

Inside the function (notice that we're exporting this function as the default export for the file), we set up our Apollo Server just like we did above. Here, though, notice that typeDefs and resolvers are missing as properties. These are contained within the schema value imported from the ./schema.js file in the same directory at /api/graphql/schema.js.

The contents of this file are nearly identical to what we saw above. It's separated in the boilerplate for organizational purposes—this does not serve any technical purpose. To utilize that file, we use the JavaScript spread operator ... to say "unpack the contents of the object contained in the imported schema value onto the object we're passing to new ApolloServer()." As part of this unpacking, the typeDefs and resolvers properties on that imported object will be assigned back to the options we're passing to new ApolloServer().

Just below this, we can also see a new property being added introspection. This—along with the existing playground property we saw earlier—is set to the value of isDevelopment, a value that's imported via the .app/environment.js file from the root of the project and tells us whether or not our process.env.NODE_ENV value is equal to development (meaning we're running this code in our development environment).

The introspection property tells Apollo Server whether or not to allow GraphQL clients to "introspect" or discover the types, queries, mutations, etc. that the GraphQL server offers. While this is helpful for debugging and public APIs built with GraphQL, it's a security risk for private APIs built with GraphQL.

/api/graphql/server.js

import { ApolloServer } from "apollo-server-express";
import schema from "./schema";
import { isDevelopment } from "../../.app/environment";
import loginWithToken from "../users/token";
import { configuration as corsConfiguration } from "../../middleware/cors";

export default (app) => {
  const server = new ApolloServer({ [...] });

  server.applyMiddleware({
    cors: corsConfiguration,
    app,
    path: "/api/graphql",
  });
};

With all of that set, finally, the part that plugs our Apollo Server into our existing Express.js server is the server.applyMiddleware() method at the bottom of our exported function. This takes in three properties:

  • cors which describes the CORS configuration and permissions for what domains are allowed to access the GraphQL server.
  • app which represents our existing Express.js server.
  • path which describes at what URL in our existing Express.js server the GraphQL server will be accessible.

For the cors property, we utilize the CORS middleware that's included with the Node.js boilerplate we're using (we'll look at this in detail in the next section). For the path, we specify that our GraphQL server will be attached to our running server (started on port 5001 by running npm run dev from the root of the project) at the path /api/graphql. In other words, instead of the http://localhost:3000/graphql path we saw earlier, now, we're "piggybacking" on the existing Express.js server and making our GraphQL server accessible on that server's port (5001) at http://localhost:5001/api/graphql.

The end result is effectively the same—we get a running GraphQL server via Apollo Server—but we do not spin up another HTTP server on a new port.

Handling CORS issues when connecting via external clients

Finally, one last detail we need to cover is CORS configuration. Like we saw in the previous section, we're relying on the cors middleware included in the Node.js boilerplate we've used throughout this tutorial. Let's open up that file in the boilerplate and explain how it impacts our GraphQL server:

/middleware/cors.js

import cors from "cors";
import settings from "../lib/settings";

const urlsAllowedToAccess =
  Object.entries(settings.urls || {}).map(([key, value]) => value) || [];

export const configuration = {
  credentials: true,
  origin: function (origin, callback) {
    if (!origin || urlsAllowedToAccess.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error(`${origin} not permitted by CORS policy.`));
    }
  },
};

export default (req, res, next) => {
  return cors(configuration)(req, res, next);
};

This looks more threatening than it is. To cut to the chase, the end goal here is to tell the browser's CORS check (CORS stands for cross origin resource sharing and defines which URLs can access a server) whether or not the URL its request is being made from (e.g., an app we're running at http://myapp.com) can access our GraphQL server.

settings-development.json

{
  [...]
  "urls": {
    "api": "http://localhost:5001",
    "app": "http://localhost:5000"
  }
}

That request's access is controlled via the urls list included in the settings-<env>.json file at the root of the project. That setting contains an array of URLs that are allowed to access the server. In this example, we want the same URLs allowed to access our existing Express.js server to access our GraphQL server.

Here, http://localhost:5001 is the server itself (meaning it can make requests back to itself, if necessary) and http://localhost:5000 is our front-end, customer-facing app (we use localhost:5000 because that's the default port CheatCode's Next.js Boilerplate runs on).

Wrapping up

In this tutorial, we learned how to set up a GraphQL server using the apollo-server package using two methods: defining a server as a standalone GraphQL server and attaching a GraphQL server to an existing HTTP server (in this case, an Express.js server).

We also learned how to set up a basic GraphQL schema and attach that to our server as well as how to define a custom context for our resolvers to handle things like authentication from within our GraphQL server.

Finally, we took a look at CORS configuration and made some sense of how to control access to our GraphQL server when attaching it to an existing server.

Written By
Ryan Glover

Ryan Glover

CEO/CTO @ CheatCode