GraphQL is one of the hottest topics in web development right now. I do a lot of work with Magento and they recently announced a whole new GraphQL API to be used for front end development. To learn GraphQL, I built an experimental Magento frontend using Next.js. After that project, I still did not understand how to build a GraphQL server. As a result, I decided to build an example GraphQL server using Node.js. The original GraphQL specification was written in JavaScript so it is a good language to use when learning GraphQL. I created a GitHub repository called graphql-nodejs-example if you want to view the whole project. In this post, I want to discuss a few concepts about GraphQL that really helped me understand how it works.
A GraphQL Server Has Only One Endpoint
In REST APIs, it is common to have multiple URLs for a single resource. You might have several endpoints for loading and creating data like /tasks
, tasks/1
, /tasks/create
. In GraphQL, your server runs only a single endpoint, usually at the root /
or at /graphql
. When submitting a query to a GraphQL server, you explicitly set the resource you want in the request body, so the server can decide what values to return.
GraphQL Is About Types
In a GraphQL API, you define what resources you have using a type language. GraphQL supports five scalar types that you can use to compose more complex object types. The five scalar types are: Int
, Float
, String
, Boolean
and ID
. To create a resource, you build an object type for it. I wanted to emulate a forum so I created three resources: User
, Thread
and Comment
. In GraphQL types, those resources look like this:
type User {
id: Int!
userName: String!
firstName: String
lastName: String
}
type Thread {
id: Int!
name: String!
description: String!
user: User!
comments: [Comment]
}
type Comment {
id: Int!
description: String!
user: User!
}
You can see that you can create an object type using the type
keyword followed by a name. In curly braces, you define the properties of the object by writing the name of the property followed by a colon and the type. An exclamation point !
after the property indicates that the value cannot be null.
You can also use custom types in other custom types. The Thread
type has a user and comments property that reference the other two types I created. Brackets around the type name like [Comment]
indicate that the property is an array.
When writing a server, where do you put those types? I put them all in a file called schema.graphql
and used the Apollo Server helper gql
to import that file into my server.
Requests Are Handled By a Query and Mutation Type
In GraphQL, there are two types of requests you can send to a GraphQL server: query
and mutation
. A query
is used to retrieve data and a mutation
is used to perform actions on data, like creating or updating. In your server schema, you define a query object type and a mutation object type, like so:
type Query {
thread(id: Int!): Thread
threads: [Thread]
}
type Mutation {
createThread(name: String!, description: String!, userId: Int!): Thread
createComment(userId: Int!, threadId: Int!, description: String!): Comment
}
You can see in my Query
type, I define two ways to retrieve a thread. The threads
property returns an array of all threads and the thread(id: ID!)
returns a single thread. The parentheses denote arguments that can be passed in the query. Since I marked id
as a non-nullable Int
, to retrieve a single thread you have to pass in the id
of a thread in your GraphQL request.
In the Mutation
type, there are two properties for creating a thread and creating a comment. Each operation requires a set of values for creating the resource and each returns the newly created resource.
Resolving Your Queries and Mutations
After defining the schema, how do you implement the logic to load the resources from a data source? You use resolvers! Resolvers are similar to controllers in a REST API. For each Query
and Mutation
property, you create a JavaScript function that accepts arguments and performs the operation on the resource to load data or change it.
I used the Apollo Server library to build my GraphQL API. The library allows you to write your schema, import it and pass in a resolver object that will handle all of the requests.
My Apollo Server setup looks like this:
const fs = require('fs');
const { ApolloServer, gql } = require('apollo-server');
const schema = fs.readFileSync(__dirname.concat('/schema.graphql'), 'utf8');
const typeDefs = gql(schema);
const resolvers = require('./resolvers');
const server = new ApolloServer({ typeDefs, resolvers });
server.listen().then(({ url }) => {
console.log(`Server ready at ${url}`)
});
All I need for my Apollo Server instance is to pass in my schema and resolvers and it will start a node server that I can query.
My resolvers file just exports a JavaScript object with a Query and Mutation property that holds references to functions for each property defined in my schema:
const threads = require('./threads');
const comments = require('./comments');
module.exports = {
Query: {
threads: threads.all,
thread: threads.findOne,
},
Mutation: {
createThread: threads.create,
createComment: comments.create,
}
};
The threads
and comments
imports each return an object of functions that can be passed into the resolver object.
So what does a resolver function look like? Here is a query resolver that returns all Thread
types from a database:
exports.all = async function () {
const threads = await db.Thread.query().eager('[comments.[user], user]');
return threads;
};
The function queries a database for the data needed to resolve the Thread
type and then Apollo Server pulls out the values it needs and returns it to the client that requested all the threads.
A mutation is very similar:
exports.create = async function (parent, args) {
const thread = await db.Thread.query().eager('user').insertAndFetch({
userId,
name,
description,
} = args);
thread.comments = [];
return thread;
};
The second parameter a resolver function receives is all the arguments passed from the request. I use those arguments to create a new thread in the database and then return the data for Apollo Server to pass back to the client.
Querying the Server
There are many ways to test a GraphQL API. I like to use Insomnia. In development mode, Apollo Server will return your schema so that Insomnia can read it, allowing you to auto-complete queries for the API.
Here is an example query you can send to the server with the above schema:
query getThreads {
threads {
id
name
description
user {
id
firstName
lastName
userName
}
comments {
id
description
user {
id
userName
}
}
}
}
In the query, I am requesting the threads
property of the query object and passing in the attributes I want for each thread. Dynamic queries are what make GraphQL so good, because you can ask for as little or as much data as the API can provide. The following json represents what the API server returns to the client:
{
"data": {
"threads": [
{
"id": 1,
"name": "Thread 1",
"description": "This is the first thread",
"user": {
"id": 1,
"firstName": "Test",
"lastName": "User 1",
"userName": "testuser1"
},
"comments": [
{
"id": 1,
"description": "This is a comment on the first thread",
"user": {
"id": 2,
"userName": "testuser2"
}
},
{
"id": 3,
"description": "Another comment",
"user": {
"id": 1,
"userName": "testuser1"
}
}
]
},
{
"id": 2,
"name": "Thread 2",
"description": "This is the second thread",
"user": {
"id": 2,
"firstName": "Test",
"lastName": "User 2",
"userName": "testuser2"
},
"comments": [
{
"id": 2,
"description": "This is a comment on the second thread",
"user": {
"id": 1,
"userName": "testuser1"
}
}
]
}
]
}
}
A mutation query for creating a thread looks like this:
mutation createThread {
createThread(
userId: 1,
name: "A new thread",
description: "This is a description"
) {
id
name
description
user {
id
firstName
lastName
userName
}
}
}
I am calling the createThread
property of the mutation type and passing in the required arguments. It returns to me the resource that it just created:
{
"data": {
"createThread": {
"id": 7,
"name": "A new thread",
"description": "This is a description",
"user": {
"id": 1,
"firstName": "Test",
"lastName": "User 1",
"userName": "testuser1"
}
}
}
}
Some General Tips and Tricks
Here are a few more general tips for starting a GraphQL server project:
If you use a database, use a NoSQL database like MongoDB or a SQL database with an ORM that supports eager loading. GraphQL types often use nested objects so it can be difficult to write plain SQL queries and map the data for your responses. I used the Objection.js ORM with sqlite and that made my database code much simpler.
GraphQL naturally validates the data types of any arguments passed into your API, but it only validates the type. By default, a string type can be empty or any length. I used the validation features of Objection.js to prevent the use of empty strings in mutations.
The
ID
scalar type converts ID values to a string. That will work great for some databases, but in my case I was using sqlite with numeric primary keys so I left my ID values as anInt
.
Conclusion
I was surprised by how quickly you can build a GraphQL API, especially with the help of libraries like Apollo Server and Objection.js. I really like being able to define my API around types which become natural documentation for your available resources. Not having to set up URL routing or type validation saves a lot of time as well. The benefits of GraphQL for building API clients have been touted widely, but I think there are some real advantages for the server too.
I hope this article helped you understand GraphQL servers even better. Leave a comment if you have any questions or thoughts about this post!
Top comments (0)