Node.js GraphQL from scratch

In this article I want to show you how to create node.js GraphQL API using YEPS framework.


It’s a copy of my previous article Node.js REST API from scratch but this time I’ll use more modern way — GraphQL.

GraphQL is a query language for your API, and a server-side runtime for executing queries by using a type system you define for your data. GraphQL isn’t tied to any specific database or storage engine and is instead backed by your existing code and data.

A GraphQL service is created by defining types and fields on those types, then providing functions for each field on each type.

So let’s start and create graph directory:

mkdir graph
cd graph

And init node.js project:

npm init -f

I’m going to use YEPS framework like I used before:

npm i -S yeps yeps-error yeps-server

And YEPS module for working with GraphQL:

npm i -S yeps-graphql graphql

All stuff for working with request we have in yeps-graphql so we need less packages.

Our package.json after:

{
"name": "graph",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"graphql": "^0.11.7",
"yeps": "^1.1.1",
"yeps-error": "^1.3.1",
"yeps-graphql": "0.0.3",
"yeps-server": "^1.1.2"
}

}

Let’s create index.js file with the main code:

touch index.js

And let’s open this file in our favourite text editor/IDE.

First we need to require all dependencies:

const App = require('yeps');

const error = require('yeps-error');
const graphql = require('yeps-graphql');
const server = require('yeps-server');

After we need to require GraphQL types:

const {
GraphQLSchema,
GraphQLObjectType,
GraphQLInt,
GraphQLString,
GraphQLList,
GraphQLNonNull,
GraphQLID,
} = require('graphql');

And create app:

const app = new App();

app.then(error());

As in previous example we use local storage just to show the main idea — how to work with GraphQL, to keep code more simple. How to work with storages you can read in my other article: Full stack react app from scratch — Part 2: classic REST API. And our local storage :

const storage = [];

So next step is creating schema:

const UserType = new GraphQLObjectType({
name: 'UserType',
description: 'User type',
fields: {
id: {
type: GraphQLInt,
},
name: {
type: GraphQLString,
}
}
});

Here we create new GraphQLObjectType for working with user data, we set name, description and fields (user id and user name). User id should have type GraphQLInt and user name: GraphQLString.

Next step is creating query and mutation types.

const QueryType = new GraphQLObjectType({
name: 'QueryType',
description: 'User information',
fields: {
users: {
type: new GraphQLList(UserType),
resolve() {
return storage;
}
},
user: {
type: UserType,
args: {
id: {
type: new GraphQLNonNull(GraphQLID)
}
},
resolve(parent, { id }) {
const index = storage.findIndex((user) => {
return user.id === parseInt(id, 10);
});

if (index !== -1) {
return storage[index];
}

return Promise.reject('User not found!');
}
}
}
});

For getting user info and all users we should create field for each action. Resolve method is our controller, it helps to work with real data.

To get info about all users we can just return our storage as array.

To get info about concrete user we need to set user id (it’s required parameter) and return user from storage. But don’t forget to check it in storage.

And mutation:

const MutationType = new GraphQLObjectType({
name: 'MutationType',
fields: () => ({
createUser: {
type: UserType,
description: 'User creating',
args: {
id: {
type: new GraphQLNonNull(GraphQLInt)
},
name: {
type: new GraphQLNonNull(GraphQLString)
},
},
resolve: (value, { id, name }) => {
const index = storage.findIndex((user) => {
return user.id === parseInt(id, 10);
});

if (index === -1) {
storage.push({ id, name });
return { id, name };
}

return Promise.reject('User exists!');
}
},
updateUser: {
type: UserType,
description: 'User updating',
args: {
id: {
type: new GraphQLNonNull(GraphQLInt)
},
name: {
type: new GraphQLNonNull(GraphQLString)
},
},
resolve: (value, { id, name }) => {
const index = storage.findIndex((user) => {
return user.id === parseInt(id, 10);
});

if (index !== -1) {
Object.assign(storage[index], { name });
return storage[index];
}


return Promise.reject('User not found!');
}
},
deleteUser: {
type: UserType,
description: 'User deleting',
args: {
id: {
type: new GraphQLNonNull(GraphQLInt)
}
},
resolve: (value, { id }) => {
const index = storage.findIndex((user) => {
return user.id === parseInt(id, 10);
});

if (index !== -1) {
storage.splice(index, 1);
return { id };
}


return Promise.reject('User not found!');
}
}
}),
});

The same idea, our fields: createUser, updateUser and deleteUser.

And put all together into schema:

const schema = new GraphQLSchema({
query: QueryType,
mutation: MutationType,
});

And using yeps-graphql run it for each request.

app.then(graphql({
schema,
graphiql: true,
}));

Parameter graphiql helps us to get online documentation. I’ll show it after.

Don’t forget to create server and return it for testing:

module.exports = server.createHttpServer(app);

Full index.js file:

const App = require('yeps');

const error = require('yeps-error');
const graphql = require('yeps-graphql');
const server = require('yeps-server');

const {
GraphQLSchema,
GraphQLObjectType,
GraphQLInt,
GraphQLString,
GraphQLList,
GraphQLNonNull,
GraphQLID,
} = require('graphql');

const app = new App();

app.then(error());


const storage = [];


const UserType = new GraphQLObjectType({
name: 'UserType',
description: 'User type',
fields: {
id: {
type: GraphQLInt,
},
name: {
type: GraphQLString,
}
}
});

const QueryType = new GraphQLObjectType({
name: 'QueryType',
description: 'User information',
fields: {
users: {
type: new GraphQLList(UserType),
resolve() {
return storage;
}
},
user: {
type: UserType,
args: {
id: {
type: new GraphQLNonNull(GraphQLID)
}
},
resolve(parent, { id }) {
const index = storage.findIndex((user) => {
return user.id === parseInt(id, 10);
});

if (index !== -1) {
return storage[index];
}

return Promise.reject('User not found!');
}
}
}
});

const MutationType = new GraphQLObjectType({
name: 'MutationType',
fields: () => ({
createUser: {
type: UserType,
description: 'User creating',
args: {
id: {
type: new GraphQLNonNull(GraphQLInt)
},
name: {
type: new GraphQLNonNull(GraphQLString)
},
},
resolve: (value, { id, name }) => {
const index = storage.findIndex((user) => {
return user.id === parseInt(id, 10);
});

if (index === -1) {
storage.push({ id, name });
return { id, name };
}

return Promise.reject('User exists!');
}
},
updateUser: {
type: UserType,
description: 'User updating',
args: {
id: {
type: new GraphQLNonNull(GraphQLInt)
},
name: {
type: new GraphQLNonNull(GraphQLString)
},
},
resolve: (value, { id, name }) => {
const index = storage.findIndex((user) => {
return user.id === parseInt(id, 10);
});

if (index !== -1) {
Object.assign(storage[index], { name });
return storage[index];
}


return Promise.reject('User not found!');
}
},
deleteUser: {
type: UserType,
description: 'User deleting',
args: {
id: {
type: new GraphQLNonNull(GraphQLInt)
}
},
resolve: (value, { id }) => {
const index = storage.findIndex((user) => {
return user.id === parseInt(id, 10);
});

if (index !== -1) {
storage.splice(index, 1);
return { id };
}


return Promise.reject('User not found!');
}
}
}),
});

const schema = new GraphQLSchema({
query: QueryType,
mutation: MutationType,
});


app.then(graphql({
schema,
graphiql: true,
}));


module.exports = server.createHttpServer(app);

Now I want to show you a great tool for testing and documentation made for GraphQL. It works if you enable graphiql option like I did before.

One small step.

Let’s add package.json scripts command to run our server:

"scripts": {
"start": "node index.js",
"test": "echo \"Error: no test specified\" && exit 1"
},

And run npm start.

Let’s open our app in browser: http://localhost:3000/

Wee see some start documentation how to use it. If we want to see our schema just click Docs link:

Here we see our query and mutation types:

And see our user type:

And our mutation:

So let’s start manual testing of our app. First step is checking our empty storage. To make it we need to send query to get all users:

query {
users {
id,
name,
}
}

And our response:

Now it’s empty. So create a new user by sending mutation request:

mutation {
createUser(id:1, name:"User 1") {
id,
name,
}
}

And check our storage using previous request:

So we just created a new user. If we want to check info about only one user we should send query to get user fields with user id:

query {
user(id:1) {
id,
name,
}
}

If we send wrong id:

we can see details what is going wrong.

Let’s update our user:

mutation {
updateUser(id:1, name:"New User name") {
id,
name,
}
}

And delete:

mutation {
deleteUser(id:1) {
id,
name,
}
}

Graphiql tool really helps on development step but to be production ready we need to make function tests.


Let’s install dev dependency:

npm i -D mocha chai chai-http

And update package.json scripts section to run testing:

"scripts": {
"start": "node index.js",
"test": "mocha test.js"
},

And final version of package.json:

{
"name": "graph",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "node index.js",
"test": "mocha test.js"
},

"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"graphql": "^0.11.7",
"yeps": "^1.1.1",
"yeps-error": "^1.3.1",
"yeps-graphql": "0.0.3",
"yeps-server": "^1.1.2"
},

"devDependencies": {
"chai": "^4.1.2",
"chai-http": "^3.0.0",
"mocha": "^4.0.1"
}

}

Create test.js file:

touch test.js

As we use mocha we need to create describe section. We need to run our server only once for our tests to keep storage data in memory. It will make yeps-server. But to finish testing process we need to close server connection. We can make it in method after:

describe('GraphQL test', () => {
after((done) => {
server.close(done);
});
// here we put our tests
});

About testing with chai you can read documentation, in our tests I use expect. Chai-http is a wrapper for supertest.

Let’s make the same steps like we made for manual testing.

First step is getting users from storage:

it('should test empty storage', async () => {
let isTestFinished = false;

const query = {
query: `{
users {
id,
name
}
}`

};

await chai.request(server)
.get('/')
.query(query)
.send()
.then((res) => {
expect(res).to.have.status(200);
expect(res.body.data.users).is.a('array');
isTestFinished = true;
});

expect(isTestFinished).is.true;
});

Next we create a new user and check storage:

it('should test create a new user', async () => {
let isTestFinished1 = false;
let isTestFinished2 = false;

const id = 1;
const name = 'User 1';

const data = {
query: `
mutation {
createUser(id: ${id}, name: "${name}") {
id,
name,
}
}
`

};

await chai.request(server)
.post('/')
.send(data)
.then((res) => {
expect(res).to.have.status(200);
expect(res.body.data.createUser.id).to.be.equal(id);
expect(res.body.data.createUser.name).to.be.equal(name);
isTestFinished1 = true;
});

const data2 = {
query: `
{
users {
id,
name,
}
}
`

};

await chai.request(server)
.get('/')
.query(data2)
.send()
.then((res) => {
expect(res).to.have.status(200);
expect(res.body.data.users).is.a('array');
expect(res.body.data.users[0].id).to.be.equal(id);
expect(res.body.data.users[0].name).to.be.equal(name);
isTestFinished2 = true;
});

expect(isTestFinished1).is.true;
expect(isTestFinished2).is.true;
});

Test specific user:

it('should test user', async () => {
let isTestFinished = false;

const id = 1;

const data = {
query: `
{
user(id: ${id}) {
id,
name,
}
}
`

};

await chai.request(server)
.get('/')
.query(data)
.send()
.then((res) => {
expect(res).to.have.status(200);
expect(res.body.data.user.id).to.be.equal(id);
isTestFinished = true;
});

expect(isTestFinished).is.true;
});

And request with wrong user id:

it('should test user with wrong id', async () => {
let isTestFinished = false;

const id = 100;

const data = {
query: `
{
user(id: ${id}) {
id,
name,
}
}
`

};

await chai.request(server)
.get('/')
.query(data)
.send()
.then((res) => {
expect(res).to.have.status(200);
expect(res.body.data.user).is.null;
expect(res.body.errors).is.a('array');
expect(res.body.errors[0].message).to.be.equal('User not found!');
isTestFinished = true;
});

expect(isTestFinished).is.true;
});

Updating user:

it('should test updating user', async () => {
let isTestFinished1 = false;
let isTestFinished2 = false;

const id = 1;
const name = 'New User name';

const data = {
query: `
mutation {
updateUser(id: ${id}, name: "${name}") {
id,
name,
}
}
`

};

await chai.request(server)
.post('/')
.send(data)
.then((res) => {
expect(res).to.have.status(200);
expect(res.body.data.updateUser.id).to.be.equal(id);
expect(res.body.data.updateUser.name).to.be.equal(name);
isTestFinished1 = true;
});

const data2 = {
query: `
{
users {
id,
name,
}
}
`

};

await chai.request(server)
.get('/')
.query(data2)
.send()
.then((res) => {
expect(res).to.have.status(200);
expect(res.body.data.users).is.a('array');
expect(res.body.data.users[0].id).to.be.equal(id);
expect(res.body.data.users[0].name).to.be.equal(name);
isTestFinished2 = true;
});

expect(isTestFinished1).is.true;
expect(isTestFinished2).is.true;
});

Deleting user:

it('should test deleting user', async () => {
let isTestFinished1 = false;
let isTestFinished2 = false;

const id = 1;

const data = {
query: `
mutation {
deleteUser(id: ${id}) {
id,
name,
}
}
`

};

await chai.request(server)
.post('/')
.send(data)
.then((res) => {
expect(res).to.have.status(200);
expect(res.body.data.deleteUser.id).to.be.equal(id);
expect(res.body.data.deleteUser.name).is.null;
isTestFinished1 = true;
});

const data2 = {
query: `
{
users {
id,
name,
}
}
`

};

await chai.request(server)
.get('/')
.query(data2)
.send()
.then((res) => {
expect(res).to.have.status(200);
expect(res.body.data.users).is.a('array');
expect(res.body.data.users.length).to.be.equal(0);
isTestFinished2 = true;
});

expect(isTestFinished1).is.true;
expect(isTestFinished2).is.true;
});

And full test.js file:

const chai = require('chai');
const chaiHttp = require('chai-http');

const server = require('.');

chai.use(chaiHttp);

const { expect } = chai;

describe('GraphQL test', () => {
after((done) => {
server.close(done);
});

it('should test empty storage', async () => {
let isTestFinished = false;

const data = {
query: `
{
users {
id,
name,
}
}
`
};

await chai.request(server)
.get('/')
.query(data)
.send()
.then((res) => {
expect(res).to.have.status(200);
expect(res.body.data.users).is.a('array');
isTestFinished = true;
});

expect(isTestFinished).is.true;
});

it('should test create a new user', async () => {
let isTestFinished1 = false;
let isTestFinished2 = false;

const id = 1;
const name = 'User 1';

const data = {
query: `
mutation {
createUser(id: ${id}, name: "${name}") {
id,
name,
}
}
`
};

await chai.request(server)
.post('/')
.send(data)
.then((res) => {
expect(res).to.have.status(200);
expect(res.body.data.createUser.id).to.be.equal(id);
expect(res.body.data.createUser.name).to.be.equal(name);
isTestFinished1 = true;
});

const data2 = {
query: `
{
users {
id,
name,
}
}
`
};

await chai.request(server)
.get('/')
.query(data2)
.send()
.then((res) => {
expect(res).to.have.status(200);
expect(res.body.data.users).is.a('array');
expect(res.body.data.users[0].id).to.be.equal(id);
expect(res.body.data.users[0].name).to.be.equal(name);
isTestFinished2 = true;
});

expect(isTestFinished1).is.true;
expect(isTestFinished2).is.true;
});

it('should test user', async () => {
let isTestFinished = false;

const id = 1;

const data = {
query: `
{
user(id: ${id}) {
id,
name,
}
}
`
};

await chai.request(server)
.get('/')
.query(data)
.send()
.then((res) => {
expect(res).to.have.status(200);
expect(res.body.data.user.id).to.be.equal(id);
isTestFinished = true;
});

expect(isTestFinished).is.true;
});

it('should test user with wrong id', async () => {
let isTestFinished = false;

const id = 100;

const data = {
query: `
{
user(id: ${id}) {
id,
name,
}
}
`
};

await chai.request(server)
.get('/')
.query(data)
.send()
.then((res) => {
expect(res).to.have.status(200);
expect(res.body.data.user).is.null;
expect(res.body.errors).is.a('array');
expect(res.body.errors[0].message).to.be.equal('User not found!');
isTestFinished = true;
});

expect(isTestFinished).is.true;
});

it('should test updating user', async () => {
let isTestFinished1 = false;
let isTestFinished2 = false;

const id = 1;
const name = 'New User name';

const data = {
query: `
mutation {
updateUser(id: ${id}, name: "${name}") {
id,
name,
}
}
`
};

await chai.request(server)
.post('/')
.send(data)
.then((res) => {
expect(res).to.have.status(200);
expect(res.body.data.updateUser.id).to.be.equal(id);
expect(res.body.data.updateUser.name).to.be.equal(name);
isTestFinished1 = true;
});

const data2 = {
query: `
{
users {
id,
name,
}
}
`
};

await chai.request(server)
.get('/')
.query(data2)
.send()
.then((res) => {
expect(res).to.have.status(200);
expect(res.body.data.users).is.a('array');
expect(res.body.data.users[0].id).to.be.equal(id);
expect(res.body.data.users[0].name).to.be.equal(name);
isTestFinished2 = true;
});

expect(isTestFinished1).is.true;
expect(isTestFinished2).is.true;
});

it('should test deleting user', async () => {
let isTestFinished1 = false;
let isTestFinished2 = false;

const id = 1;

const data = {
query: `
mutation {
deleteUser(id: ${id}) {
id,
name,
}
}
`
};

await chai.request(server)
.post('/')
.send(data)
.then((res) => {
expect(res).to.have.status(200);
expect(res.body.data.deleteUser.id).to.be.equal(id);
expect(res.body.data.deleteUser.name).is.null;
isTestFinished1 = true;
});

const data2 = {
query: `
{
users {
id,
name,
}
}
`
};

await chai.request(server)
.get('/')
.query(data2)
.send()
.then((res) => {
expect(res).to.have.status(200);
expect(res.body.data.users).is.a('array');
expect(res.body.data.users.length).to.be.equal(0);
isTestFinished2 = true;
});

expect(isTestFinished1).is.true;
expect(isTestFinished2).is.true;
});
});

To run tests:

npm test

In this article we created GraphQL server for working with user data. We tested it with graphiql UI and functional tests.

If you like it please star framework on github.

References

Like what you read? Give Evheniy Bystrov a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.