Skip to content

Technologies GraphQL

Rhett Lowe edited this page Apr 10, 2020 · 5 revisions

GraphQL

GraphQL is a query language for client and server to communicate data. GraphQL provides a complete and understandable description of the data in your API, gives clients the power to ask for exactly what they need and nothing more, makes it easier to evolve APIs over time, and enables powerful developer tools.

With GraphQL you dictate your schema, link queries with resolvers (which fetch data from the database or another API), and decide on how users are allowed to make changes. It will accept GraphQL requests, reject improperly formatted request, help client site create requests (GraphQL Playground), force clients to be explicit about what data they need (opposed to accepting all data available).

It doesn't fetch your data, understand your database, authenticate your users, or secure your data/database/connection. It only solidifies how you communicate that data between client and server.

API Server Schema

First, The API server dictates the Schema

type Query {
  getThing: Thing
  getThings(limit: Int): [Thing]!

  getRandom(max: Int!): RandomNumber!
}
type Mutation {
  setThing(thing: ThingInput): Thing
}

type Thing {
  id: String!
  requiredInt: Int!
  optionalString: String
  requiredArrayOfOptionalString: [String]!
}

type RandomNumber {
  number: Number!
  pretruncate: Float!
  precision: Int!
}

input ThingInput {
  id: String!
  requiredInt: Int!
  optionalString: String
  requiredArrayOfOptionalString: [String]!
}

GraphQL has a few primitive types: String, Int, Float, Boolean, and ID. The keyword type denotes a new object class. Objects begin with a capital letter and the language uses camel/pascal case. There are two types that are restricted keywords:

  • Query is a list of gettable functions. They should be cache-able and have no side-effects, they will use the GET HTTP method
  • Mutation is a list of functions to alter the database. These functions do not need to be cache-able, they will use the POST HTTP method.

In schema declaration, the ! (exclamation point) indicates 'required'. [*] (square brackets) indicates an array of the given type (*). ex: String! is a required string, [String]! is a required array of indeterminate length where length may equal 0, [String!]! is the same except a length of 0 is not allowed.

Server Resolvers

These examples will be in javascript and exemplifying Apollo's structure as it does a lot of the minor boilerplate we don't want to deal with.

const resolvers = {
  Query: {
    getThing: () => database.findThingPromise(),
    getThings: (parentResult, args) => database.findManyThingsPromise(args.limit)

    /* ... */
  },
  Mutation: {
    setThing: (parentResult, { thing }) => database.setThing(thing)
  },
  Thing: {
    optionalString: (parentResult) => parentResult.optionalString || '',
    requiredArrayOfOptionalString: ({requiredArrayOfOptionalString}) => requiredArrayOfOptionalString.split(/,/g)
  },

  /* ... more things ... */
}

The resolvers object has two things at the base:

  1. A description of the Query/Mutation callable functions, how they handle requests/data
  2. A description of how custom types handle themselves. By default, all custom type properties have a resolver which returns the existing property as found in the database, assuming it meets the API Schema's definition of the type's property, ex: if the database stores requiredArrayOfOptionalString as a comma separated sting, the schema will eject because it's not an array. If the array provided has booleans, it will be rejected because the array should have strings.

All resolvers take a function with signature, (parentOrSelf, args, context) => returnPromise:

  • parentOrSelf is the self or the root.
    • In a Query or Mutation, it is the root passed by the server. I don't use this much.
    • It is more regularly used in the Type descriptions, where it is the provided object in question. So when providing a default for Thing.optionalString, we can use this object to get what the database things optionalString should be and operate based on that. It can also be used to convert requiredArrayOfOptionalString from a string to an array of strings
  • args is an object holding the properties as stated by the API Schema.
    • In Query and Mutation this is the client input.
    • In Type declarations, this is often empty, unless there's an input stated for the property itself.
  • context is a single object that is consistent for the duration of the request. Often this will hold authentication information to allow/deny functions/information on a per-user/request basis.

Resolvers don't need to access a database. They can run a function, call a local database, call a remote database, make another request to a third party API, or anything else as long as it returns a value or promise to get a value. With this, you can federate or segment your API to sub-APIs as you see fit.

Client Query

query arbitraryName {
  getThing {
    id
  }
}
  • query indicates you are accessing a Query from the type Query {} in the API Schema. This may alternatively by mutation to access a Mutation instead.
  • arbitraryName is optional and meaningless. It is added to better document/troubleshoot the request. Often multiple queries happen in tandem and it can be hard to troubleshoot which is the one you're looking for. This puts a name on each query to help you identify it in the network console.
  • getThing is the Query function you are choosing to access.
  • getThing { ... } the ... are the things you want from resultant Thing object the API Schema has promised to provide via the getThing function.
  • id indicates that you want id from the resultant Thing object. Since nothing else is listed, nothing else is sent. Any time an object is provided, desired properties must be provided or the query will return a syntax error.
mutation arbitraryMutationName(input: ThingInput!) {
  setThing(thing: $input): {
    optionalString
  }
}

In this mutation:

  • we must provide a ThingInput object, wherein there must be an id, requiredInt, and requiredArrayOfOptionalString and the latter must be an array with or without String values.
  • arbitraryMutationName is still optional, but (input: ThingInput!) defines that there will by a provided variable names input and used as $input. input will meet the input ThingInput input-type listed in the API Schema and ! states that input is required.
  • optionalString is again what we hope to get out of the Thing object we are promised by the API Schema.
query {
  getThings(limit: 6) {
    id
    requiredArrayOfOptionalString
  }
}

In this example:

  • there is no query name
  • getThings is the query function from the API Schema
  • (limit: 6) denotes that there is a fixed limit of 6, where 6 is the required int stated in the API Schema

As an alternative to the above example, you could try:

query (max: Int!) {
  getThings(limit: $max) {
    id
    requiredArrayOfOptionalString
  }
}

Where max is an input property which must be an integer and may change each query.

Federation

Federation is a concept to help divide APIs into smaller segments and cluster the APIs as needed. The separation is best be concerns. So people can be stored separate from places, because they have nothing to do with each other, and the APIs can be maintained independently, possibly be different teams. The separated API Schemas are allowed to know about each other and may reference or extend objects in each other as needed.

Usually there is a Gateway that knows about all the federated APIs, collects their Schemas, stitches them together, and provides a single collective API of all services it knows exist.

There are two ways to create these federated services:

  1. make the basic services, then make other services that treat the basic services as sub-APIs, calling upon them as needed. In this case the API Server acts and a client to another API Server
  2. Use a system like Apollo Federated which will do all the heavy lifting for you. You modify the base APIs to include some notes to help the gateway understand how the APIs might connect.

Read more ...

Clone this wiki locally