GraphQL Node JS - Moving Existing API From REST To GraphQL
GraphQL Node JS - Moving Existing API From REST To GraphQL

In this post you will learn everything, that you need to know about GraphQL. If you don’t know GraphQL is a really awesome alternative to REST API. This is why in this post we will talk why GraphQL is better than REST API and also we will migrate existing rest application to GraphQL.

In this tutorial you will learn:
- What is GraphQL?
- REST API Problems
- How to convert REST API project to GraphQL

What is GraphQL?

So, the first question here is what is GraphQL? And actually there are lots of misconceptions what is GraphQL. So, first of all you must understand, that this is just a layer between your frontend and your backend. Which actually means this is something in between, that helps us with our API. This is it. It is not a replacement for the backend, it is not a replacement for database and it has nothing to do with frontend.

So, typically when we are talking about REST API we have some API like http://localhost:3012/artists and we are making here POST request and we are providing some data inside. And this is our response, that we are getting.

Rest API example

So, this is a typical example of Rest API. We have some restful resources that we want to get. If we are talking about for example artist entity then typically we want to get a list of our artists and it will be get on /artists. If we want to get a single artist it will be /artists/:id. Then we have specific request for post and get.

And it is all fine in a simple application. But it really won't fit in a big application. There you have lots of custom things. For example just imagine that you have comments and you can put your comments both on your Artist and maybe on your Post. Which actually means you must generate a lot of different URLs. And actually it is not that comfortable to use. This is why people simply create custom not restful API.

Rest API problems

  • So, the first problem that I see here is Rest API is not customizable enough or flexible enough when we are talking about big business applications
  • Secondly (which is super important for me) is that we are getting zero documentation from rest API out of the box
  • No validation out of the box

Which actually means a backend guy in your company created some new API. And you as a frontend have zero understanding what it is about. You don’t know what routes inside and you really need to go and ask "Ok, what routes did you create? What data I must provide inside?". It is not documented out of the box and we are using different additional tools to implement this functionality.

And the last feature which is the most important – there is no validation in Rest API and actually no rules. And this is super bad, because actually from my perspective this is exactly like using JavaScript in production. This is just not safe enough. We are not getting any validation we can throw any stuff inside and only backend validate things.

And actually all this stuff that I am talking about here we are getting out of the box with GraphQL. This is why I am such a huge fan of GraphQL. So, let's look on the real example, how we can convert our Rest API to GraphQL so you can see all its benefits.

Project overview

So, actually here I have a project where we used express and MongoDB and we implemented Rest API in one of previous posts.

// server.js
const express = require("express");
const bodyParser = require("body-parser");
const { connect } = require("./db");
const artistsController = require("./controllers/artists");

const app = express();

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

app.get("/", (_, res) => {
  res.send("Hello API");
});

app.get("/artists", artistsController.all);

app.get("/artists/:id", artistsController.findById);

app.post("/artists", artistsController.create);

app.put("/artists/:id", artistsController.update);

app.delete("/artists/:id", artistsController.delete);

const startServer = async () => {
  await connect("mongodb://localhost:27017/api");

  app.listen(3012, function () {
    console.log("API app started");
  });
};

startServer();

So here we have a server.js where we create our express server. And as you can see we have 5 requests for our artist. So we can get a list of artists, an artist by ID, we can create an artist, we can update an artist and delete an artist. This is it.

And we binding this application with MongoDB, which actually means you must install MongoDB on your machine and start it. And this is exactly what we have here. Before starting of the server we connect to MongoDB first and then after we are starting our API.

The most important point, that here we splitted everything correctly. So we really used here controllers and models for example as you can see here all our requests are not just written everything inline, but they are going in controllers.

// artistsController
const Artists = require("../models/artists");

exports.all = async (_, res, next) => {
  try {
    const docs = await Artists.all();
    res.send(docs);
  } catch (err) {
    return next(err);
  }
};

exports.findById = async (req, res, next) => {
  try {
    const doc = await Artists.findById(req.params.id);
    res.send(doc);
  } catch (err) {
    return next(err);
  }
};

exports.create = async (req, res, next) => {
  try {
    const artist = {
      name: req.body.name,
    };
    const doc = await Artists.create(artist);
    res.send(doc);
  } catch (err) {
    return next(err);
  }
};

exports.update = async (req, res, next) => {
  try {
    const doc = await Artists.update(req.params.id, { name: req.body.name });
    res.send(doc);
  } catch (err) {
    return next(err);
  }
};

exports.delete = async (req, res, next) => {
  try {
    await Artists.delete(req.params.id);
    res.sendStatus(200);
  } catch (err) {
    return next(err);
  }
};

And here we have just a single artist controller which is just a bunch of methods which is using inside a model.

Now let's look on our model.

const { ObjectId } = require("mongodb");
const db = require("../db");

exports.all = () => {
  return db.get().collection("artists").find().toArray();
};

exports.findById = (id) => {
  console.log("ss", ObjectId(id));
  return db
    .get()
    .collection("artists")
    .findOne({ _id: ObjectId(id) });
};

exports.create = async (artist) => {
  await db.get().collection("artists").insertOne(artist);
  return artist;
};

exports.update = async (id, newData) => {
  await db
    .get()
    .collection("artists")
    .updateOne({ _id: ObjectId(id) }, { $set: newData });
  return {
    ...newData,
    _id: id,
  };
};

exports.delete = (id) => {
  return db
    .get()
    .collection("artists")
    .deleteOne({ _id: ObjectId(id) });
};

Here we have a model, which is also just a bunch of functions that we wrote ourselves. And here we are using our DB which is our MongoDB reference and we are making different requests, for example, get list of our artists, find artist by ID, create, update and so on.

And here is how it’s working. I started our web server. Now I can jump inside postman and make a POST request on http://localhost:3012/artists and here is our body. So we actually want to create here an artist with name test. I am hitting send and as you can see we created a new record and we are getting our name and ID of our artist.

Creating REST artist

So this is our basic Rest API for our artist that we want to convert to GraphQL API in this post.

Needed packages

{
  "dependencies": {
    "body-parser": "^1.19.0",
    "express": "^4.17.1",
    "express-graphql": "^0.12.0",
    "graphql": "^16.2.0",
    "mongodb": "^4.1.1"
  },
  "scripts": {
    "start": "node server.js"
  }
}

So, as you can see in our package.json file we had just 3 packages: express (this is our framework), body parser (to work with body) and MongoDB database. And we are leaving everything as it is, but we installed 2 additional packages. First of all GraphQL package and secondly GraphQL express package which is a binding to express and it is working really amazing out of the box.

To install them you can jump to console and write

yarn add graphql
yarn add express-graphql

Configuring GraphQL

Now we installed everything that we need and we can configure GraphQL inside our server.js. The main idea is that we won't change our server.js at all and we will leave all this REST API in place. Additionally to this we will create a new route for our graphql requests.

So this is one of the benefits that we are getting out of the box. We have just a single route /graphql that we are using across the whole application.

We won't create hundreds of different routes for every single request which is already awesome. So this is what we must add to server.js in order to configure GraphQL.

const { graphqlHTTP } = require("express-graphql");
...
app.use(
  "/graphql",
  graphqlHTTP({
    schema: schema,
    pretty: true,
    graphiql: true,
  })
);

So inside app.use we must provide first of all our URL and it will be /graphql. This is a typical route for our graphql server. Now here is a second paramiter we are calling our graphqlHTTP where we must provide an object. And inside this object we need several things. First of all it will be schema. We will talk about it a little bit later, because this is the core point of out GraphQL implementation. The next thing that I want to add here is pretty: true in this case all our requests, when we are making graphql requests will be prettified. And this is really nice to see them in easy to read way. And the last thing that we need here is graphiql: true and this is where all this magic is happening. This is the best tool possible that I can imagine when working with API. And I will show you this graphiql tool in the second.

So, as you can see it is extremely easy to bind graphql to our express server. It is just several lines and usage of graphqlHTTP.

Creating schema

Now our next point is to create schema, because this is exactly where all our code about graphql will go. For this let's create a new file schema.js. So here we want to export our graphql schema.

 const {
  GraphQLSchema,
} = require("graphql");
...
 module.exports = new GraphQLSchema({
});

As you can see we are getting GraphQLSchema from graphql package.

Now inside on our graphql schema we are writing 2 things. First of all we have here query and secondly we have here mutation. Typically people create here a variable which is called rootQuery and rootMutation.

So what is query and what is mutation?

  • Query means that we are just getting some entities. For example getting a list of articles or getting a single user are examples of queries.
  • When we are talking about mutation we are talking about changing our entities. For example updating a user or creating a user or deleting a user are mutations . This is how we typically split this stuff.

Now let's create here a root query.

const rootQuery = new GraphQLObjectType({
  name: 'Query',
  fields: {
    artists: {
      type: GraphQLBoolean,
      resolve: () => {
        return true;
      }
    }
  }
})


module.exports = new GraphQLSchema({
  query: rootQuery
});

So here we define rootQuery as a variable by calling GraphQLObjectType. Inside we provide a name which is Query and fields. fields is an object which contains every single request as a key. Our first key that we used here is artists. It's a key to get a list of artists back. It is also an object where inside we pass a type and resolve. Inside type we define what data type we are getting back and resolve is a function to get data from database. For now we just returned true to make it working.

But this code won't work because we must also provide a rootMutation. Let's create it now in exactly the same way.

const rootMutation = new GraphQLObjectType({
  name: 'Mutation',
  fields: {
    createArtist: {
      type: GraphQLBoolean,
      resolve: () => {
        return true;
      }
    }
  }
})


module.exports = new GraphQLSchema({
  query: rootQuery,
  mutation: rootMutation
});

So once again, here in this file we must return our new GraphQLSchema. This is the schema of graphql, where inside we will create all our requests. And we are creating here query and mutation, which is rootQuery and rootMutation. And inside query we are packing all our requests to get data and inside mutation we pack all our mutations for changing data.

Now let's try to restart our web server and check if we have any errors. As we can see, we don't have any errors and now we want to start GraphiQL. So I will jump to the browser and here I am in http://localhost:3012/graphql. And as you can see we are getting this nice tool here.

Empty GraphiQLi

The most interesting part is that here inside server.js we wrote app.use('/graphql'). This is why here we are getting both POST and GET request for /graphql. GET request is used for GraphiQLi tool and POST request to send data to our API. And this is exactly that option that enables it in config.

This is the best tool possible. It helps us understand, what request we are covering and how we can make some requests to our backend and it validates all this data. Now on the right let's click on the docs link.

Docs explorer

And as you can see here, we are getting query and mutation, we can click on the Query, which is our root query. And as you can see here we have our fields and we see here all our requests. And we see our artists request and it shows us that it must return Boolean.

Now in this tool we can try to do this request

 query {
  artists
 }

When we want to execute query we pack it in a query object with needed keys. In our case it's artists. Also we get validation and autocompletion out of the box. We literally can't write something wrong here. After hitting play we can see our response directly on the right.

Artists query example

And this is exactly this code, that we wrote here, which just resolved this root query artists with true, and we returned here Boolean. And actually we successfully implemented our first route with GraphQL, because as you can see here it is working in browser.

GraphQLI tool is awesome

  • it autocompletes our API
  • you see all your queries and mutations in documentation.
  • you can test here every single request.
  • you have history, prettify, everything, that you want.
  • it validates everything

In comparison REST API can't do anything from this list

Creating custom type

So, now we can start to move our API from Rest to GraphQL. And our first request is this field artists, which must return for us a list of our artist. And actually as you already saw here, we must specify type, what we will get back. And this is awesome, because it is going in strict typing direction, where you are typing what you are getting from the backend.

So what we typically want to do inside graphql, we want to define our entities, that we are getting back or what we are sending to the backend. This is why here on the top before our root mutation I want to create a new data type. And we want to create here an artist type.

const artistType = new GraphQLObjectType({
  name: "Artist",
  fields: () => ({
    _id: {
      type: new GraphQLNonNull(GraphQLID),
      description: "ID",
    },
    name: {
      type: new GraphQLNonNull(GraphQLString),
      description: "The name of the category.",
    },
  }),
});

This is why here we name it artistType and we are using new GraphQLObjectType just like we did previously. Now inside we can provide a name, in our case it will be artist, and it really helps to see in GraphQLI the name later, to know what we are talking about. And after a name here we must provide fields, exactly like we did here in mutation and query, but in this case here we are just defining what fields are possible inside our artist. And inside previous Rest API we had there just an ID and the name. And our ID was with was underscore, because we are talking about MongoDB ID here. And all IDs inside MongoDB is with underscore.

And here we are saying new GraphQLNonNull and inside we are passing GraphQLID. So, this is something new, that we didn't use previously. When we are talking about fields of our entity, for example an artist, in this case we can define if these fields can be null or not. And as you see here, I said that it can’t be null, because here I used new GraphQLNonNull(GraphQLID). This means that this is an ID that can never be null, it must be defined there.

So, this is how we typically define our entity. We have a name, fields and here inside every single property we are defining type. And it can be just a type or it can be null or you can wrap it with new GraphQLNonNull and in this case it will never be null and you will get an error if you will try to get it like this.

Getting the list of artists

Now let's jump back to our request artists.

artists: {
  type: new GraphQLList(artistType),
  resolve: async () => {
    try {
      return await Artists.all();
    } catch (err) {
      return new Error(err.message);
    }
  },
},

Here we want to return a list of our artist type. And actually, when we want to return an array, we must wrap our entity with new GraphQLList and inside in our case we are writing artistType. And this is exactly, what we created here on the top.

Now we need to tune our resolve method. And actually typically we are doing our resolve async, because we are getting data from database asynchronously. Inside I pasted exactly the same code that we used inside REST controllers previously. So we just use Artists model to make a database request. Here we are getting a list of our documents, which are all our artists and we are sending them back. In other case we are throwing an error if we are getting some error here.

As you can see GraphQL doesn't have anything to do with database.

This is why our ORM or model is staying the same. Also as you can see we don't have req, res, next but we just return data or throw an error.

Now let's make this request in GraphQLI

query {
  artists {
    _id
    name
  }
}

And we are getting back our data from the API. Here we must provide fields that we want to get back from GraphQL. This is also extremely efficient as in REST API it is not possible out of the box to specify what fields we want to get back.

It can be that you get empty array because you didn't create artists inside your database. In order to do that you can still use REST API which exists in project.

Getting artist by ID

Now let’s implement our second request and this is getting artist by ID.

artist: {
  type: artistType,
  args: {
    id: {
      type: new GraphQLNonNull(GraphQLID),
    },
  },
  resolve: async (_, args) => {
    try {
      return await Artists.findById(args.id);
    } catch (err) {
      return new Error(err.message);
    }
  },
},

So here what I want to create is new field with the name artist because we are getting a single artist and first of all here we must provide a type that we get back. And our type will be artistType, we already created it and this is just a single artist. But it is not all here, we have something new. We have here a property, which is called args. Which means arguments. And actually to get an artist by ID we must provide an ID. Which means we have a dynamic parameter. And in this case inside GraphQL we have args. So here we say, that we must provide an ID, this is an object with type, which is new GraphQLNonNull, because this is a mandatory, in other case we can’t really do this request and we are passing inside GraphQLID.

So as you can see the logic is exactly the same. Now after args we must create our resolve function, just like we did on the top. And this is an async function, where inside we use our model to get artist by ID. But now actually the question is how we can get this arguments from the request inside our resolve function. So as a first argument we are getting root property and in this post I won't talk about root property, because it is not needed for our application. And it is more advanced stuff, this is why here I will just put _ to ignore this property. And second property is actually arguments. So inside this arguments we will get our ID.

Now let’s check if it’s working.

query {
  artist (id: "4543543546564") {
    _id
    name
  }
}

So here we must put round brackets and provide all our arguments inside. In our case it's only an ID. So we successfully implemented all our queries and now we are coming to mutations.

Create artist

We already create empty mutation createArtist. But we must change the returned type and the logic inside.

createArtist: {
  type: artistType,
  args: {
    artist: {
      type: new GraphQLNonNull(artistInputType),
    },
  },
  resolve: async (_, args) => {
    try {
      return await Artists.create(args.artist);
    } catch (err) {
      throw new Error(err.message);
    }
  },
},

What we want to get here back is obviously an artist, what we created. But also here we must provide our arguments, because here we must provide a body with some data to create this artist. And actually we also want to type it correctly, because we want to show what data we need inside GraphQL. This is why here I want to write, that we are getting artist as an argument and inside we have our type new GraphQLNonNull and inside we want to create a new type artistInputType. So what is the difference between artistType and artistInputType? ArtistType is exactly our entity inside backend inside database but artistInputType is what we are providing from outside, for example from our frontend. And now we must create this artistInput type.

const artistInputType = new GraphQLInputObjectType({
  name: "ArtistInput",
  fields: {
    name: {
      type: new GraphQLNonNull(GraphQLString),
      description: "The name of the category.",
    },
  },
});

Here is our artistInputType. The main difference is that it's GraphQLInputObjectType and we have here only mandatory name to provide.

Inside our createArtist in resolve function we just use our model and pass our artistInputType inside through the arguments. Now let’s try if it’s working.

mutation {
  createArtist(artist: {name: 'graphql'}) {
    _id
    name
  }
}

So instead of work query we write now mutation. We also provide inside artistInput. As you can see we successfully created an artist inside our database.

Updating artist

Now let's implement updating of our artist and actually it will be super similar. This is why I will copy paste create artist completely and just change it a bit.

updateArtist: {
  type: artistType,
  args: {
    id: {
      type: new GraphQLNonNull(GraphQLID),
    },
    artist: {
      type: new GraphQLNonNull(artistInputType),
    },
  },
  resolve: async (_, args) => {
    try {
      return await Artists.update(args.id, args.artist);
    } catch (err) {
      throw new Error(err.message);
    }
  },
},

So we want to name this field updateArtist and what we are doing here? First of all we are getting back our updated artist. As an argument here we can still pass our artistInputType but we must provide an ID of the record, that we need to update. This is why we wrote argument ID new GraphQLNonNull and here we are getting GraphQLID.

Now here inside our resolve function we must use not Artists.create but Artists.update. And inside we must provide not only args.artist but also args.id because in this case we are providing what element we want to update and what data we want to update inside. And our throw is staying the same.

Let's check if it's working.

mutation {
  updateArtist(id: "43243252353", artist: {name: 'graphql'}) {
    _id
    name
  }
}

And our artist was successfully updated.

Deleting artist

And the last method that we want to implement is deleting of our artist.

deleteArtist: {
  type: GraphQLBoolean,
  args: {
    id: {
      type: new GraphQLNonNull(GraphQLID),
    },
  },
  resolve: async (_, args) => {
    try {
      await Artists.delete(args.id);
      return true;
    } catch (err) {
      throw new Error(err.message);
    }
  },
},

Here we need to name it deleteArtist and actually, we don't get our artist back because we removed it. This is why typically you want to have here a Boolean. Now here as an argument - we don't need to provide our artistInputType, we need just to provide an ID that we need to remove. And yes it must be not null because this is mandatory for this request. And here inside our resolve we are getting our arguments with ID inside. And I want inside to call our artists.delete and we are just providing args.id there.

The most important part, that the response of artist delete is the response of MongoDB and this is not exactly the form that we want to. This is why here I just want without return to write await, so we are waiting until deletion and now after this I will simply return true to say that it was removed.

mutation {
  deleteArtist(id: "43243252353")
}

So here we just need to provide an ID. Also we don't actually need to provide anything back as a field, because here it will just return true. And as you can see we are getting back our deleteArtist true, which means our record was successfully removed. And actually we can easily check this. Because here we have history and here on the left we can see the history of our requests. And we are interested of getting an artist by ID. So let's try to find removed record by ID. Now here I am hitting play and we are getting artist null, because we successfully removed it from our data base.

Conclusion

As you can see GraphQL is an extremely powerful tool to work and build APIs. And actually if you are interested to learn why most programmers fail don’t forget to check this post also.

📚 Source code of what we've done