This tutorial is out of date and no longer maintained.
In this article we’ll be looking into creating our own data structures in a GraphQL API. Everything we’ll be going over can be explored deeper in the official GraphQL docs.
Since we are setting up a basic API, it would be best to first know how to make queries to test our data.
For the sake of simplicity I recommend using Prepros, or babel, so we can use some of the latest JavaScript features like import.
If you use Prepros, make sure the Babel and Auto Compile options are on for your server.js file
For setting up our server, we’ll be using graphql-yoga, and nodemon. nodemon
will allow us to have automatic serve reload on file changes and graphql-yoga
gives us simple tooling to setup an Express-based GraphQL server.
$ npm i graphql-yoga nodemon
Lets’ start with getting nodemon
working. Under scripts
in the project’s package.json
file, we’ll just add "start": "nodemon server.js"
, so instead of running node server
we’ll run npm run start
.
{
"name": "graphql-api",
"version": "1.0.0",
"description": "",
"main": "server.js",
"dependencies": {
"graphql-yoga": "^1.16.7"
},
"devDependencies": {
"nodemon": "^1.19.1"
},
"scripts": {
"start": "nodemon server.js"
},
"author": "",
"license": "ISC"
}
Over in our main file we’re going to import GraphQLServer
from graphql-yoga
to get our API working on localhost:4000
. Our server is going to need two things, our type definitions, which will initialize and structure our data, and our resolvers, which tell the server how to get and filter the data. To keep things organized we’re going to create variables for our definitions and resolvers and pass those to our server function.
import { GraphQLServer } from 'graphql-yoga'
const typeDefs = ``; // Our definitions need to be in template literals
const resolvers = {};
const server = new GraphQLServer({ typeDefs, resolvers }) ;
server.start(() => console.log('server running'));
This will return an error for now since we haven’t set up a schema in typeDefs
yet.
We can set up a basic query by simply adding type Query {}
with a list of the data we want to make available with what type the return data will be. While we can make our own, GraphQL comes with a few baked in types; String
, Int
(all real numbers), Float
(decimal numbers), Boolean
, ID
, and any of these placed in brackets is an array of that type. Any type ending with a !
is set as a non-nullable type, meaning it will return an error if nothing is sent back.
Here’s an example of different possible variations. Note that you wouldn’t have multiple variations for the same field, as this is just for illustrative purposes:
const typeDefs = `
type Query {
user: String # null
user: String! # error 'Cannot return null for non-nullable field Query.user'
users: [String!]! # must return an array of non-null strings
}
`;
Over in localhost:4000
you’ll notice that you no longer get an error when typing any of these, but will get an error or null when you run the query since we haven’t told it how to get the data yet.
To be able to get data, we need to make a function with the same name as what we’re querying, and have it return what we want.
const resolvers = {
Query: {
user(){
return 'I am a string';
},
users(){
const names = ['Chomp', 'Jaws', 'Alli'];
return names;
}
}
};
Types like String
, Int
, and Boolean
are just what comes with GraphQL out-of-the-box, but we can also make our own types that will return much more intricate data and pass that type to our query to make it requestable.
Outside of Query
but still in typeDefs
we can make our own type like User
(it’s best practice for custom types to be capitalized) and set up the data we want it to have. Just like in Query
, each item in our type must also be typed, like age: Int!
, and we’ll have the data returned by the corresponding resolver.
const typeDefs = `
type Query {
user: User! # Must be of our custom type, User
}
type User {
name: String!
age: Int!
friends: [String]!
}
`;
And over in our resolver we can just pass back an object with our data.
const resolvers = {
Query: {
user(){
return {
name: 'Chomp',
age: 47,
friends: ['Alli', 'Jaws']
};
}
}
};
In a real-world scenario we wouldn’t be returning all of our data in the resolvers like this, but actually getting it from some other source like a database. Instead of worrying about a backend we’re just going to set up some dummy data to use.
const users = [
{
name: 'Chomp',
age: 47,
friends: ['Alli', 'Jaws']
},{
name: 'Alli',
age: 16,
friends: ['Chomp', 'Jaws']
},{
name: 'Jaws',
age: 35,
friends: ['Alli', 'Chomp']
},
];
Since we’re just going to get the whole array of users, we’ll tell typeDefs
we want an array of type User
and the resolver will just return the whole users array.
const typeDefs = `
type Query {
user: [User!]!
}
type User {
name: String!
age: Int!
friends: [String]!
}
`;
const resolvers = {
Query: {
user(){
return users;
}
}
};
Right now our only options are to return one specific piece of data, everything. What if we want to filter the response down to what we want?
We can do that by setting up arguments in our query and using them in conditional statements in the resolver. Our arguments must also be typed.
const typeDefs = `
type Query {
user(name: String!): [User!]!
}
`;
In our resolvers we have access to some very useful objects that GraphQl gives us. For now all we need are parent
and args
.
parent
: Info on the parent element when you have nested custom types. So if we had a User
type which accessed a Post
type, each post would have access to the data on the user.args
: All of our arguments passed into the query.ctx
: Short for context, anything that the query made may need like authentication data.info
: Information about the state of the query.Here, we’re going to check if there’s any argument at all on args
and, if there is, use JavaScript’s filter()
to return only the users whose name matches our argument.
const resolvers = {
Query: {
user(parent, args, ctx, info){
if (!args.name) return users;
else return users.filter(user => {
return user.name.toLowerCase().includes(args.name.toLowerCase());
});
}
}
};
Over in localhost:4000
we can make use of our argument like this:
{
user(name: "alli") {
name
friends
}
}
To allow more nested data we can add our custom types into other custom types. When we do that, we need to add another resolver outside of Query
for that type of data.
We’re going to make it so whenever we get a user we can get back the user data for each of their friends.
server.js
Our resolver is going to create an empty object, loop over each friend on parent
to put every matching user into the friends
array, and return friends
to our query.
const typeDefs = `
type Query {
user(name: String!): [User!]!
}
type User {
name: String!
age: Int!
friends: [User!]!
}
`;
const resolvers {
Query: { ... },
User: { // The nested type that we're querying for
friends(parent, args, ctx, info) {
const friends = [];
parent.friends.forEach(friend => users.filter(user => {
if (user.name.toLowerCase().includes(friend.toLowerCase())) friends.push(user);
}));
return friends;
}
}
};
While there’s still much more to explore with GraphQL, I hope this worked as a thorough enough introduction to setting up our own API.
If you had any difficulty getting our example to work properly you can always reference this repo for it.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!
Sign up for Infrastructure as a Newsletter.
Working on improving health and education, reducing inequality, and spurring economic growth? We'd like to help.
Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.