data :{
addUser :"a12e5d"
}
Subscription in GraphQL
With the help of subscriptions, the client receives database changes in real time. Under the hood, subscriptions use WebSockets. Here’s an example:
subscription listenLikes {
listenLikes {
fname
likes
}
}
The above query can, for example, return a list of users with their names and the count of likes every time it changes. Extremely helpful!
For example, when a user with fname Matt receives a like, the response would look like:
data:{
listenLikes:{
"fname":"Matt",
"likes":245
}
}
A similar request can be used to update the likes count in real-time, say for the voting form results.
GraphQL Concepts
Now that we know different query types, let’s figure out how to deal with elements that are used in GraphQL.
Concepts I am going to cover below:
- Fields
- Arguments
- Aliases
- Fragments
- Variables
- Directives
1. Fields
Look at a simple GraphQL query:
{
user {
name
}
}
In this request, you see 2 fields. The user field returns an object containing another field of type String. GraphQL server will return a user object with only the user’s name. So simple, so let’s move on.
2. Arguments
In the example below, an argument is passed to indicate which user to refer to:
{
user(id:"1"){
name
}
}
Here in particular we’re passing the user’s id, but we could also pass a name argument, assuming the API has a backend function to return such a response. We can also have a limit argument indicating how many subscribers we want to return in the response. The below query returns the name of the user with id=1 and their first 50 followers:
{
user(id:"1"){
name
followers(limit:50)
}
}
3. Aliases
GraphQL uses aliases to rename fields within a query response. It might be useful to retrieve data from multiple fields having the same names so that you ensure these fields will have different names in the response to distinguish. Here’s an example of a GraphQL query using aliases:
query {
products {
name
description
}
users {
userName: name
userDescription: description
}
}
as well as the response to it:
{
"data":{
"products":[
{
"name":"Product A",
"description":"Description A"
},
{
"name":"Product B",
"description":"Description B"
}
],
"users":[
{
"userName":"User 1",
"userDescription":"User Description 1"
},
{
"userName":"User 2",
"userDescription":"User Description 2"
}
]
}
}
This way we can distinguish the name and description of the product from the name and description of the user in the response. It reminds me of the way we did this in SQL when joining two tables, to distinguish between the same names of two joined columns. This problem most often occurs with the id and name columns.
4. Fragments
The fragments are often used to break up complex application data requirements into smaller chunks, especially when you need to combine many UI components with different fragments into one initial data sample.
{
leftComparison: tweet(id:1){
...comparisonFields
}
rightComparison: tweet(id:2){
...comparisonFields
}
}
fragment comparisonFields on tweet {
userName
userHandle
date
body
repliesCount
likes
}
What’s going on with this request?
- We sent two requests to obtain information about two different tweets: a tweet with
id equal 1 and tweet with id equal 2.
- For each request, we create aliases:
leftComparison and rightComparison.
- We use the fragment
comparisonFields, which contains a set of fields that we want to get for each tweet. Fragments allow us to avoid duplicating code and reuse the same set of fields in multiple places in the request (DRY principle).
It returns the following response:
{
"data":{
"leftComparison":{
userName:"foo",
userHandle:"@foo",
date:"2019-05-01",
body:"Life is good",
repliesCount:10,
tweetsCount:200,
likes:500,
},
"rightComparison":{
userName:"boo",
userHandle:"@boo",
date:"2018-05-01",
body:"This blog is awesome",
repliesCount:15,
tweetsCount:500,
likes:700
}
}
}
5. Variables
GraphQL variables are a way to dynamically pass a value into a query. The example below provides a user id statically to the request:
{
accholder: user(id:"1"){
fullname: name
}
}
Let’s now replace the static value by adding a variable. The above can be rewritten as:
query GetAccHolder($id: String){
accholder: user(id: $id){
fullname: name
}
}
{
"id":"1"
}
In this example, GetAccHolder is a named function that is useful when you have plenty of requests in your application.
Then we declared the variable $id of type String. Well, then it’s exactly the same as in the original request, instead of a fixed id, we provided the variable $id to the request. The actual values of the variables are passed in a separate block.
We can also specify a default value for a variable:
query GetAccHolder($id: String = "1"){
accholder: user(id: $id){
fullname: name
}
}
Additionally, it is possible to define a variable mandatory by adding ! to data type:
query GetAccHolder($id: String!){
accholder: user(id: $id){
fullname: name
}
}
6. Directives
We can dynamically generate a query structure by using directives. They help us dynamically change the structure and form of our queries using variables. @include and @skip are two directives available in GraphQL.
Examples of directives:
@include(if: Boolean) – include the field if the value of the boolean variable = true
@skip(if: Boolean) — skip field if boolean variable value = true
query GetFollowers($id: String){
user(id: $id){
fullname: name,
followers: @include(if: $getFollowers){
name
userHandle
tweets
}
}
}
{
"id":"1",
"$getFollowers":false
}
Since $getFollowers equals true, the followers field will get skipped, i.e. excluded from the response.
GraphQL Schema
In order to work with GraphQL on the server, you need to deploy a GraphQL Schema, which describes the logic of the GraphQL API, types, and data structure. A schema consists of two interrelated objects: typeDefs and resolvers.
In order for the server to work with GraphQL types, they must be defined. The typeDef object defines a list of available types, its code looks as below:
const typeDefs= gql`
type User {
id: Int
fname: String
age: Int
likes: Int
posts:[Post]
}
type Post {
id: Int
user: User
body: String
}
type Query {
users(id: Int!): User!
posts(id: Int!): Post!
}
type Mutation {
incrementLike(fname: String!):[User!]
}
type Subscription {
listenLikes :[User]
}
`;
The above code defines a type User, which specifies fname, age, likes as well as other data. Each field defines a data type: String or Int, an exclamation point next to it means that a field is required. GraphQL supports four data types:
String
Int
Float
Boolean
The above example also defines all three types – Query, Mutation, and Subscription.
- The first type which contains
Query, is called users. It takes an id and returns an object with the user’s data, it is a required field. There is another Query type called posts which is designed the same way as users.
- The
Mutation type is called incrementLike. It takes a fname parameter and returns a list of users.
- The
Subscription type is called listenLikes. It returns a list of users.
After defining the types, you need to implement their logic so that the server knows how to respond to requests from a client. We use Resolvers to address that. Resolver is a function that returns specific field data of the type defined in the schema. Resolvers can be asynchronous. You can use resolvers to retrieve data from a REST API, database, or any other source.
So, let’s define resolvers:
const resolvers= {
Query:{
users(root, args){return users.filter(user=> user.id=== args.id)[0]},
posts(root, args){return posts.filter(post=> post.id=== args.id)[0]}
},
User:{
posts:(user)=> {
return posts.filter(post=> post.userId=== user.id)
}
},
Post:{
user:(post)=> {
return users.filter(user=> user.id=== post.userId)[0]
}
},
Mutation:{
incrementLike(parent, args){
users.map((user)=> {
if(user.fname=== args.fname) user.likes++return user
})
pubsub.publish('LIKES',{listenLikes: users});
return users
}
},
Subscription:{
listenLikes:{
subscribe:()=> pubsub.asyncIterator(['LIKES'])
}
}
};
The above example features six functions:
- The
users request returns a user object having the passed id.
- The
posts request returns a post object having the passed id.
- In the
posts User field, the resolver accepts the user’s data and returns a list of his posts.
- In the
user Posts field, the function accepts post data and returns the user who published the post.
- The
incrementLike mutation changes the users object: it increases the number of likes for the user with the corresponding fname. After this, users get published in pubsub with the name LIKES.
listenLikes subscription listens to LIKES and responds when pubsub is updated.
Two words about pubsub. This tool is a real-time information transfer system using WebSockets. pubsub is convenient to use, since everything related to WebSockets is placed in separate abstractions.
Why GraphQL is conceptually successful
- Flexibility. GraphQL does not impose restrictions on query types, making it useful for both typical CRUD operations (create, read, update, delete) and queries with multiple data types.
- Schema definition. GraphQL automatically creates a schema for the API, and the hierarchical code organization with object relationships reduces the complexity.
- Query optimization. GraphQL allows clients to request the exact information they need. This reduces server response time and the volume of data to be transferred over the network.
- Context. GraphQL takes care of the requests and responses implementation so that developers can focus on business logic. Strong Typing helps prevent errors before executing a request.
- Extensibility. GraphQL allows extending the API schema and adding new data types along with reusing existing code and data sources to avoid code redundancy.
Sources and references