We can create a simple GraphQL server with Express. To do this, we need the express-graphql
and graphql
packages.
In this article, we’ll look at how to create and use our own GraphQL data types.
Object Types
In many cases, we don’t want to accept and return a number or a string from the API. We can create our own data types to accept and return whatever we want from the API.
With the express-graphql
package, we can define our data types in a string and then pass it into the buildSchema
function.
For example, we can write the following code to define our types, build a schema, and add our resolvers to our code:
const express = require('express');
const graphqlHTTP = require('express-graphql');
const { buildSchema } = require('graphql');
const schema = buildSchema(`
type RandomDie {
numSides: Int!
rollOnce: Int!
roll(numRolls: Int!): [Int]
}
type Query {
getDie(numSides: Int): RandomDie
}
`);
class RandomDie {
constructor(numSides) {
this.numSides = numSides;
}
rollOnce() {
return 1 + Math.floor(Math.random() * this.numSides);
}
roll({ numRolls }) {
const output = [];
for (let i = 0; i < numRolls; i++) {
output.push(this.rollOnce());
}
return output;
}
}
const root = {
getDie: ({ numSides }) => {
return new RandomDie(numSides || 6);
},
};
const app = express();
app.use('/graphql', graphqlHTTP({
schema: schema,
rootValue: root,
graphiql: true,
}));
app.listen(3000, () => console.log('server started'));
In the code above, we defined our schema by writing:
const schema = buildSchema(`
type RandomDie {
numSides: Int!
rollOnce: Int!
roll(numRolls: Int!): [Int]
}
type Query {
getDie(numSides: Int): RandomDie
}
`);
We defined the RandomDie
type with the numSides
field and rollOnce
and roll
methods.
Then we defined our getDie
query to let access the members that we defined in the RandomDie
type.
Then we defined our RandomDie
class, which we’ll use in our getDie
resolver which we’ll define later:
class RandomDie {
constructor(numSides) {
this.numSides = numSides;
}
rollOnce() {
return 1 + Math.floor(Math.random() * this.numSides);
}
roll({ numRolls }) {
const output = [];
for (let i = 0; i < numRolls; i++) {
output.push(this.rollOnce());
}
return output;
}
}
In the class, we created the rollOnce
and roll
methods where we’ll return the results.
Then finally, we define our getDie
resolver as follows:
const root = {
getDie: ({ numSides }) => {
return new RandomDie(numSides || 6);
},
};
We get numSides
from the parameter and then passed it into the RandomDie
constructor when we instantiate it.
Then in the /graphql
page, we can make the following query in the GraphiQL UI:
{
getDie(numSides: 6) {
rollOnce
roll(numRolls: 3)
numSides
}
}
and we should get something like the following as a response:
{
"data": {
"getDie": {
"rollOnce": 3,
"roll": [
6,
4,
5
],
"numSides": 6
}
}
}
Note that we access fields and call methods with no arguments the same way, as we did with rollOnce
and numSides
.
This way of defining objects provides us with some advantages over traditional REST APIs. Instead of using an API request to get basic information about an object and multiple requests to find out more about the object, we can just make one query to get the things we need.
This saves bandwidth, increases performance, and simplifies client-side logic.
Conclusion
We can create new types by putting it in the string with other parts of the schema. Then we can use the buildSchema
function to build the schema.
Once we did that, we create a class to map the type fields into class members. Then we can instantiate that class in our resolver.
Then finally, we can make our request by sending the class name as the query name and then the member names with arguments if required inside the braces.