PB138: GraphQL

Presented by Lukas Grolig

 GraphQL 

GraphQL is a query language for your API, and a server-side runtime for executing queries by using a type system you define for your data. GraphQL isn't tied to any specific database or storage engine and is instead backed by your existing code and data.

 

It is markup to query JSON and define schemas.

And why to use it?

Control over Data Structures

Ask for what you need, get exactly that.

Send a GraphQL query to your API and get exactly what you need, nothing more and nothing less. GraphQL queries always return predictable results.

Apps using GraphQL are fast and stable because they control the data they get.

And why to use it?

Limiting number of requests

Get many resources in a single request.

GraphQL queries access not just the properties of one resource but also smoothly follow references between them. While typical REST APIs require loading from multiple URLs, GraphQL APIs get all the data your app needs in a single request.

Apps using GraphQL can be quick even on slow mobile network connections.

 

Available Constructs

Queries

Queries enable clients to fetch data, but not to modify data.

Fields

At its simplest, GraphQL is about asking for specific fields on objects

{
  hero {
    name
  }
}

Arguments

But in GraphQL, every field and nested object can get its own set of arguments, making GraphQL a complete replacement for making multiple API fetches.

 

{
  human(id: "1000") {
    name
    height(unit: FOOT)
  }
}

Arguments can be of many different types. In the above example, we have used an Enumeration type, which represents one of a finite set of options (in this case, units of length, either METER or FOOT)

{
  human(id: "1000") {
    name
    height(unit: FOOT)
  }
}

Aliases

The result object fields match the name of the field in the query but don't include arguments, you can't directly query for the same field with different arguments. That's why you need aliases - they let you rename the result of a field to anything you want.

{
  empireHero: hero(episode: EMPIRE) {
    name
  }
  jediHero: hero(episode: JEDI) {
    name
  }
}

Fragments

When you need to repead some parts.

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

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

Variables

Something you already know

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

{
  "episode": "JEDI"
}

Default variables

Sometimes you want to add default value

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

Using variables inside fragments

And of course you can use it all in 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
      }
    }
  }
}

Directives

Passing variables in arguments solves a pretty big class of these problems, but we might also need a way to dynamically change the structure and shape of our queries using variables. For example, we can imagine a UI component that has a summarized and detailed view, where one includes more fields than the other.

query Hero($episode: Episode, $withFriends: Boolean!) {
  hero(episode: $episode) {
    name
    friends @include(if: $withFriends) {
      name
    }
  }
}

{
  "episode": "JEDI",
  "withFriends": false
}

Inline Fragments

If you are querying a field that returns an interface or a union type, you will need to use inline fragments to access data on the underlying concrete type. It's easiest to see with an example:

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

Mutations

To enable clients to modify data, our schema needs to define some mutations = writes should be sent explicitly via a mutation.

Mutation

mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) {
  createReview(episode: $ep, review: $review) {
    stars
    commentary
  }
}

{
  "ep": "JEDI",
  "review": {
    "stars": 5,
    "commentary": "This is a great movie!"
  }
}

While query fields are executed in parallel.

Mutation fields run in series, one after the other.

Using 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
      }
    }
  }
}

A schema

Schema

Schemas are strongly typed

type Book {
  title: String
  author: Author
}

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

Types

Schemas are strongly typed

type Book {
  title: String
  author: Author
}

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

Types

Types
The Launch object type has a collection of fields, and each field has a type of its own. A field's type can be either an object type or a scalar type. A scalar type is a primitive (like ID, String, Boolean, or Int) that resolves to a single value. In addition to GraphQL's built-in scalar types, you can define custom scalar types.

In the Launch definition above, Mission and Rocket refer to other object types. Let's add definitions for those, along with the User type (again, all inside the backticks):

If a declared field's type is in [Square Brackets], it's an array of the specified type. If an array has an exclamation point after it, the array cannot be null, but it can be empty.

Object types and fields

type Character {
  name: String!
  appearsIn: [Episode!]!
}

* Character is a GraphQL Object Type, meaning it's a type with some fields. Most of the types in your schema will be object types.
* name and appearsIn are fields on the Character type. That means that name and appearsIn are the only fields that can appear in any part of a GraphQL query that operates on the Character type.
* String is one of the built-in scalar types - these are types that resolve to a single scalar object, and can't have sub-selections in the query. We'll go over scalar types more later.
* String! means that the field is non-nullable, meaning that the GraphQL service promises to always give you a value when you query this field. In the type language, we'll represent those with an exclamation mark.
* [Episode!]! represents an array of Episode objects. Since it is also non-nullable, you can always expect an array (with zero or more items) when you query the appearsIn field. And since Episode! is also non-nullable, you can always expect every item of the array to be an Episode object.

Arguments

Every field on a GraphQL object type can have zero or more arguments, for example the length field below:

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

All arguments are named. Unlike languages like JavaScript and Python where functions take a list of ordered arguments, all arguments in GraphQL are passed by name specifically. In this case, the length field has one defined argument, unit.

The Query and Mutation types

Most types in your schema will just be normal object types, but there are two types that are special within a schema:

schema {
  query: Query
  mutation: Mutation
}

Every GraphQL service has a query type and may or may not have a mutation type. These types are the same as a regular object type, but they are special because they define the entry point of every request

Scalar types 

GraphQL comes with a set of default scalar types out of the box:

  • Int: A signed 32‐bit integer.
  • Float: A signed double-precision floating-point value.
  • String: A UTF‐8 character sequence.
  • Boolean: true or false.
  • ID: The ID scalar type represents a unique identifier

Enumeration types

Scalar that is restricted to a particular set of allowed values. This allows you to:
1. Validate that any arguments of this type are one of the allowed values
2. Communicate through the type system that a field will always be one of a finite set of values

enum Episode {
  NEWHOPE
  EMPIRE
  JEDI
}

Lists and Non-Null

List id defined using []

Everything can be null by default. To specify it is not null, use an exclamation mark !

[Episode!]!

type Character {
  name: String!
  appearsIn: [Episode]!
}

Interfaces

Interface is an abstract type that includes a certain set of fields that a type must include to implement the interface.

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

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

type Droid implements Character {
  id: ID!
  name: String!
  friends: [Character]
  appearsIn: [Episode]!
  primaryFunction: String
}

Union types

Union types are very similar to interfaces, but they don't get to specify any common fields between the types.

 

union SearchResult = Human | Droid | Starship

Input types 

In the case of mutations, where you might want to pass in a whole object to be created. In the GraphQL schema language, input types look exactly the same as regular object types, but with the keyword input instead of type:

 

input ReviewInput {
  stars: Int!
  commentary: String
}

mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) {
  createReview(episode: $ep, review: $review) {
    stars
    commentary
  }
}

{
  "ep": "JEDI",
  "review": {
    "stars": 5,
    "commentary": "This is a great movie!"
  }
}

GraphQLi

Hasura

The Hasura GraphQL engine makes your data instantly accessible over a real-time GraphQL API, so you can build and ship modern apps and APIs faster. Hasura connects to your databases, REST servers, GraphQL servers, and third party APIs to provide a unified realtime GraphQL API across all your data sources.

# in a new directory run
wget https://raw.githubusercontent.com/hasura/graphql-engine/master/install-manifests/docker-compose-v2.0.0/docker-compose.yaml
# or run
curl https://raw.githubusercontent.com/hasura/graphql-engine/master/install-manifests/docker-compose-v2.0.0/docker-compose.yaml -o docker-compose.yml

Quick setup

docker-compose up -d

download componse file

and run

Can be used with existing DB

Can be used with existing DB

Note: if you have an existing migration system it is often better continue using it.

In greenfield project use Hasura migrations

Apollo

 is a comprehensive state management library for JavaScript. It enables you to use GraphQL to manage both local and remote data. Apollo Client is view-layer agnostic, so you can use it with React, Vue, Angular, or even vanilla JS.

Setup

npm install @apollo/client

it is recommended to install Apollo for VSCode

Init client

import {
  ApolloClient,
  gql,
  NormalizedCacheObject
} from '@apollo/client';
import { cache } from './cache';

const client: ApolloClient<NormalizedCacheObject> = new ApolloClient({
  cache,
  uri: 'http://localhost:4000/graphql'
});

Add provider

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

Make query

export const GET_MY_TRIPS = gql`
  query GetMyTrips {
    me {
      id
      email
      trips {
        ...LaunchTile
      }
    }
  }
`;

Connect with component

import React, { Fragment } from 'react';
import { gql, useQuery } from '@apollo/client';

import { Loading, Header, LaunchTile } from '../components';
import { RouteComponentProps } from '@reach/router';
import * as GetMyTripsTypes from './__generated__/GetMyTrips';

interface ProfileProps extends RouteComponentProps {}

const Profile: React.FC<ProfileProps> = () => {
  const {
    data,
    loading,
    error
  } = useQuery<GetMyTripsTypes.GetMyTrips>(
    GET_MY_TRIPS,
    { fetchPolicy: "network-only" }
  );
  if (loading) return <Loading />;
  if (error) return <p>ERROR: {error.message}</p>;
  if (data === undefined) return <p>ERROR</p>;

  return (
    <Fragment>
      <Header>My Trips</Header>
      {data.me && data.me.trips.length ? (
        data.me.trips.map((launch: any) => (
          <LaunchTile key={launch.id} launch={launch} />
        ))
      ) : (
        <p>You haven't booked any trips</p>
      )}
    </Fragment>
  );
}

export default Profile;

Ok, that's it.

PB138: GraphQL

By Lukáš Grolig

PB138: GraphQL

  • 425