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