Vanilla GraphQL With NodeJS And PostgreSQL: Setting Up Application

Reading Time: 7 minutes

Hi guys, in today’s post, let’s talk about GraphQL.

So it’s been 5 years since the day GraphQL was publicly released and the majority of us may be using it for our daily projects. 5 years is long enough for a new piece of technology and its ecosystem to mature. Today, we can easily integrate GraphQL into our workflow with Apollo Server/GraphQL Yoga, etc on the backend side and Relay/Apollo Client, etc if you’re on the frontend team. That’s pretty cool. However, those fancy tools (especially when used together with ORMs) also added many functionalities which can sometimes prevent us from grasping what GraphQL actually is and what it is capable of.

That being said, let’s do something differently. In this post and the next coming one, I would like to walk you through a journey to fire up GraphQL without its fancy friends but just vanilla graphql package. In fact, in the next post when we introduce using PostgreSQL, we won’t be using any ORM or SQL builder either. The whole point of doing this is not about giving up using third parties entirely, but to actually see that it’s not so difficult to live without them, and most importantly, we will be able to gain a deeper understanding of how things work and appreciate how much third parties make life easier by abstracting away the heavy works.

So, what are we gonna build? Well, we will create a simple authentication service that consists of a GraphQL server & PostgreSQL database for users to register and log in, as well as have their login status managed via sessions.

This will be a mini-series consisting of five posts like below:

  1. Setting Up Application (this post)
  2. Refactoring
  3. Adding Real Database (TBA)
  4. Session & Authorization (TBA)
  5. Rewrite Schema Definition (TBA)

Sounds simple enough? Let’s get started!

Preparation

First, clone my ExpressJS starter-code from GitHub. It will give us a nice and compact setup for ExpressJS, Typescript, Eslint, and Prettier. Then we will go inside and run yarn to install all the dependencies. After that run yarn start and make sure you see {"status": "success"} at localhost:8000.

(Most likely, there will be errors the first time you run yarn start. Just Ctrl-C to terminate and run again)

git clone https://github.com/ChunML/expressjs-starter server
cd server
yarn
yarn start

Next, let’s open up and examine index.ts inside the src folder. Right now we have a bare minimum setup for an ExpressJS app with only one endpoint.

import express from "express";

const main = (): void => {
  const app = express();

  app.get("/", (_, res) => {
    res.json({ status: "success" });
  });

  app.listen(8000, () => {
    console.log(
      "server is listening at http://localhost:8000. Check it outtttttt!"
    );
  });
};

main();

Lastly, we need to install a few dependencies to set up a GraphQL server:

yarn add graphql express-graphql

Schema Definition

With GraphQL installed, now we can define the schema for the GraphQL server. We will need one ObjectType to define the User type, two Queries for getting one single user/multiple users, and two Mutations for register and login functionality. Using buildSchema function from graphql, it’s gonna look like this:

...
import { buildSchema } from "graphql";

const schema = buildSchema(`
  type User {
    id: ID!
    username: String
    password: String
    createdAt: String
    lastLogin: String
  }

  type Query {
    getUser(id: ID!): User
    getUsers: [User]
  }

  type Mutation {
    register(username: String, password: String): User
    login(username: String, password: String): User
  }
`);

Next, we need to define a User class that holds the actual data for the User type. We also implement the constructor method to make the initialization easier:

const schema = {
  ...
}

class User {
  id: string;

  username: string;

  password: string;

  createdAt: Date;

  lastLogin: Date;

  constructor({ id, username, password, createdAt, lastLogin }: User) {
    this.id = id;
    this.username = username;
    this.password = password;
    this.createdAt = createdAt;
    this.lastLogin = lastLogin;
  }
}

Okay, what else do we need? Since we’re not using a database today (I will talk about database setup in the next post), let’s define a dictionary-like object to serve as an in-memory database. It will have User‘s id as keys and the User object as values. And by the way, this is how we define such objects in Typescript:

class User {
 ...
}

const database: { [id: string]: User } = {};

Let’s feed in a few data before setting up the GraphQL server:

const database ...

database["1"] = new User({
  id: "1",
  username: "chun",
  password: "chun",
  createdAt: new Date(),
  lastLogin: new Date(),
});
database["2"] = new User({
  id: "2",
  username: "trung",
  password: "trung",
  createdAt: new Date(),
  lastLogin: new Date(),
});

Alright, cool. Now we can go ahead and set up the GraphQL server. We achieve that by using graphqlHTTP, which acts as a middleware to ExpressJS so that we both have a REST server and a GraphQL server sitting on top of it. Inside index.ts, modify the main function:

const main = (): void => {
  const app = express();

  app.use(
    "/graphql",         // GraphQL endpoint
    graphqlHTTP({
      schema,           // short of schema: schema
      rootValue: {},    
      graphiql: true,   // enable GraphiQL (a playground for GraphQL)
    })
  );

  ...
}

Now let’s fire up the server by running yarn start, head over to localhost:8000/graphql, we can see an interface to play with GraphQL called GraphiQL:

Fig 1: GraphiQL interface

Now, if we try to run a Query, say, the easiest one: getUsers, we will receive null as a result.

Fig 2: getUsers returns null

Oops! Let’s figure out why in the next session.

Resolver Implementation

How can that be? Well, if you remember, we only declared the Query & Mutation types, we haven’t provided an implementation for them yet. All we have to do is to add behaviors to each of getUser, getUsers, register, and login types. And those behaviors are called resolvers in GraphQL.

So, where do we put them, then? Inside the empty object that we passed to rootValue above.

Let’s start with getUsers, shall we? We know that we want to return an array of all available users, so this is how we’re gonna implement it:

...
  app.use(
    "/graphql",
    graphqlHTTP({
      schema,
      rootValue: {
        getUsers: (): User[] => Object.keys(database).map((id) => database[id]),  // getUsers resolver
      },
      graphiql: true,
    })
  );
...

Let’s switch to the browser and try to run the query one more time:

Fig 3: getUsers shows up data after implementing the resolver

We can see the data showing up now. Yay! Let’s move on and implement getUser query. This is a little bit more special because it requires an id argument:

      rootValue: {
        getUser: ({ id }: { id: string }): User | null => {
          const userIds = Object.keys(database).filter((_id) => _id === id);
          if (userIds.length === 0) {
            return null;
          }
          const userId = userIds[0];
          return database[userId];
        },
        getUsers: (): User[] => ...
      },

Now, if you look at the function, you might wonder why we have to pass id inside an object. That is because a resolver expects a parameter named args, which will contain all the arguments that got passed to its function. So what we’re doing here is to simply destructure that parameter and get the id argument from it. Okay, let’s test it out:

Fig 4: getUser returns result if there is any matching user
Fig 5: getUser when there is no matching user

If there is a user that matches the id, then we give that user back, otherwise, we simply return null. The tests above confirmed the logic we wanted. Cool!

So we have done with the Query types, let’s move on to the Mutation types. This time, instead of getting the data from memory, we will insert/update the data into memory. The logic shouldn’t be that different though. Do you want some challenge? Pause here and try implementing them yourselves!

We will start off by implementing the resolver for login type. If username doesn’t exist or if password doesn’t match, we will return null, otherwise, we will update the lastLogin attribute and return the User object. The code looks something like this:

      rootValue: {
        getUser: ...,
        getUsers: ...,
        login: ({
          username,
          password,
        }: {
          username: string;
          password: string;
        }): User | null => {
          const userIds = Object.keys(database).filter(
            (id) => database[id].username === username
          );
          if (userIds.length === 0) {
            return null;
          }
          const user = database[userIds[0]];
          if (user.password !== password) {
            return null;
          }

          user.lastLogin = new Date();

          return user;
        },

The result when login is successful is as follows. You can check for the case when login failed by yourselves, you should see null get returned.

Fig 6: login successful

Lastly, let’s tackle the register‘s resolver. What we’re gonna do is to check if that username has been taken first. In case it hasn’t, we will add a new User into our in-memory database. Alright, let’s code:

      rootValue: {
        getUser: ...,
        getUsers: ...,
        login: ...,
        register({
          username,
          password,
        }: {
          username: string;
          password: string;
        }): User | null {
          const isValid =
            Object.values(database).filter((user) => user.username === username)
              .length === 0;
          if (!isValid) {
            return null;
          }
          const newId = (Object.keys(database).length + 1).toString();
          const user = new User({
            id: newId,
            username,
            password,
            createdAt: new Date(),
            lastLogin: new Date(),
          });
          database[newId] = user;

          return user;
        },

It’s time to check our new code for register‘s resolver:

Fig 7: register successful

If we execute it one more time, i.e. register with an existing username, we will receive null back. Please confirm that to be sure 😉

Password Hashing

Now, there’s one more thing that we can do to improve the register’s resolver: we don’t want to save naked passwords like that. As usual, let’s add a step to hash the password before inserting into the database.

How to hash the password is up to you. You can either manually hash using hashing functions or even better, rely on password hashing packages such as Bcrypt or Argon2. I typically like the latter better, since they provide functions for both hashing and verifying passwords. There are ongoing debates on whether Bcrypt or Argon2 works better than the other, I couldn’t care less because they both work well for me.

For this particular project, I’m gonna choose Argon2 so let’s go ahead and install it:

yarn add argon2

The first thing we need to update is the seed data. But wait a minute, don’t we have a register function now? We can go ahead and comment out the chunk of code that we used to seed initial data to the database. There’s another reason for not using those lines, more on that in a second.

// database["1"] = new User({
//   id: "1",
//   username: "chun",
//   password: "chun",
//   createdAt: new Date(),
//   lastLogin: new Date(),
// });
// database["2"] = new User({
//   id: "2",
//   username: "trung",
//   password: "trung",
//   createdAt: new Date(),
//   lastLogin: new Date(),
// 

Alright, great. We can update our register logic now. To use Argon2 to hash passwords is simple, just call its hash function on the raw password.

        register({
          username,
          password,
        }: {
          username: string;
          password: string;
        }): User | null {
          ...
          const newId = (Object.keys(database).length + 1).toString();
          const hashedPassword = argon2.hash(password);         // hash the password with argon2
          const user = new User({
            id: newId,
            username,
            password: hashedPassword,                           // use the hashed password
            createdAt: new Date(),
            lastLogin: new Date(),
          });
          database[newId] = user;

          return user;
        },

Compilation error, wasn’t it? Since hash returns a Promise, let’s await it and turn register’s resolver into an async function. That’s also the reason why we should not update the seed data since we have to wrap that code into an async function too. That’s too much!

Our new register function looks like this:

        async register({                                            // register is now an async function
          username,
          password,
        }: {
          username: string;
          password: string;
        }): Promise<User | null> {
          const isValid =
            Object.values(database).filter((user) => user.username === username)
              .length === 0;
          if (!isValid) {
            return null;
          }
          const newId = (Object.keys(database).length + 1).toString();
          const hashedPassword = await argon2.hash(password);       // await until hashing finishes
          const user = new User({
            id: newId,
            username,
            password: hashedPassword,
            createdAt: new Date(),
            lastLogin: new Date(),
          });
          database[newId] = user;

          return user;
        },

Let’s test this out to make sure it still works:

Fig 8: register with hashed password

We can confirm that register resolver still works, and the password was hashed before saved to the database. Great!

If we try to log in now, we couldn’t anymore because the password was hashed. So, inside login resolver, we need to update the logic to check if the input password is correct. Again, we will use a function called verify that Argon2 provides. Pretty neat, isn’t it? Here’s what our new login looks like:

        login: async ({
          username,
          password,
        }: {
          username: string;
          password: string;
        }): Promise<User | null> => {
          const userIds = Object.keys(database).filter(
            (id) => database[id].username === username
          );
          if (userIds.length === 0) {
            return null;
          }
          const user = database[userIds[0]];

          const isPasswordValid = await argon2.verify(user.password, password);
          if (!isPasswordValid) {
            return null;
          }

          user.lastLogin = new Date();
          return user;
        },

Again, let’s make sure everything works as expected:

Fig 9: login with hashed password
Fig 10: getUsers still works
Fig 11: as well as getUser!

Okay, cool! Everything is working properly. So, eventually, our index.ts will look like below:

import express from "express";
import argon2 from "argon2";
import { graphqlHTTP } from "express-graphql";
import { buildSchema } from "graphql";

const schema = buildSchema(`
  input UsernamePasswordInput {
    username: String
    password: String
  }

  type User {
    id: ID!
    username: String
    password: String
    createdAt: String
    lastLogin: String
  }

  type Query {
    getUser(id: ID!): User
    getUsers: [User]
  }

  type Mutation {
    register(username: String, password: String): User
    login(username: String, password: String): User
  }
`);

class User {
  id: string;

  username: string;

  password: string;

  createdAt: Date;

  lastLogin: Date;

  constructor({ id, username, password, createdAt, lastLogin }: User) {
    this.id = id;
    this.username = username;
    this.password = password;
    this.createdAt = createdAt;
    this.lastLogin = lastLogin;
  }
}

const database: { [id: string]: User } = {};
// database["1"] = new User({
//   id: "1",
//   username: "chun",
//   password: "chun",
//   createdAt: new Date(),
//   lastLogin: new Date(),
// });
// database["2"] = new User({
//   id: "2",
//   username: "trung",
//   password: "trung",
//   createdAt: new Date(),
//   lastLogin: new Date(),
// });

const main = (): void => {
  const app = express();

  app.use(
    "/graphql",
    graphqlHTTP({
      schema,
      rootValue: {
        getUser: ({ id }: { id: string }): User | null => {
          const userIds = Object.keys(database).filter((_id) => _id === id);
          if (userIds.length === 0) {
            return null;
          }
          const userId = userIds[0];
          return database[userId];
        },
        getUsers: (): User[] => Object.keys(database).map((id) => database[id]),
        login: async ({
          username,
          password,
        }: {
          username: string;
          password: string;
        }): Promise<User | null> => {
          const userIds = Object.keys(database).filter(
            (id) => database[id].username === username
          );
          if (userIds.length === 0) {
            return null;
          }
          const user = database[userIds[0]];

          const isPasswordValid = await argon2.verify(user.password, password);
          if (!isPasswordValid) {
            return null;
          }

          user.lastLogin = new Date();
          return user;
        },
        async register({
          username,
          password,
        }: {
          username: string;
          password: string;
        }): Promise<User | null> {
          const isValid =
            Object.values(database).filter((user) => user.username === username)
              .length === 0;
          if (!isValid) {
            return null;
          }
          const newId = (Object.keys(database).length + 1).toString();
          const hashedPassword = await argon2.hash(password);
          const user = new User({
            id: newId,
            username,
            password: hashedPassword,
            createdAt: new Date(),
            lastLogin: new Date(),
          });
          database[newId] = user;

          return user;
        },
      },
      graphiql: true,
    })
  );

  app.get("/", (_, res) => {
    res.json({ status: "success" });
  });

  app.listen(8000, () => {
    console.log(
      "server is listening at http://localhost:8000. Check it outtttttt!"
    );
  });
};

main();

And we’re done! Congratulations! In case you encountered any problems and want to compare to my code, please clone my GitHub repo and check out the branch for this post:

git clone https://github.com/ChunML/vanilla-graphql
cd vanilla-graphql
git checkout 1-setting-up-application

Conclusions

Alright, guys. Thank you for reading this long, we have reached the end. Today we have successfully set up a GraphQL server, defined a schema and implemented resolvers so that we can get users’ information, register a new user, and log an existing user in. We’re on our way to create an authentication functionality for our future projects!

In the next post, we will discuss how to migrate from in-memory object-type database to using PostgreSQL. We will also explore how to properly use session to manage login status to finish off the backend of our application. Until then, take good care of yourself and I’m gonna see you soon.

Trung Tran is a Deep Learning Engineer working in the car industry. His main daily job is to build deep learning models for autonomous driving projects, which varies from 2D/3D object detection to road scene segmentation. After office hours, he works on his personal projects which focus on Natural Language Processing and Reinforcement Learning. He loves to write technical blog posts, which helps spread his knowledge/experience to those who are struggling. Less pain, more gain.

Leave a reply:

Your email address will not be published.