-
Notifications
You must be signed in to change notification settings - Fork 0
Technologies 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.
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:
-
Queryis a list of gettable functions. They should be cache-able and have no side-effects, they will use theGETHTTP method -
Mutationis a list of functions to alter the database. These functions do not need to be cache-able, they will use thePOSTHTTP 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.
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:
- A description of the
Query/Mutationcallable functions, how they handle requests/data - 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
requiredArrayOfOptionalStringas 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:
-
parentOrSelfis the self or the root.- In a
QueryorMutation, 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 thingsoptionalStringshould be and operate based on that. It can also be used to convertrequiredArrayOfOptionalStringfrom a string to an array of strings
- In a
-
argsis an object holding the properties as stated by the API Schema.- In
QueryandMutationthis is the client input. - In Type declarations, this is often empty, unless there's an input stated for the property itself.
- In
-
contextis 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.
query arbitraryName {
getThing {
id
}
}-
queryindicates you are accessing aQueryfrom thetype Query {}in the API Schema. This may alternatively bymutationto access aMutationinstead. -
arbitraryNameis 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. -
getThingis theQueryfunction you are choosing to access. -
getThing { ... }the...are the things you want from resultantThingobject the API Schema has promised to provide via thegetThingfunction. -
idindicates that you wantidfrom the resultantThingobject. 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
ThingInputobject, wherein there must be anid,requiredInt, andrequiredArrayOfOptionalStringand the latter must be an array with or withoutStringvalues. -
arbitraryMutationNameis still optional, but(input: ThingInput!)defines that there will by a provided variable namesinputand used as$input.inputwill meet theinput ThingInputinput-type listed in the API Schema and!states thatinputis required. -
optionalStringis again what we hope to get out of theThingobject we are promised by the API Schema.
query {
getThings(limit: 6) {
id
requiredArrayOfOptionalString
}
}In this example:
- there is no query name
-
getThingsis 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 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:
- 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
- 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.