GraphQL

GraphQL is a declarative API query language that you can use to fetch data from a server. Facebook started its development in 2012 and open sourced it in 2015. Ever since a huge community and tooling ecosystem around GraphQL emerged.

Why GraphQL?

GraphQL is often considered to be a better alternative to REST, because…

  • it gives you exactly the data you asked for (no overfetching of data) and thus reduces the required amount of data being sent over the wire
  • You can avoid multiple REST calls. GraphQL contains all requested data in a single response.
  • GraphQL does not need versioning because you can extend your API in a backwards compatible way
  • Easy to learn and understand. Queries look very similar to the responses.
  • You can wrap GraphQL around existing APIs to create a single API interface
  • is language agnostic. You can use many languages to build a GraphQL server (yet in this article we will use JavaScript).

A quick overview on how GraphQL works

From your client application you send requests that are written in the specific GraphQL query language which uses queries to fetch data, mutations to create/update/delete data or subscriptions to observe data changes. More precisely, you request fields from an object on the backend. Request are commonly sent using a GraphQL client library such as Apollo or Relay to a single /graphql endpoint provided by your GraphQL server. That server is composed of a single Schema with many Types. A schema creates a strongly typed relationship between client and server. The GraphQL Execution Engine parses the client query and validates the schema, then each field that was sent to the server will get resolved to a scalar value using resolver functions. A resolver functions usually queries a database to “resolve to” a value, but it can be anything such as requests to others APIs. This “resolving” is done for each field in sequence and makes use of batched resolving, which effectively prevents the need to make additional requests for fields that have already been resolved. Finally, results are returned as a JSON response that conforms to the GraphQL schema.

Client sends queries, mutations or subscriptions:

query {
  book
}

mutation {
  addBook(title: "My book")
}

subscription PostFeed {
  postCreated {
    author
    comment
  }
}

Server receives client request and matches it against its schema:

type Query {
  book: Book
}

type Mutation {
  addBook(title: String!)
}

type Subscription {
  postCreated: Post
}

Schemas and Types

  • A schema is nothing else than the definition of your backend API
  • You use a schema definition language (SDL) to define your API endpoints as types with fields
  • There is only ever one schema per GraphQL service in a file usually called schema.graphql
// schema.graphql

type Book {
  title: String
  author: Author
}

type Author {
  name: String! # Can't return null because of "!"
  books: [Book] # A list of Books because of []
}
  • The schema is not responsible for defining where data comes from or how it’s stored
  • A schema also serves the purpose of self-documenting your API
  • Incoming queries are validated and executed against the schema
  • A schema shows the relations between your data endpoints

Supported types are

Scalar types

Scalar fields are fields that contain concrete data, they represent the leaves of the query. GraphQL comes with a set of default scalar types:

  • Int
  • Float
  • String
  • Boolean
  • ID (often used to refetch an object or as the key for a cache, can be serialized but is not intended to be human-readable)

We can also define a custom scalar type:

scalar Date

Then it’s up to our implementation to define how that type should be serialized, deserialized, and validated.

Object type

This is how to create a custom Object Type Book and Author:

type Book {
  title: String
  author: Author
}

type Author {
  name: String
  books: [Book]
}

Every object type in your schema automatically has a field named __typename that you can query for and which returns the Object type’s name as a string, Book and Author in the example above.

Query type

The Query type is a special object type that defines all of the top-level entry points for queries that clients execute against your server.

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

In a REST API this would typically be implemented as two endpoints like /api/books and /api/authors, but GraphQL enables clients to query both resources with a single request. How this client request is structured is up to you:

query GetBooksAndAuthors {
  books {
    title
  }

  authors {
    name
  }
}

or

query GetBooks {
  books {
    title
    author {
      name
    }
  }
}

Mutation type

Whereas the Query type defines entry points for read operations, the Mutation type defines entry points for write operations.

The following mutation creates a new Book and requests certain fields of the created object as a return value

mutation CreateBook {
  addBook(title: "Fox in Socks", author: "Dr. Seuss") {
    title
    author {
      name
    }
  }
}

To prevent race conditions, top-level Mutation fields are resolved serially in the order they’re listed

Input types

Input types are special object types that allow you to provide hierarchical data as arguments to fields (as opposed to providing only flat scalar arguments).

Each field of an input type can be only a scalar, an enum, or another input type:

# Define input types

input BlogPostContent {
  title: String
  body: String
  media: [MediaDetails!]
}

input MediaDetails {
  format: MediaFormat!
  url: String!
}

enum MediaFormat {
  IMAGE
  VIDEO
}
# Example of used input types

type Mutation {
  createBlogPost(content: BlogPostContent!): Post
  updateBlogPost(id: ID!, content: BlogPostContent!): Post
}

Take care if using the same input type for fields of both Query and Mutation. In many cases, arguments that are required for a mutation are optional for a corresponding query. You might want to create separate input types for each operation type.

Enumerations

enum AllowedColor {
  RED
  GREEN
  BLUE
}
type Query {
  favoriteColor: AllowedColor # enum return value
  avatar(borderColor: AllowedColor): String # enum argument
}

Arguments

Here we specify an argument unit with default value METER.

type Starship {
  id: ID!
  name: String!
  length(unit: LengthUnit = METER): Float
}

Descriptions (docstrings)

"Description for the type"
type MyObjectType {
  """
  Description for field
  Supports **multi-line** description for your [API](http://example.com)!
  """
  myField: String!

  otherField(
    "Description for argument"
    arg: Int
  )
}

Things to consider when creating/changing the schema

All of the following schema changes are potentially breaking changes:

  • Removing a type or field
  • Renaming a type or field
  • Adding nullability to a field
  • Removing a field’s arguments
  • It’s easier and safer to add a new field to a schema than it is to remove an existing field that some of your clients are using.

Query driven design

Design your schema based on how data is used (aka Query-driven schema design), not based on how it’s stored.

If your client wants to query that:

query EventList {
  upcomingEvents {
    name
    date
    location {
      name
      weather {
        temperature
        description
      }
    }
  }
}

Then your schema could look like that:

type Query {
  upcomingEvents: [Event!]!
}

type Event {
  name: String!
  date: String!
  location: Location
}

type Location {
  name: String!
  weather: WeatherInfo
}

type WeatherInfo {
  temperature: Float
  description: String
}

Naming conventions

  • Field names should use camelCase
  • Type names should use PascalCase
  • Enum names should use PascalCase
  • Enum values should use ALL_CAPS

In GraphQL, it’s recommended for every mutation’s response to include the data that the mutation modified. This enables clients to obtain the latest persisted data without needing to send a followup query.

Error handling

Mutations are much more likely than queries to cause errors, because they modify data. A mutation might even result in a partial error, in which it successfully modifies one piece of data and fails to modify another. It is recommended to define a MutationResponse interface in your schema.

interface MutationResponse {
  code: String!
  success: Boolean!
  message: String!
}

and implemented:

type UpdateUserEmailMutationResponse implements MutationResponse {
  code: String!
  success: Boolean!
  message: String!
  user: User
}

Fetching data

A GraphQL Query that requests the field hero and its containing field name looks like this:

{
  hero {
    name
  }
}

The returned data:

{
  "data": {
    "hero": {
      "name": "R2-D2"
    }
  }
}

Fetching related data

The query can also fetch related data using sub-selections. In the following example an array of friends is returned:

{
  hero {
    name
    # Queries can have comments!
    friends {
      name
    }
  }
}
{
  "data": {
    "hero": {
      "name": "R2-D2",
      "friends": [
        {
          "name": "Luke Skywalker"
        },
        {
          "name": "Han Solo"
        },
        {
          "name": "Leia Organa"
        }
      ]
    }
  }
}

Arguments in a Query

human and height each are passed an argument:

{
  human(id: "1000") {
    name
    height(unit: FOOT)
  }
}
{
  "data": {
    "human": {
      "name": "Luke Skywalker",
      "height": 5.6430448
    }
  }
}

Here we passed “1000” as a fixed string. In a real app this should be dynamic of course. See below on how to use argument variables.

Aliases

Aliases let you rename the result of a field to anything you want. You need this to prevent a response name conflict, for example requesting field hero twice but with different arguments:

{
  empireHero: hero(episode: EMPIRE) {
    name
  }
  jediHero: hero(episode: JEDI) {
    name
  }
}
{
  "data": {
    "empireHero": {
      "name": "Luke Skywalker"
    },
    "jediHero": {
      "name": "R2-D2"
    }
  }
}

Fragments

Instead of repeating fields in your query over and over you should use a Fragment instead and define on which object you want to use it:

{
  leftComparison: hero(episode: EMPIRE) {
    ...comparisonFields
  }
  rightComparison: hero(episode: JEDI) {
    ...comparisonFields
  }
}

fragment comparisonFields on Character {
  name
  appearsIn
  friends {
    name
  }
}

You can use variables inside fragments:

query HeroComparison($first: Int = 3) {
  leftComparison: hero(episode: EMPIRE) {
    ...comparisonFields
  }
  rightComparison: hero(episode: JEDI) {
    ...comparisonFields
  }
}

fragment comparisonFields on Character {
  name
  friendsConnection(first: $first) {
    totalCount
    edges {
      node {
        name
      }
    }
  }
}

Operation name

In production apps it’s useful to use operation name and type explicitly to make debugging and server-side logging easier. query is the operation type and HeroNameAndFriends is the operation name:

query HeroNameAndFriends {
  hero {
    name
    friends {
      name
    }
  }
}

Variables in arguments

In your app you need to pass dynamic variables to your GraphQL queries instead of hard-coding a value of “1000” for example. So first you write your query specifying your variable names:

query HeroNameAndFriends($episode: Episode = JEDI) {
  hero(episode: $episode) {
    name
    friends {
      name
    }
  }
}

Second, you specify your variable values separately:

{
  "episode": "NEWHOPE"
}

All declared variables must be either scalars, enums, or input object types.

Dynamically changing query structure (Directives)

Directives @include and @skip allow you to include or skip entire query structures based on a boolean value:

query Hero($episode: Episode, $withFriends: Boolean!) {
  hero(episode: $episode) {
    name
    # also @skip(if: Boolean)
    friends @include(if: $withFriends) {
      name
    }
  }
}
{
  "episode": "JEDI",
  "withFriends": false
}

Sending mutations from a client

Creating or updating data is done via Mutations. We can mutate and query the new value of the field with one request.

mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) {
  createReview(episode: $ep, review: $review) {
    stars
    commentary
  }
}
// the variables to pass in
{
  "ep": "JEDI",
  "review": {
    "stars": 5,
    "commentary": "This is a great movie!"
  }
}

In this example, the review variable we passed in is not a scalar. It’s an input object type.

If a mutation contains multiple fields, then they will be executed one after the other, not parallel as a query would. In other words, the first is guaranteed to finish before the second begins, ensuring that we don’t end up with a race condition.

Inline Fragments

Depending on a passed in query argument a query can return fields from different types. Example: If $ep is “JEDI” then the field name of type Droid will be returned, otherwise the field height of type Human is returned. We specify this using Inline Fragments:

query HeroForEpisode($ep: Episode!) {
  hero(episode: $ep) {
    name
    ... on Droid {
      primaryFunction
    }
    ... on Human {
      height
    }
  }
}

Meta fields

In your client you can check the returned __typename meta field to determine which type was returned:

{
  search(text: "an") {
    __typename
    ... on Human {
      name
    }
    ... on Droid {
      name
    }
    ... on Starship {
      name
    }
  }
}
{
  "data": {
    "search": [
      {
        "__typename": "Human",
        "name": "Han Solo"
      },
      {
        "__typename": "Human",
        "name": "Leia Organa"
      },
      {
        "__typename": "Starship",
        "name": "TIE Advanced x1"
      }
    ]
  }
}

This means that wherever we use the type Episode in our schema, we expect it to be exactly one of NEWHOPE, EMPIRE, or JEDI.

Lists and Non-Null

This following example means that the list itself can be null, but it can’t have any null members

myField: [String!]
myField: null // valid
myField: [] // valid
myField: ['a', 'b'] // valid
myField: ['a', null, 'b'] // error

The following example means that the list itself cannot be null, but it can contain null values:

myField: [String]!
myField: null // error
myField: [] // valid
myField: ['a', 'b'] // valid
myField: ['a', null, 'b'] // valid

Interfaces

This means that any type that implements Character needs to have these exact fields, with these arguments and return types

interface Character {
  id: ID!
  name: String!
  friends: [Character]
  appearsIn: [Episode]!
}

The following type implements the interface and additionally specifies own fields.

type Human implements Character {
  id: ID!
  name: String!
  friends: [Character]
  appearsIn: [Episode]!
  starships: [Starship]
  totalCredits: Int
}

Union types

Union types are very similar to interfaces, but they don’t get to specify any common fields between the types. All of a union’s included types must be object types (not scalars, input types, etc.)

union SearchResult = Book | Author

type Book {
  title: String!
}


type Author {
  name: String!
}


type Query {
  search(contains: String): [SearchResult!]
}

GraphQL clients don’t know which object type a field will return if the field’s return type is a union. To account for this, a query can include the subfields of multiple possible types.

query GetSearchResults {
  search(contains: "Shakespeare") {
    # Querying for __typename is almost always recommended,
    # but it's even more important when querying a field that
    # might return one of multiple types.
    __typename
    ... on Book {
      title
    }
    ... on Author {
      name
    }
  }
}

Resolvers

A resolver is a function that’s responsible for populating the data for a single field in your schema. It can populate that data in any way you define, such as by fetching data from a back-end database or a third-party API.

If you don’t define a resolver for a particular field, Apollo Server automatically defines a default resolver for it.

type User {
  id: ID!
  name: String
}


type Query {
  user(id: ID!): User
}
// resolvers.ts

const users = [
  {
    id: '1',
    name: 'Elizabeth Bennet'
  },
  // ...
];

const resolvers = {
  Query: {
    user(parent, args, context, info) {
      return users.find(user => user.id === args.id);
    }
  }
}

When a field is executed, the corresponding resolver is called to produce the next value. If a field produces a scalar value like a string or number, then the execution completes. However if a field produces an object value then the query will contain another selection of fields which apply to that object. This continues until scalar values are reached. GraphQL queries always end at scalar values.

The context argument is useful for passing things that any resolver might need, like authentication scope, database connections, and custom fetch functions. If you’re using dataloaders to batch requests across resolvers, you can attach them to the context as well.

To provide an initial context to your resolvers, add a context initialization function to the ApolloServer constructor. This function is called with every request, so you can customize the context based on each request’s details (such as HTTP headers).

// Constructor
const server = new ApolloServer({
  typeDefs,
  resolvers,
  context: ({ req }) => ({
    authScope: getScope(req.headers.authorization)
  })
}));

// Example resolver
(parent, args, context, info) => {
  if(context.authScope !== ADMIN) throw new AuthenticationError('not admin');
  // Proceed
}

Subscriptions

Subscriptions are long-lasting GraphQL read operations using the WebSocket protocol instead of HTTP. Subscriptions can update their result whenever a particular server-side event occurs. Most commonly, updated results are pushed from the server to subscribing clients. For example, a chat application’s server might use a subscription to push newly received messages to all clients in a particular chat room.

To enable subscriptions, you must first swap to the apollo-server-express package.

More about it on the Apollo Server page.

Using Prisma to let a GraphQL server fetch data from databases

Prisma sits between the server and the database and supports SQL and NoSQL databases. It replaces traditional ORMs and simplifies database workflows by providing a data access layer that makes it easy to access the database in a type-safe way. It also works for GraphQL subscriptions and offers a powerful visual data management admin tool.

tbc

GraphQL Tools

  • GraphiQL is an in-browser IDE for writing, live validating and testing GraphQL queries
  • GraphQL Voyager to visualize the GraphQL API as an interactive graph
  • GraphQL Visual Editor creates schemas by joing visual blocks together and then transforms them into code
  • GraphQL Faker mocks your API with realistic data from faker.js and it is useful for testing and coding against future APIs

About Author

Mathias Bothe Contact me

I am Mathias, born 38 years ago in Heidelberg, Germany. Today I am living in Munich and Stockholm. I am a passionate IT freelancer with more than 14 years experience in programming, especially in developing web based applications for companies that range from small startups to the big players out there. I am founder of bosy.com, creator of the security service platform BosyProtect© and initiator of several other software projects.