MuleSoft announces solutions to build a future-ready foundation for AI.
Skip to main content
Contact Us 1-800-596-4880

 

Introduction to Designing, Launching and Querying a GraphQL API

 

This is Part 3 of the ProgrammableWeb API University Guide to GraphQL: Understanding, Building and Using GraphQL APIs

In the previous two installments of this series (see Part 1: What is GraphQL and How Did It Evolve From REST and Other API Technologies and Part 2: GraphQL APIs for Everyone: An In-Depth Tutorial on How GraphQL Works and Why It's Special), we talked about how GraphQL emerged from the IT landscape. We also provided an overview of how to use the GraphQL query language against a GraphQL API. For you API providers out there, now it's time to get down to the nitty-gritty and show you the work that goes into creating and providing a fully functional GraphQL API. In this installment, we're going to examine not only how to define the data types for the API, but also how to program the behaviors that allow your users to get, add, update and delete data. In addition, you'll learn how to put a layer of security authentication on the API as well as how to construct asynchronous connections in GraphQL using subscriptions. Finally, well cover pagination over large datasets and how to create and work with GraphQL directives to decorate a GraphQL query for special behavior.

Before we move into the concept and programming behind creating a GraphQL API, let's answer a fundamental question, why Apollo Server?

 

Moving From Specification to Implementation with Apollo Server

 

The important thing to remember about GraphQL is that it's an open, royalty-free specification that is still under Facebook copyright. All the spec does is describe what GraphQL is supposed to do, not how it's supposed to do it. Thus, any person, group or company with the time, expertise and inclination to implement the GraphQL spec can do so. However, the better way to get up and running with a GraphQL API is to use one of the frameworks that have already been created by organizations with expertise in the technology. Apollo is one such organization.

Apollo is a commercial company that provides services and products for the GraphQL ecosystem. One of these products is Apollo Server.

Apollo Server is an implementation of the GraphQL specification for Node.js. Apollo Server abstracts away much of the complexities of programming a GraphQL API thus allowing developers to focus more on the logic and features of their GraphQL API and less on the low-level coding required to get an API up and running. While Apollo Server does not eliminate all the complexity that goes with creating a GraphQL API, it does make life easier by segmenting programming to particular areas of concern, which we'll demonstrate throughout this article.

There are some things you've got to know about the way Apollo Server "wires up" GraphQL in order to create an API. Apollo Server is a particular way of wiring up types, resolvers, subscriptions, and the basic operation types. We'll cover the details in moment. But first let's review the basics of a data graph. The concepts are essential to working with GraphQL.

 

Understanding Data Graphs

 

In order to work effectively with GraphQL you need to understand the concept of a data graph. Whereas with more traditional approaches to data management in which information is organized in tabular rows and columns typical of a relational database such as MySQL or in the document-centric way found in NoSQL technologies such as MongoDB, GraphQL considers the data graph to be the foundational way to represent data for publication. The concept behind the data graph is that data exists as distinct entities within a domain, with an entity being structured according to properties (aka, attributes or fields). In addition, an entity can have none, one or many relationships to other entities.

In discrete mathematics, the discipline that gave birth to the concept of the data graph, such entities, are referred to as nodes. The relationship between two nodes is called an edge. The demonstration application that accompanies this article captures a number of edges that can exist between nodes. (See Figure 1, below.)

In a data graph, a node is an entity (e.g., Person) and an edge is a relationship (e.g., likes).

Figure 1: In a data graph, an entity such as Person is called a node and the relationship between two nodes; for example, likes, is called an edge

Many GraphQL APIs refer to an edge as a connection. Describing an edge as a connection is a convention that evolved among the GraphQL community. For example, the demonstration application that accompanies describes a likes relationship as a likesConnection.

The edge that is default in real-world implementations of GraphQL is, has. The IMBOB API manages and presents information related to people, movies and actors. The custom object types defined in the demonstration application are movie, director, actor, and role. Figure 2 below shows the "has" relationships between various entities — a movie has directors, a director has movies, on so on.

Figure 2: The default relationship between nodes in a GraphQL API is, has

Figure 2: The default relationship between nodes in a GraphQL API is, has

The reason that it's important to understand that the "has" relationship is because it's the basis for the parent-child pattern that is typically expressed in many GraphQL APIs. Many developers who are new to GraphQL define the object graph solely in terms of the "has" edge. This is good and useful as a starting place. But, the more advanced implementations of GraphQL will move beyond has to more sophisticated edges named according to the connections convention described above.

Now that we've covered the basic concept of the data graph and how it's associated with a GraphQL API implementation, let's move onto actually creating a GraphQL API server and a GraphQL subscription server using Apollo Server 2.0.

 

Anatomy of the API Server

 

Implementing a GraphQL API using Apollo Server 2.0 involves a few steps. These steps are

  • Define the types that data graph will support
  • Define the queries and mutations by which users will perform CRUD operations
  • Define the resolvers that will provide behavior for the queries and mutations
  • Define the subscription to which users will register and listen for event messages
  • Create the schema that uses the types, resolvers and subscriptions created previously
  • Add a context definition to the schema in order to support access authentication

The details of the steps will be provided in the sections that follow.

Defining the API Types in a Typedefs File

The mentioned earlier and in the previous installment of this series, the way that GraphQL describes data structures in an API is according to a type system. You can think of a type as an object similar to those used in Object Oriented Programming. Not only does the IMBOB demonstration API use types to describe custom data structures such as movie, person and actor, types are also used to describe the basic GraphQL operations, Query, Mutation, and Subscription.

While the GraphQL specifies how a type is to be structured, they way types are implemented in an API varies by framework. One technique you can use when implementing types in Apollo Server 2.0 is to declare the types in a Javascript module file that exports the type declaration as a string in GraphQL Schema Definition Language. Then, the types definition string is assigned to the required variable typeDefs and passed onto the constructor of the schema object that will be consumed during the creation of the actual GraphQL API server.

Excerpt from typedefs.js describing demo app types in GraphQL Schema Definition Language (SDL).

Listing 1: A an excerpt from the file, typedefs.js that describes some of the demonstration application types declared in GraphQL Schema Definition Language (SDL)

Implementing Inheritance in GraphQL Using interface and extend

As you can see from the excerpt in Listing 1 and from the entire contents of the typedefs.js file, IMBOB uses the interface keyword to implement interface inheritance when creating a type. (Support for interfaces is a standard feature of the GraphQL specification.) Also, IMBOB uses the extend keyword defined by the GraphQL specification and supported by Apollo Server to implement object inheritance directly.

Figure 3 below shows the types and inheritance patterns defined in the IMBOB API in a dialect of the Unified Model Language. For example, notice that both the types, Person and Actor implement the Personable interface. Also, the types Movie and Cartoon implement the Movieable interface, yet Cartoon adds the field animators, which make sense. It's quite permissible to add fields not defined in an interface to a type that implements that interface.

Description of GraphQL interfaces, types, and inputTypes in UML (Unified Modeling Language).

Figure 3: A description of some of the GraphQL interfaces, types and inputTypes represented in UML (Unified Modeling Language)

Notice that in Figure 3 the IMBOB adds the fields, marriedToConnection, divorcedFromConnection, knowsConnection, and likeConnection to the Person type by using the extend keyword. This is an example of type inheritance implemented according to the GraphQL specification.

The purpose of the type CursorPaginationInput is to pass pagination information to the API so that it can retrieve a specific segment of items from a much longer collection. (We'll talk about supporting pagination later in this article.) PersonConnection is the return type that represents how IMBOB describes relationships (edges) between persons.

Defining custom types is an important part of creating a GraphQL API. Types represent the information available in the API.

In addition to creating custom types to represent entities that are special to IMBOB, we also need to define the Query and Mutation types that are required to do CRUD operations. CRUD (create, read, update, delete) operations.

Defining Queries, Mutations and Subscriptions

The way that GraphQL specifies implementing CRUD operations is to define two types; Query and Mutation. These are commonly known as GraphQL's basic operation types. The fields on the type Query define "read" operations. The fields defined in the Mutation type define create, update and delete operations. The IMBOB application defines Query and Mutation types along with the custom type definitions in the file typedefs.js as shown below in Listing 2.

Listing 2: The Query, Mutation and Subscription types are basic for any GraphQL Schema

Listing 2: The Query, Mutation and Subscription types are basic for any GraphQL Schema

Working with Types and Resolvers

In order to provide behavior for Query, Mutation, and Subscription types, a GraphQL API must define analogous resolvers that correspond to each field in the given type. For example, the field, movies at line 16, in Listing 2 above is the logical name that describes the query to get movies from the API.

Figure 4 below, shows the query, movies defined in the Query section of the IMBOB type definition file, typedefs.js. Notice that there is also a corresponding object, Query in a separate Resolver file, resolvers.js. The Query resolver contains a field, movies that maps to a Javascript function that contains the logic for getting movie information from the API's data storage resource. This is the behavior that gets executed when a user runs the query, movies against the IMBOB API.

The movies query in the demo app is backed by a corresponding resolver providing its logic.

Figure 4: The demonstration application's movies: [Movie] query is backed by logic provided by an analogous resolver with the same name

The query-to-resolver mapping pattern is fundamental to the GraphQL specification. It's supported by all GraphQL implementations including Apollo Server. In fact, a resolver object is required when creating the schema that gets passed as a parameter when constructing an Apollo Server. (this exact concept will be covered in the section, Creating the API and Subscription Servers that appears later in this article.)

Once Query and Mutation types and their associated resolvers are declared, we need to define the Subscription type that allows IMBOB to emit messages asynchronously to registered listeners.
 

 

Adding Events and Messaging Using Subscriptions

 

The typical API pattern whereby an API-consuming client "calls" an API is incredibly inefficient when the data it is interested in is constantly changing (like a stock-ticker). In order for the client to keep up with the changes, it must constantly poll the API and then execute some business logic on the result to see if something has changed. It's a waste of both compute and networking resources.

The more efficient way to handle such "evented" workflows (workflows that are triggered by the occurrence of an event, like a change in stock price) is for the client to tell the API server that it wants to be notified when such an event has happened. In broader terms, this idea of pushing or streaming updates to a client is sometimes called publish and subscribe or pubsub. The API server publishes messages to certain topics to which clients can subscribe.

Unfortunately, with the REST architectural style for APIs, there is no built-in mechanism for publishing or subscribing to topics. As a result, there are several bolt-on approaches including webhooks and websockets that REST API providers turn to in order to offer such "evented APIs."

With GraphQL however, the idea of evented APIs is built-in (though the semantics of this built-in approach are a bit unintuitive at first). In order for a client to register interest in an event-driven topic, the server must first make that topic available as a GraphQL "subscription." A client can then register its interest with that GraphQL subscription, or, at the API provider's option, the client can indicate interest in a more granular level of that subscription known as a "channel."

In either case, once the client has registered its interest with the API server, the API server creates a queue that's specific to that client. Once an event fires (ie: a change in stock price), the queue is stuffed with the details of the event. Those details could be a simple reflection of the event that occurred (ie: stock ticker and its new price) or a processed version of the event (ie: the stock ticker, the new price, and its net impact on the value of the end-user's stock portfolio, etc). Then, it's the client's responsibility to see if a new event is waiting for it in its queue.

The IMBOB demonstration application provides a number of subscriptions available for clients.

One such subscription is onMovieAdded(). A user looking to get real-time updates from the onMovieAdded() subscription would use the following query in GraphQL query language to register with the subscription:

subscription onMovieAdded{
  onMovieAdded(channelName:"MOVIE_CHANNEL"){
    id
    name
    createdAt
    storedAt
    body
  }
}

The user is then "wired" into the API and will receive messages from the API as each movie is added. The message will adhere to the format described by a custom type. In the case of IMBOB the custom type is, Event which is declared like so:

"""
Event is a type that describes messages emitted
from a subscription.
"""
type Event {
   id: ID
   name: String
   createdAt: Date
   storedAt: Date
   body: String
}

That the name of the type representing a message is Event is incidental. Type naming for messages is arbitrary. Neither "Event" nor "event" are reserved words in GraphQL. We could have just as easily called the response message, OnMovieAddMessage. The reason that the generic name Event is used in IMBOB is to have a single type to describe messages emitted by the IMBOB API. This is special to IMBOB. Other APIs might have a different structure for the messages they return.

Figure 5 below shows the way to subscribe to, onMovieAdded within the GraphQL Playground.

Figure 5: Subscribing to onMovieAdded events

Figure 5: Subscribing to onMovieAdded events

To demonstrate how the act of adding a new movie can trigger the sort of event that a subscription is listening for, listing 3 below is an example of a mutation to add a movie to the IMBOB API using the GraphQL query language. In the real world, a mutation is likely to be coded with variable names as opposed to the actual data as shown below (for tutorial readability and demonstration purposes).

mutation{
  addMovie(movie:{title:"The Great Escape", 
                  releaseDate:"1963-07-04"}) {
    id
    title
  }
}

Listing 3: The mutation for adding a movie to IMBOB

Figure 6 below shows the actual mutation for addMovie executed in GraphQL Playground.

 Figure 6: The mutation, addMovie is declared to return the fields, id and title upon success.

Figure 6: The mutation, addMovie is declared to return the fields, id and title upon success.

The right panel highlighted with a red border, in the GraphQL Playground screenshot in Figure 7 below shows that a message was emitted out of the API upon a movie being added to the system, as shown above in Figure 6.

Figure 7: UI displays API-emitted messages on movie additions post subscription to onMovieAdded

Figure 7: Once a user subscribes to onMovieAdded, the UI will display messages emitted by the API for the subscription when a movie is added to the system.

Notice that in Figure 7 the Event message fired by onMovieAdded (as shown in the right panel highlighted by a red border) contains the fields id, name, createdAt,storedAt and body. These are the "fields to return" that were defined when we registered to the subscription. (See the left panel in Figure 7 above.)

The important thing to understand is that GraphQL in general and IMBOB in particular supports subscriptions. A user registers to a subscription then continuously listens for messages coming out of the API in response to a query or a mutation. Connection to the subscription is constant until the user disconnects. (You can disconnect from a subscription in the GraphQL Playground by closing the tab in which the subscription registration was made.)

Now that we've covered subscriptions at the user level. Let's look at how to program subscriptions under Apollo Server.

Defining a Subscription

As mentioned at the beginning of this article, a Subscription is one of the basic operations types specified by GraphQL. They are defined in somewhat the same manner that queries or mutations are implemented. You define the type Subscription in the typedefs.js file. Then, apply fields to the type that was just defined, with each field representing a particular subscription. However, unlike queries and mutations in which you program resolvers to provide CRUD behavior, subscriptions are a bit different. With a subscription you are going to use the PubSub package that ships with Apollo Server to emit a message from resolver behavior that's associated with a particular query or mutation. For example, we'll publish a message to subscribers of onMovieAdded from within the resolver, addMovie().

Granted, this dynamic of implementing subscription behavior by publishing from within a resolver method can be a bit confusing. It can take time to develop a clear understanding of the technique, particularly if you are new to message driven architectures. Working with a concrete example makes things easier to understand.

Let's start by taking a look at how IMBOB implements subscriptions.

The first place to start is by looking at how IMBOB creates a Subscription type. Figure 8 below shows the definition of the type Subscription. Each field in the type names a particular subscription, The subscriptions of interest for this discussion are:

  • onMovieAdded(channelName: String): Event
  • onMovieUpdated(channelName: String): Event

These fields are highlighted in bold in the figure below.

Figure 8: The onMoviedAdded and onMovieUpdated Subscriptions returns the custom defined Event object

Figure 8: The onMoviedAdded and onMovieUpdated Subscriptions returns the custom defined Event object

Let's examine the details of the field, onMovieAdded.

onMovideAdded(channelName: MovieChannelName): Event

The declaration of onMovieAdded has 3 parts.

onMovideAdded is the name of the subscription

channelName defines an associated channel that can have a value from the custom defined GraphQL enum, MovieChannelName, (see Listing 2.)

onMovideAdded is the name of the subscription

Event is the particular return type (based on how it was defined earlier). It is the message emitted by the subscription

The following describes each part.

Declaring a Channel

channelName: MovieChannelName is a parameter that indicates a channel that is supported by the subscription. You can think of a channel as a named stream from which messages flow. Channel names are defined in a GraphQL enum, MovieChannelName, that is predefined by the IMBOB API and is special to the API. A user can declare a channel when registering to a subscription. Then, not only will the user receive messages particular to the subscription, but also particular to the channel. In other words, channels add a finer grain to message emission for a particular subscription.

Figure 9 below shows an excerpt from the IMBOB API documentation published by the Apollo Server GraphQL Playground. The excerpt describes the subscription, onMovieAdded. (Remember, one of the nice things about GraphQL is that it is self-documenting. By default all type, query, mutation, and subscription information is published automatically via GraphQL introspection. And, when you comment the type definitions and fields using three double quotes (""") to open and close a comment, that comment will appear in the documentation too.)

Apollo Server's GraphQL Playground uses GraphQL's introspection to automate online API documentation

Figure 9: Apollo Server's GraphQL Playground takes advantage of the introspection feature of GraphQL to automate online documentation for an API.

Notice that the documentation shown above in Figure 9, indicates that onMovieAdded has the parameter channelName that can be used to bind the subscription registration to a particular channel. As you can see in the documentation, IMBOB supports four channels: MOVIE_CHANNEL, HORROR_MOVIE_CHANNEL, DRAMA_MOVIE_CHANNEL, COMEDY_MOVIE_CHANNEL. These channels are special to the IMBOB API.

Different API's will have their own channels and related conditions by which messages will be emitted. In the case of IMBOB, messages are emitted according to the genre of the movie in question. If no channel is declared, the subscriber will receive all messages published to a particular subscription, regardless of the channel.

Declaring a Message Type

The aforementioned custom type Event describes the structure of the message that will be emitted by a subscription running under IMBOB. Figure 10 below shows an excerpt of the GraphQL documentation for the type Event.

Custom Event object details data for onMovieAdded and onMovieUpdated Subscriptions listeners.

Figure 10: The custom Event object describes the data sent to listeners of the onMovieAdded and onMovieUpdated Subscriptions

Figure 10 above not only shows the documentation that describes the fields that are part of the Event type, but also show the details of the particular field body.

The important thing to understand about subscriptions and the message type (also known as return type) is that when you define a subscription in the typeDef file, you need to also declare the type of the message the the subscription will emit. As mentioned above, different APIs will emit different types that satisfy the purpose of the API. In the case of IMBOB, there is but one message type at this time: Event.

Now that we've covered how to define a subscription in the file typedefs.js, let's examine the way IMBOB emits a message in response to behavior in a particular resolver.

Publishing a Subscription Message

Listing 4, below shows the code for the resolver, addMovie which is associated with the mutation, addMovie that is defined in the type definition file of IMBOB, typedefs.js. The file that contains this resolver code is resolver.js.

The resolver addMovie adds movie information to the IMBOB data store and publishes information.

Listing 4: The resolver, addMovie adds movie information to the IMBOB data store and also publishes information.

Notice that addMovie is bound to an asynchronous Javascript function (running under Node.js). The Javascript function declares two parameters, parent and args. The function expects the values for these parameters to be passed into the resolver by Apollo Server when the mutation, addMovie is called. Passing these parameters into the resolver is part of the "automatic wiring" that Apollo Server provides.

The parameter parent indicates the parent object related to the resolver. (We'll have a general discussion of the parent parameter at another time. In this case, there is no parent in play with addMovie.) The parameter args indicates the parameter information that gets passed by the caller of the mutation. In IMBOB the type definition for the mutation addMovie is as follows:

addMovie(movie: MovieInput!): Movie

Thus, an instance of type MovieInput is passed by Apollo Server to the args parameter of the resolver.

The type, MovieInput, is defined like so:

input MovieInput{
  title: String!
  releaseDate: Date!
  genre: Genre
  directors: [KnownPersonInput]
  actors: [ActorInput]
}

Where, as indicated by the exclamation point, the fields title and releaseDate are required to be provided when executing the mutation. The other fields in MovieInput are optional.

Since the movie to be added is new to the system, a unique identifier must be created by the API and added to the MovieInput object that is bound to the args parameter. Assigning the unique identifier is done at line 2 in Listing 4 above.

Once the MovieInput object is given a unique identifier, it can be added to the IMBOB data store, which is done at line 3 of Listing 4. Then, after the movie as been added to data store successfully, IMBOB publishes a message to any subscribers interested in knowing that a movie has been added to the system. This is done at line 4 in Listing 3 above, when executing the publishMovieEvent(MOVIE_EVENT_TYPE_ADD, movie) function. This is where all the action happens. So let's take a look at the details in Listing 5 below.

The method publishMovieEvent sends messages to subscribers listening to movie-related subscription

Listing 5: The method, publishMovieEvent does the work of sending a message to subscribers listening to subscriptions related to movie activity

In the method, publishMovieEvent the actual publishing of a message happens at line 29 in Listing 4.

Lines 11 - 26 in Listing 4 configures the message that's going to be published. Let's take a look at the details.

The IMBOB API function, publishMovieEvent has two parameters, eventType and movie. The parameter, eventType describes a constant value that's relevant only to the internals of IMBOB. The eventType indicates whether the message will be published to the subscription onMovieAdded or onMovieUpdated. The parameter, movie contains the movie information that will be assigned to the body of the message. As you can read in the code comments, line 8 sets the default subscription channel to MOVIE_CHANNEL. Then in lines 11 -15, if the movie object has a genre field, the channel is reassigned a value accordingly. Line 18 calls the custom method, createEvent which is a factory method that creates an Event object. (You can view the code for createEvent in Listing 5 below.)

Listing 5: createEvent is a factory method that creates Event objects in a uniform manner.

Listing 5: createEvent is a factory method that creates Event objects in a uniform manner.

Going back to Listing 4, once everything is set up, it's time to create the object, subscriptionDefinitionObject starting at line 24 in Listing 4. The object, subscriptionDefinitionObject fulfills a need that is particular to the Apollo Server PubSub.publish() method. PubSub.publish() has two parameters like so:

PubSub.publish(channelName, subscriptionDefinitionObject)

The parameter, channelName, indicates the subscription channel to which to send the message. The parameter, subscriptionDefinitionObject describes the actual named subscription along with the message that will be sent.

(Please be advised that subscriptionDefinitionObject is a term created in this article to describe the JSON structure that Apollo Server PubSub requires in order to send messages to subscribers. The concepts relevant to subscriptionDefinitionObject are similar to explanations found in other GraphQL documentation. But, as of this writing there is no standard term offered by Apollo Server for naming the required JSON structure.)

In order to understand, subscriptionDefinitionObject, you need to understand its JSON structure. This structure is special to the way that Apollo Server's PubSub object published messages.

Conceptually, the structure of the subscriptionDefinitionObject is:

{subscriptionName: {payloadField_1: info, payloadField_2: info, payloadField_X: info}}

WHERE

subscriptionName, is a field named according the exact subscription of interest

{payloadField_1: info, payloadField_2: info, payloadField_X: info}, is a JSON object that contains the fields that describe the particular segments of the payload overall.

Thus, concretely, if we want to send a message to the onMovieAdded subscription, we need to create an subscriptionDefinitionObject on the server side that might look like this:

{
  "onMovieAdded": {
    "id": "4ad5e551-69d9-4af5-8a21-782f0b6ac57a",
    "name": "MOVIE_EVENT_TYPE_ADD",
    "createdAt": "Wed May 01 2019 12:01:48 GMT-0700 (PDT)",
    "storedAt": "Wed May 01 2019 12:01:48 GMT-0700 (PDT)",
    "body": {
        "title": "The Great Escape",
        "releaseDate": "1963-07-04",
        "id": "0268fcca-8687-43fe-ba51-62455103d45b"
      }
  }
}

WHERE

"onMovieAdded" is the field name that is the actual name of the subscription

Then, once Apollo Server publishes the message, the following JSON would get sent to listeners to the subscription:

{
  "data": {
    "onMovieAdded": {
      "id": "4ad5e551-69d9-4af5-8a21-782f0b6ac57a",
      "name": "MOVIE_EVENT_TYPE_ADD",
      "createdAt": "Wed May 01 2019 12:01:48 GMT-0700 (PDT)",
      "storedAt": "Wed May 01 2019 12:01:48 GMT-0700 (PDT)",
      "body": {
        "title": "The Great Escape",
        "releaseDate": "1963-07-04",
        "id": "0268fcca-8687-43fe-ba51-62455103d45b"
      }
    }
  }
}

These lines of code from Listing 4, at lines 25-26 above describe the actual work of adding a field name for a particular subscription to the subscriptionDefinitionObject and then assigning a message to that field

  //assign the event to the appropriate subscription
   if(MOVIE_EVENT_TYPE_ADD) subscriptionDefinitionObj.onMovieAdded = event;
   if(MOVIE_EVENT_TYPE_UPDATE) subscriptionDefinitionObj.onMovieUpdated = event;

Once the subscriptionDefinitionObject is created and configured, it is published by the Apollo Server PubSub object like so:

await pubsub.publish(channel, subscriptionDefinitionObj);

 

Adding Security

 

API security is a very complex issue to which there is no single approach or solution. The subject is worthy of an entire book all to itself. We therefore won't be offering an exhaustive, in-depth overview of all it takes to secure APIs. Instead, we'll cover one of the most common and important components of API security; user authentication.

Implementing authentication in the IMBOB GraphQL API is a twofold undertaking. We need to secure both the API and Subscription servers. There is a common task that applies to securing both in which code inspects security information that is submitted by the user as a part of the HTTP request. The location of this security information varies by API Server or Subscription Server. In terms of the API Server, the security information is located in the HTTP request captured in the context object that's passed to the API schema during construction.

In terms of the Subscription Server, the authentication information is provided in the connectionParams parameter object that's passed to a subscription's onConnect field. (Be advised, the subscription object associated with the onConnect field in not the same as the Subscription type defined in the typeDefs file.)

The techniques for inspecting HTTP requests to determine security information is shown in Figure 11, below.

Figure 11: The security overview for IMBOB

Figure 11: The security overview for IMBOB

Let's take a look at the details of each technique. First, we'll start with security the API server.

Authenticating with the API Server

A query or mutation that is executed against a GraphQL API is represented as a request in the API's context. Part of the work for creating a schema for an API server is to declare a context field in the schema object that will be passed onto the server during implementation of the API. You'll create an anonymous function with two parameters, req and res, and assign that function to the context field. The parameter, req represents the current request submitted. The parameter, res represents the HTTP response that will be returned to the calling client eventually after the query or mutation represented by the request is processed. (See Figure 12.)

Figure 12: The context field makes request information available for inspection and processing.

Figure 12: The context field makes request information available for inspection and processing.

The work of assigning the incoming request to the req parameter and managing the response, res, is handled by the internals of Apollo Server (but should be handled by the internals of other GraphQL implementations as well.)

One of the many ways to facilitate security authorization is to add authentication information to the headers of an HTTP request. With regard to providing access information to the IMBOB API, we add that information to the header of the request representing the given query or request. When using GraphQL Playground, we type the JSON {"authorization":"ch3ddarch33s3"} into the HTTP HEADERS pane of the UI as shown below in Figure 13.

Authenticate to an API in GraphQL Playground by submitting authorization in the HTTP HEADERS pane.

Figure 13: To authenticate to an API in GraphQL Playground, submit the authorization information in the HTTP HEADERS pane

Should we need to access the API in code, for example in a Node.js unit test, we would write:

graphQLClient = new GraphQLClient(serverConfig.serverUrl, {
   headers: {
       authorization: `${config.ACCESS_TOKEN}`,
   },
});

Where the value of config.ACCESS_TOKEN is displayed above in the upper right of Figure 11.

Once the request is submitted, Apollo Server makes the request headers available to the schema's context function by way of the req parameter. As you can see in Figure 11 above, the context function has the logic to examine the request and determine if the required authentication information is present. If it is, the request is processed. If not, an error is thrown.

Securing the Subscription Server

Securing the subscription server requires a different approach. Subscription connections are continuous using the websocket protocol. It's like plugging an electric appliance into a wall socket. Once the plug is inserted and the appliance is turned on, the electricity flows until the appliance is turned off or the plug is pulled.

In terms of the Apollo Subscription Server, security analysis is performed according to the functional logic assigned to the onConnect field of the subscription object that is passed to the schema's constructor.

Just as Apollo passes a request onto the schemas context function to provide authentication information to the API Server, for the Subscription Server, it passes header information to a parameter named, connectionParams in the function assigned to the onConnect field. (See Figure 14, below)

onConnect subscription field processes authentication info via connectionParams in Apollo Server.

Figure 14: The subscription field, onConnect allows you to inspect and process authentication information that Apollo Server will send to the onConnect handler, by way of parameter, connectionParams

The connectionParams object has a field, authorization, that will contain the information declared in the authorization, key-value header pair. Submitting authorization information to a subscription under GraphQL Playground is similar to the technique used when executing a query or mutation. Only, in terms of subscriptions, you provide the JSON-based authentication information when you register the subscription, as shown in Figure 15 below.

Figure 15: Authenticate subscriptions in GraphQL Playground using the HTTP HEADERS pane.

Figure 15: You provide authentication information in the HTTP HEADERS pane in GraphQL Playground when you register a subscription

Authenticating to the Subscription Server in code, for example when executing a unit test in the Mocha/Chai framework looks like the code shown in Listing 6 below.

Listing 6: The code for submitting authentication information to a GraphQL Subscription Server

Listing 6: The code for submitting authentication information to a GraphQL Subscription Server

Now that we've covered the concepts and techniques for providing basic security to both an API and Subscription Server running under Apollo, let's take a look at the steps required to actually get the servers up and running.

 

Creating the API and Subscription Servers

 

Listing 7 below shows the code from server.js of IMBOB. This the code that initializes both the API and Subscription servers. Overall the steps to create the server are to import the Apollo Server object that is part of the apollo-server NPM package that's downloaded via NPM install. Lines 4 - 8 create the objects that are needed by the schema. These objects will be used for both the API and Subscription Server. Lines 23 - 29 creates the schema, that contains the typeDefs, resolvers, and schemaDirectives objects the API requires. The schema is created by using the function, makeExecutableSchema() which is part of the graphql-tools library.

Line 52 calls initGlobalDataSync(), which is a function special to IMBOB. This function does the work required to create the data layer for IMBOB and make it accessible by way of private helper functions which are used throughout the source code. (All IMBOB data is stored in files that are local to the API. This way the API carries its own data independent of any service or database making it easy to use and replenish. Please be advised that this technique is used for instructional purposes only. GraphQL API data management in the real world is a very complex topic. A production level implementation will store data in one of a variety of database technologies or through a separate storage API altogether.)

[[Listing 7]]

Listing 7: The Node.js code for defining a GraphQL schema and using it to launch the IMBOB API and Subscription servers

Line 31 in Listing 7, above, creates the actual server object that will create both the API and Subscription servers. As you can see, the schema and subscription objects that were created earlier get passed into the ApolloServer constructor as fields in constructor object starting at line 32. Also, the context field within the constructor object is assigned an anonymous Javascript function that provides authentication behavior.

Once the server is created, server.listen(PORT) is called at line 56 where PORT is the port number on which both the API server and Subscription server are listening for incoming data. The method server.listen() returns a Node.js promise. That result of the promise, which is a JSON object that contains the URL of the API server and the URL of the Subscription server is captured in a then clause. The then clause assigns the server URL and subscription URL to an environment variable, SERVER_CONFIG. Assigning the URLs to an environment variable makes them accessible globally throughout the application. Also, the then clause sends some startup information to the console. Line 64 exports the server. This is done to make both the API and Subscription servers available for instantiation in the IMBOB unit tests.

 

Using Pagination

 

In the real world, most clients using GraphQL will be working with arrays of data that are very big. For example, imagine that you're Facebook and you want to get a list of all items relevant to a particular user's news feed. This list can contain tens of thousands of items and keeps growing perpetually. Getting the entire list in one call is impractical.

The alternative is to get the information you need in chunks, from a particular starting point. This technique is called pagination. To better understand pagination, let's look at this example. Imagine that we have a list with one hundred items. Your client code sends a JSON object that's configured to say to a GraphQL API query, "get me 10 items from the list starting at Item 0." The API responds with 10 items, staring at point zero, the start of the list. Also, the GraphQL query returns as part of its response a piece of information that says, "The next item in the list in Item 10." Thus, when you make the query again, you tell the API, "get me 10 items from the list starting at Item 10." You use the technique to get back 10 items at a time, moving your "cursor' to the next starting position in the list as reported in the last query's response. This is how pagination works. You tell the query the starting point and the number of items to return. Then, the query acts accordingly.

Pagination is a technique that is used in a variety of APIs and database technologies. It's been around for a while. Pagination can be used under GraphQL, but it is not a built in part of the specification. Thus, when creating a GraphQL API using Apollo Server, you need to create your own pagination mechanisms.

IMBOB does support pagination through custom coding. The way pagination information is passed to an IMBOB API query is by way of the GraphQL Input Type, custom to IMBOB, CursorPaginationInput.

Figure 16 below, shows the IMBOB documentation for the query, persons(). Notice that query has a parameter paginationSpec of type, CursorPaginationInput. CursorPaginationInput is the custom object that contains the information the query needs to support pagination. CursorPaginationInput tells the query the starting point and the number of items to return.

IMBOB defines the Input type, CursorPaginationInput, for efficient pagination with large datasets.

Figure 16: IMBOB defines the Input type, CursorPaginationInput to facilitate using pagination with a very large list of data.

Listing 8 below shows an example of a paginated query executed against the IMBOB API. The query is persons. Notice that the pagination information defined by CursorPaginationInput is assigned to the query parameter, paginationSpec at Listing 8, line 2. While other IMBOB queries we've seen previously in this article define return-fields such as title, releaseDate, directors, and actors, the query persons takes a different approach. Notice that persons declare two fields to return, pageInfo and collection. The field, collection has the subordinate fields, firstName, lastName, and id. This might seem strange, but there is a reason.

Listing 8: Query using CursorPaginationInput for pagination, assigned to paginationSpec parameter.

Listing 8: A query that defined pagination information using the CursorPaginationInput type and assigns that information to the query parameter, paginationSpec.

The reason why the firstName, lastName, and id is declared as a subordinate to the field, collection is revealed in Listing 9, below. Notice that the type, persons, has two fields. One field is collection, which is an array of Person objects. This is essentially the information we want. The other field, pageInfo describes the pagination state. Thus, the type, Persons contains pagination state information as well as an array of Person objects sized according to the configuration information in the paginationSpec parameter that's passed to the persons query.

persons (paginationSpec: CursorPaginationInput): Persons
type Persons {
   collection: [Person]
   pageInfo: PageInfo!
}
type PageInfo {
   endCursor: String
   hasNextPage: Boolean
}

Listing 9: The query persons, returns not only an array of Person objects, but also a PageInfo object that describes the current state of pagination activity

Notice above in Listing 9 that the type PageInfo has a field endCursor. The field endCursor indicates the value of the unique identifier of the last item returned in the array of Person objects. When a client wants to requery the API for more data, the endCursor unique identifier value will be assigned to the field, CursorPaginationInput.after, as shown above in Listing 8. Effectively, we're telling the query to get the next X number of items starting after the person who has the unique defined by endCursor.

Now, the very important thing to understand here is that the pagination structures used in IMBOB are special to this API. Other APIs will implement pagination in ways to meet their particular needs. However, what is true in general for all APIs is that pagination information must be exchanged between query and response. There is no magic. Each API will have a special way, using special types to implement pagination. But in terms of a GraphQL query, pagination is not automatic. Pagination control must be deliberately included as part of the APIs logic.

Listing 10 below shows two sets of queries and responses using pagination for the query, persons. The first query defines only the value of CursorPaginationInput.first, with no value for CursorPaginationInput.after. By default IMBOB will start at the first item in the list, as is special to the default query logic programmed into this API. The second query uses the additional details in the pageinationSpec definition and returns items accordingly. These additional details are extracted from the pageInfo field.

Query 1
{
                                                                                                                                                                                                                                                                                                                                  persons(paginationSpec:{first:3, sortFieldName:"lastName"}){
                                                                                                                                                                                                                                                                                                                                    pageInfo{
                                                                                                                                                                                                                                                                                                                                      endCursor
                                                                                                                                                                                                                                                                                                                                    }
                                                                                                                                                                                                                                                                                                                                    collection{
                                                                                                                                                                                                                                                                                                                                      firstName
                                                                                                                                                                                                                                                                                                                                      lastName
                                                                                                                                                                                                                                                                                                                                      id
                                                                                                                                                                                                                                                                                                                                    }
                                                                                                                                                                                                                                                                                                                                  }
Result 1
{
                                                                                                                                                                                                                                                                                                                                  "data": {
                                                                                                                                                                                                                                                                                                                                    "persons": {
                                                                                                                                                                                                                                                                                                                                      "pageInfo": {
                                                                                                                                                                                                                                                                                                                                        "endCursor": "7f1e7daf-376b-4a0d-937f-2d2d09c290f6"
                                                                                                                                                                                                                                                                                                                                      },
                                                                                                                                                                                                                                                                                                                                      "collection": [
                                                                                                                                                                                                                                                                                                                                        {
                                                                                                                                                                                                                                                                                                                                          "firstName": "Ben",
                                                                                                                                                                                                                                                                                                                                          "lastName": "Affleck",
                                                                                                                                                                                                                                                                                                                                          "id": "b026dd59-98e1-46ac-9ade-5d8794265599"
                                                                                                                                                                                                                                                                                                                                        },
                                                                                                                                                                                                                                                                                                                                        {
                                                                                                                                                                                                                                                                                                                                          "firstName": "Casey",
                                                                                                                                                                                                                                                                                                                                          "lastName": "Affleck",
                                                                                                                                                                                                                                                                                                                                          "id": "68db2026-1d6c-4e27-abdb-43994d517513"
                                                                                                                                                                                                                                                                                                                                        },
                                                                                                                                                                                                                                                                                                                                        {
                                                                                                                                                                                                                                                                                                                                          "firstName": "Roslyn",
                                                                                                                                                                                                                                                                                                                                          "lastName": "Armstrong",
                                                                                                                                                                                                                                                                                                                                          "id": "7f1e7daf-376b-4a0d-937f-2d2d09c290f6"
                                                                                                                                                                                                                                                                                                                                        }
                                                                                                                                                                                                                                                                                                                                      ]
                                                                                                                                                                                                                                                                                                                                    }
                                                                                                                                                                                                                                                                                                                                  }
                                                                                                                                                                                                                                                                                                                                }
Query 2
        
                                                                                                                                                                                                                                                                                                                                {
                                                                                                                                                                                                                                                                                                                                  persons(paginationSpec:{after:"7f1e7daf-376b-4a0d-937f-2d2d09c290f6",
                                                                                                                                                                                                                                                                                                                                          first:3, 
                                                                                                                                                                                                                                                                                                                                          sortFieldName:"lastName"}){
                                                                                                                                                                                                                                                                                                                                    pageInfo{
                                                                                                                                                                                                                                                                                                                                      endCursor
                                                                                                                                                                                                                                                                                                                                    }
                                                                                                                                                                                                                                                                                                                                    collection{
                                                                                                                                                                                                                                                                                                                                      firstName
                                                                                                                                                                                                                                                                                                                                      lastName
                                                                                                                                                                                                                                                                                                                                      id
                                                                                                                                                                                                                                                                                                                                    }
                                                                                                                                                                                                                                                                                                                                  }
Result 2
{
                                                                                                                                                                                                                                                                                                                                  "data": {
                                                                                                                                                                                                                                                                                                                                    "persons": {
                                                                                                                                                                                                                                                                                                                                      "pageInfo": {
                                                                                                                                                                                                                                                                                                                                        "endCursor": "9eb66b6f-5871-4b53-8bcb-64e8ac7586b4"
                                                                                                                                                                                                                                                                                                                                      },
                                                                                                                                                                                                                                                                                                                                      "collection": [
                                                                                                                                                                                                                                                                                                                                        {
                                                                                                                                                                                                                                                                                                                                          "firstName": "Hal",
                                                                                                                                                                                                                                                                                                                                          "lastName": "Ashby",
                                                                                                                                                                                                                                                                                                                                          "id": "b5f0b9da-8ed1-4743-920e-f85d78a62131"
                                                                                                                                                                                                                                                                                                                                        },
                                                                                                                                                                                                                                                                                                                                        {
                                                                                                                                                                                                                                                                                                                                          "firstName": "Anthony",
                                                                                                                                                                                                                                                                                                                                          "lastName": "Asquith",
                                                                                                                                                                                                                                                                                                                                          "id": "ad382532-4d32-4bbe-b3ea-69213be4703a"
                                                                                                                                                                                                                                                                                                                                        },
                                                                                                                                                                                                                                                                                                                                        {
                                                                                                                                                                                                                                                                                                                                          "firstName": "Brando",
                                                                                                                                                                                                                                                                                                                                          "lastName": "Bartell",
                                                                                                                                                                                                                                                                                                                                          "id": "9eb66b6f-5871-4b53-8bcb-64e8ac7586b4"
                                                                                                                                                                                                                                                                                                                                        }
                                                                                                                                                                                                                                                                                                                                      ]
                                                                                                                                                                                                                                                                                                                                    }
                                                                                                                                                                                                                                                                                                                                  }
                                                                                                                                                                                                                                                                                                                                }

Listing 10: An example running the query, Persons multiple times using pagination

As you can see, pagination is a complex topic in GraphQL. The basic intention of this discussion is to explain that in order to support pagination, any API running under GraphQL will need to create a pagination structure that meets its particular need and that once the structure is determined, information exchange about the pagination state needs to be ongoing between query and response.

 

Using Directives to Validate Viewing Permissions

 

The last topic we're going to cover is how IMBOB supports GraphQL directives. A directive is a feature of GraphQL that allows users and developers to decorate code to alter operational behavior. Directives are similar in concept to attributes in C# and annotations in Java. You can think of a directive as way by which a developer can go into existing GraphQL type definition source code and "mark" it so that a rule is applied to a type or one of its fields. Of course, a developer must be actually program behavior for the defined directive somewhere in the API code.

A simple scenario for a directive is as follows. As you saw earlier in the IMBOB demonstration application there is a GraphQL type, person that's defined as follows:

type Person {
  id: ID
  firstName: String
  lastName: String
  dob: Date
  email: String
}

Imagine that one day the Head of Security at IMBOB decides that only users who have permission to access personal information of a person can view the email address of the person. Or, to put it another way, only users that have the special permission "PersonalScope" can view email information.

So, the security team at IMBOB goes through the IMBOB user system and identifies users entitled to "PersonalScope" and alters their profile accordingly.

However, there is still an outstanding problem: how to ensure that only users with "PersonalScope" can actually view personal information. To apply validation code in each resolver that uses a person type will be a laborious undertaking. It's much more efficient to have a single point of validation for all objects to ensure that only the right people can view email information. This is where a directive comes in.

A directive allows a developer to go into the person type in the IMBOB code base and make a single change, like so:

type Person {
  id: ID
  firstName: String
  lastName: String
  dob: Date
  email: String @requiresPersonalScope
}

That single change makes it so that any user who has "PersonalScope" can view the email information. Those that do not have "PersonalScope" will not be allowed to access to email information.

This is the theory. But, there is no magic. A developer will need to create the code that makes is so the directive @requiresPersonalScope actually does the work of enforcing the rule.

Creating the Directive

The first step to create the directive is for the developer to declare it within the GraphQL type system, like so:

directive @requiresPersonalScope on FIELD_DEFINITION

WHERE

directive is a reserved keyword under GraphQL

@requiresPersonalScope is the actual name of the directive

on FIELD_DEFINITION indicates that the directive is to assigned to a type's field at design time.

Once the directive has been declared within IMBOB GraphQL's type system, the developer needs to program the behavior that will enforce the rule the directive represents.

Programming the Directive

The way IMBOB implements the rule for @requiresPersonalScope is a technique that is special to Apollo Server. The developer creates a Javascript class, RequirePersonalScope whose behavior backs the rule the directive represents, This class will be assigned to the field requiresPersonalScope that is part of the schemaDirectives field of the schema used by Apollo Server, as shown below in Listing 11.

cconst schema = makeExecutableSchema({
   typeDefs,
   resolvers,
   schemaDirectives: {
       requiresPersonalScope: RequiresPersonalScope
   }
});

Listing 11: The directive requiresPersonalScope is declared as part of the schema definition.

The field requiresPersonalScope corresponds to the directive @requiresPersonalScope that was declared in the typedef file earlier. The fields in the schemaDirective object is the mechanism that binds the name of a directive to the actual directive behavior.

Once the directive requiresPersonalScope is defined, the developer needs to program the behavior for it. Listing 12 below, shows the code that's implemented for the @requiresPersonalScope behavior. The code is from the module, directives.js that is part of the IMBOB source on GitHub.

Listing 12: JavaScript class RequiresPersonalScope enforces @requiresPersonalScope directive.

Listing 12: The Javascript class RequiresPersonalScope that enforces the rule for the directive @requiresPersonalScope

Conceptually what's going in Listing 12 is that at runtime ApolloServer will "automatically" pass to RequiresPersonalScope any object that has a field that was marked at design-time with the directive @requiresPersonalScope. This "magic" is part of the internals of Apollo Server. The code not only knows about the marked field (in this case, email), but it also knows about the request that executed the query. The way that RequiresPersonalScope knows about the request is by inspecting the global context object also supplied by Apollo server. Context carries the request with it in either its req or request field as shown in the isValidToken function at line 52 of Listing 12 above. Once the request is known, the security access token can be inspected and the user can be determined. Then it's just a matter of determining if the user has "PersonalScope" as shown in Line 54 of Listing 12. If the user does indeed have PersonalScope, the value of the marked field is revealed as shown in Figure 17 below.

The IMBOB @requiresPersonalScope directive exposes sensitive info only to authorized users.

Figure 17: The IMBOB @requiresPersonalScope directive exposes sensitive information only to users who have the necessary permissions.

If the user does not have "PersonalScope, a "forbidden" message is assigned to the file value as shown in Line 44 of Listing 12. The "forbidden" message is then displayed as the field value instead of the sensitive information as shown below in Figure 18.

Figure 18: Users without the required permissions do not get to see sensitive information.

Figure 18: Users without the required permissions do not get to see sensitive information.

The beauty of implementing a security policy by using a directive such as P@requiresPersonalScope, or for that matter any other directive intended as a PFIELD_DESCRIPTION, is that they can be assigned to any field in the type system. It's a great efficiency. Instead of having to go through each resolver and implement code to support a rule, all the developer needs to do is define the directive behavior once in a single module and then apply it to fields that need to support that particular rule. It's a real benefit.

 

Putting it All Together

This has been a voluminous installment of the series. We really did cover things soup to nuts. We covered real implementations of types, queries, mutations, and resolvers. We also covered creating and using subscriptions to program asynchronous messaging out of the API in real time. We looked at how to implement authentication. Finally we wrapped up with a discussion of implementing pagination in GraphQL and using GraphQL directives.

In the next installment of this series we're going to step back from heads down work with GraphQL and take a higher level view of an important aspect of GraphQL. We're going to look at how GraphQL fits into the promise of the Semantic Web.

NextPart 4 -- How GraphQL Delivers on the Original Promise of the Semantic Web. Or Not.

Be sure to read the next GraphQL article: How GraphQL Delivers on the Original Promise of the Semantic Web. Or Not.