Building your first GraphQL Server
In this chapter you will learn some concepts like schema
and resolver
and finally you will learn how to create a GraphQL server and query the same. This covers the necessary basics. I you already know the basics have a look at Develop a GraphQL server with nodejs + express.
There are different ways of defining a schema. Use the one that works for you
- Raw approach, this uses the primitives exposed by the
graphql
library - GraphQL query language definition, here we will use the method
buildSchema()
, this is way more easy to work with
The raw approach is meant to show what goes on under the hood. If you only want to know how to build your schemas then jump to the
buildSchema
section
Schema Content
The Schema can be said to consist of two parts:
- Schema definition, by defining a schema we define the entities that exist but also the different queries and mutations that are possible to make
- Resolvers, resolvers are functions that are invoked when the user makes a query or mutation. Resolvers talk to a 3rd party API or a database to perform an operation and ends up returning an answer back to the calling user
Schema definition
A schema pretty much contains the following concepts:
- Query, this is queryable data, it is either parameterless or takes a parameter
- Custom types, you can build your own types if you are not happy with the basic types that is being provided
- Mutations, these are "methods" that should change the data like CREATE, UPDATE or DELETE
- Resolvers, these are functions
- Fragment, this is a reusable piece of schema that you can use in many places in your definition
- Alias, this is a rename mechanism that allows you to rename a column
- Directive, think of this one as if/else and it allows you to decide what columns to include/exclude in your query
Resolvers
Resolvers are part of your schema instance but not part of the Schema definition. Resolvers are simply functions that responds when you try to call a query or mutation. A resolver function responds either with data directly or with a Promise.
The raw approach
In the raw approach we are about to describe below the schema definition and the resolvers are baked in together. We will define a resource, we can call, and its corresponding resolver, in one definition.
To create a schema you need to create an instance of the type GraphQLSchema
. There are different ways of creating this instance but let's look at the most raw approach first. In this approach we have the following code:
new GraphQLSchema(options)
options
is of type GraphQLSchemaConfig
and looks like below:
export interface GraphQLSchemaConfig extends GraphQLSchemaValidationOptions {
query: Maybe<GraphQLObjectType>;
mutation?: Maybe<GraphQLObjectType>;
subscription?: Maybe<GraphQLObjectType>;
types?: Maybe<GraphQLNamedType[]>;
directives?: Maybe<GraphQLDirective[]>;
astNode?: Maybe<SchemaDefinitionNode>;
extensionASTNodes?: Maybe<ReadonlyArray<SchemaExtensionNode>>;
}
As we can see above, the only mandatory field we need to give it for now is query
. This is simple queries/questions and we can see that those are of type GraphQLObjectType
. So let's start constructing such an instance:
import {
graphql,
GraphQLSchema,
GraphQLObjectType,
GraphQLString,
} from "graphql";
const schema = new GraphQLObjectType({
name: "RootQueryType",
fields: {
hello: {
type: GraphQLString,
resolve() {
return "world";
}
}
}
})
Above we can see that we have created the query hello
. hello
is assigned an object so let's break that object down:
- type, this is the datatype
- resolve, this is a resolve function that simply says, if someone invokes me, what should I answer with
There are more fields that we could be declaring like
description
but these two are the most important.
Querying our schema
Next step is to tell graphql about this schema so we can use it:
let query = `{ hello }`;
graphql(schema, query).then(result => {
res.json(result);
});
Above you can see that we invoke graphql
with two different parameters:
- schema, the schema we just defined
- query, this is a variable we just initialized and it contains the GraphQL query language
Let's zoom in on the query
and see what it does:
{
hello
}
Above we simply query for a known query hello
and this will respond with hello world
as querying for it will invoke the resolve()
method. Ok great, we have just created a hello world
in GraphQL. Pause for effect :)
Introducing a custom type
Now there are a lot of primitives we could be using in GraphQL and we have just looked at a String
type. There comes a time when that is really limiting and we want to construct our own types. So let's do that next:
let humanType = new GraphQLObjectType({
name: "Human",
fields: () => ({
id: { type: GraphQLString },
description: { type: GraphQLString },
name: { type: GraphQLString }
})
});
Above we are using the GraphQLObjectType
to create our custom type. The property fields
allows us to list all the different properties our type should have like id
, description
and name
. Ok so we have a custom type, let's put it into use by using it in our schema like so:
let schema = new GraphQLSchema({
query: new GraphQLObjectType({
name: "RootQueryType",
fields: {
hello: {
type: GraphQLString,
resolve() {
return "world";
}
},
person: {
type: humanType,
resolve() {
return people[0];
}
}
}
})
});
Above we added person
that has the type humanType
and is thereby using our custom type. Because we added person
as a queryable we can now extend our query with this type, like so:
let query = `{ hello, person { name, description } }`;
graphql(schema, query).then(result => {
res.json(result);
});
As you can see above we are now adding the following:
{
person {
name, description
}
}
We are querying for person
and we know that person is of type humanType
and humanType
has the fields id
, name
and description
and we choose to select for a subset of that, namely name
and description
.
Full code so far looks like this:
const people = [
{
id: 1,
name: "chris",
description: "viking"
},
{
id: 2,
name: "maxim",
description: "viking"
},
{
id: 3,
name: "sherry",
description: "viking"
},
{
id: 4,
name: "ana",
description: "viking"
}
];
let humanType = new GraphQLObjectType({
name: "Human",
fields: () => ({
id: { type: GraphQLString },
description: { type: GraphQLString },
name: { type: GraphQLString }
})
});
let schema = new GraphQLSchema({
query: new GraphQLObjectType({
name: "RootQueryType",
fields: {
hello: {
type: GraphQLString,
resolve() {
return "world";
}
},
person: {
type: humanType,
resolve() {
return people[0];
}
}
}
})
});
let query = `{ hello, person { name, description } }`;
graphql(schema, query).then(result => {
res.json(result);
});
Introducing a list type
Ok, so far we have describe how we can use a primitive like a String
but also how we can create a custom type humanKind
. Let's look at how we can use the lis type. To use a list type we need to create something of type GraphQLList
. Let's add that to our existing schema, like so:
let schema = new GraphQLSchema({
query: new GraphQLObjectType({
name: "RootQueryType",
fields: {
hello: {
type: GraphQLString,
resolve() {
return "world";
}
},
person: {
type: humanType,
resolve() {
return people[0];
}
},
people: {
type: new GraphQLList(humanType),
resolve() {
return people;
}
}
}
})
});
Above we added people
that uses GraphQLList
and you can see we invoke it like a method and pass our custom type humanType
to it to say what type of list this is. That's really all there is to it.
buildSchema - using query language definition
So far we have been defining a schema in the most painful way possible by working with types like GraphQLObjectType
, GraphQLList
etc, low level types that forces us to write a lot produce our schema.
Using this approach we will separate the schema definition from the resolvers, so we need to create those separately
Sure it is somewhat readable but there is a better way. That better way is using a method called buildSchema()
that will produce an instance of GraphQLSchema
and enables us to define the schema in a much more readable way. Let me show you:
import { buildSchema } from 'graphql';
var schema = buildSchema(`
type Query {
hello: String,
}
`);
var root = {
hello: () => { return 'world' }
}
let query = `{ hello, person { name }, people { name, description } }`;
graphql({
schema,
source: query,
rootValue: root
}).then(result => {
console.log(result);
});
custom type
To create a custom type we simply need to use the type
keyword and choose a name for our type, like so:
var schema = buildSchema(`
type Person {
id: Int,
name: String
},
type Query {
hello: String,
person: Person
}
`);
What you can see above is that we do two things:
- we create our type
Person
- we extend the
Query
withperson
that is of typePerson
This means that we know need to add a resolver function forperson
so that if someone tries to query for it, they won't get an error, so let's do that next:
const people = [{
id: 1, name: 'chris'
},
{
id: 2, name: 'maxim'
}]
var root = {
hello: () => { return 'world' },
person: () => people[0]
}
Above you can see that we created the people
array and that we also extended our root
, our resolver with the property person
. The end result here is that someone can now query for person
and it will work
list type
Let's add another a list type to our schema definition:
var schema = buildSchema(`
type Person {
id: Int,
name: String
},
type Query {
hello: String,
person: Person,
people: [Person]
}
`);
Above we have now added the queryable people
and we can see that it is of list type because it is using square brackets [Person]
. Thereafter we need to add a resolver function for person
that we are used to by now:
var root = {
hello: () => { return 'world' },
person: (id) => people.find(p => p.id === id),
people: () => people
}
parameterized query
So far we have looked simple queries, with no parameters, but most likely we will need to send some parameters to our query to filter down our response. We can easily do that by first defining our schema correctly. We have an existing person
property so let's update that one to take a parameter, like so:
var schema = buildSchema(`
type Person {
id: Int,
name: String
},
type Query {
hello: String,
person( id: Int!): Person,
people: [Person]
}
`);
person
now looks like the following:
person( id: Int!): Person
It looks like the signature of a method call.
Mutation
Mutation is the way we change data in our application we can CREATE, UPDATE or DELETE a resource. To define a mutation we simply need to create the reserved type Mutation
, like so:
type Mutation {
// mutation
// another mutation
}
Once we have the above we can start thinking about what things we want to do. Given the above narrative with a peoples list it makes sense to be able to add a person to that list, so let's create such a mutation, so we update our schema to the following:
input PersonInput {
name: String!
},
type Mutation {
addPerson(person: PersonInput!): String
}
The above tells us we are able to invoke a method/mutation called addPerson()
that takes a person
as input parameter and ends up responding with a string. We are adding a new type of construct here of type input
. When dealing with mutations we need to construct things of type input
if we want to send something more complicated than a primitive ( String, Boolean etc ). Yes our PersonInput
looks a bit simple and a primitive might have been enough in this case but I opted to show you how you could have sent a more complicated input instead, should you wish it.
Next step is to define a resolver for this so that if someone invokes addPerson()
we know what to do:
const people = [{
id: 1,
name: 'chris'
}]
const addPerson = (person) => {
const nextId = people.length === 0 ? 1 : people[people.length -1].id + 1
people = [...people, {...person, { id : nextId } }]
}
var root = {
hello: () => { return 'world' },
person: (id) => people.find(p => p.id === id),
people: () => people,
addPerson: (args) => {
const { person } = args;
addPerson(person);
return 'success'
}
}
Add this point we have created a specific mutation addPerson()
and we have defined a resolver.
Now we know the basics of creating a GraphQL server.
Summary
We've learned that there are two major ways to declare a schema:
- the raw approach, more to type
- the GraphQL schema definition approach using
buildSchema()
We've also covered how we can answer a query by invoking resolvers
, which are functions that needs to fetch the data from somewhere.
Lastly we've learnt how to create mutations.
Next up we will cover how to create a schema, write some resolvers and get a GraphQL server up and running GraphQL + Node.js Express