Vanilla GraphQL With NodeJS And PostgreSQL: Refactoring

Reading Time: 7 minutes

Hello, fellas! So glad to see you guys in the second post of the mini-series: Vanilla GraphQL With NodeJS And PostgreSQL. If you guys missed the previous post or want to jump up to the next post, please click the links below:

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

So, in the previous post, we already set up the application with a GraphQL server. I told you guys that in this post, we will continue to introduce PostgreSQL to replace the in-memory database. However, I had a change of thoughts since it would involve refactoring the source code and then the actual work of introducing PostgreSQL itself. It would be too overwhelming to digest in one single post. That’s why I decided to split it into two separate posts like above. Believe me, in the next post, you will see how easy it gets to start off with an already refactored codebase.

Alright. Let’s get started!

Preparation

In order to follow along this post, you can use the code that you’ve written last time. Or you can clone from my GitHub and check out the right branch as follows:

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

After that, let run yarn start to make sure we can fire up the application. Also, check the behaviors of all functions that we implemented last time: getUser, getUsers, register, and login. If nothing strange happens, we’re good to go to the next section.

Creating DB Class

The first thing we can do as an attempt to introduce a real database is to mimic the way that we’re gonna use it. If you have experience working with ORM (object-relational mapping) or any SQL Building libraries before, then the following steps may be fairly familiar. In case you don’t, don’t worry, it’s not the end of the world. Normally, in order to work with a database, the majority of times it would only involve the following tasks:

  • Define entities
  • Connect to the database & create tables
  • Perform queries

Since we’re not using a real database today, we will tackle the first two tasks in the next post and get prepared for the last one. What we’re about to do is to create a class to encapsulate all database related functionality. More details in a second.

First, let’s comment out the following line that creates the in-memory database:

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

Then, we will create a new class called DB with a field called database and a constructor to initialize it (i.e. seed initial data).

class DB {
  database: { [id: string]: User };

  constructor(database: { [id: string]: User }) {
    this.database = database;
  }
}

Next, let’s talk about query methods. Remember in REST API, we have CRUD which stands for create, read, update, and delete. However, it’s not necessarily limited to REST API. In fact, it applies to everything that involves setting/retrieving data to/from persistent storage. If you notice, we’re also using three of them within our application (we don’t need delete functionality but we can implement in case we need it).

  • read: getUser/getUsers
  • create: register
  • update: login

So, technically speaking, we already have everything we need. We only need to change one thing: the naming convention. It’s good to use the naming that tells us what it’s gonna do to the database.

Let’s get started with getUser. Its equivalent name is findOne so let’s create a method named fineOne inside DB class and move the logic of getUser into it. Note that we also need to use this.database instead of database, since we are using the database field of the DB class.

class DB {
  database: { [id: string]: User };

  constructor(database: { [id: string]: User }) {
    this.database = database;
  }

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

Next, we will implement the equivalent function of getUsers for the DB class, which we can call findAll:

class DB {
  database: { [id: string]: User };

  constructor(database: { [id: string]: User }) {...}

  findOne(id: string): User | null {...}

  findAll(): User[] {
    return Object.keys(this.database).map((id) => this.database[id]);
  }
}

Then, it’s time for the register function, which involves a database insertion so that it’s normally called insert:

class DB {
  database: { [id: string]: User };

  constructor(database: { [id: string]: User }) {...}

  findOne(id: string): User | null {...}

  findAll(): User[] {...}

  async insert(input: {
    username: string;
    password: string;
  }): Promise<User | null> {
    const { username, password } = input;
    const isValid =
      Object.values(this.database).filter((user) => user.username === username)
        .length === 0;
    if (!isValid) {
      return null;
    }
    const newId = (Object.keys(this.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(),
    });
    this.database[newId] = user;

    return user;
  }
}

Last but not least, let’s move onto our login function. login involves updating the database (i.e. changing the lastLogin value), and we can name the method for such behavior update:

class DB {
  database: { [id: string]: User };

  constructor(database: { [id: string]: User }) {...}

  findOne(id: string): User | null {...}

  findAll(): User[] {...}

  async insert(input: {...}

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

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

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

Using the DB Class

Alright, cool! So now the DB class is complete. Next, let’s go ahead and initialize the DB class with an empty object inside the main function:

const main = (): void => {
  const db = new DB({});
  ...
}

Next, we can update rootValue in graphqlHTTP to use the methods that we’ve just implemented in DB class:

  app.use(
    "/graphql",
    graphqlHTTP({
      schema,
      rootValue: {
        getUser: ({ id }: { id: string }): User | null => db.findOne(id),
        getUsers: (): User[] => db.findAll(),
        login: async (input: {
          username: string;
          password: string;
        }): Promise<User | null> => db.update(input),
        register: async (input: {
          username: string;
          password: string;
        }): Promise<User | null> => db.insert(input),
      },
      graphiql: true,
    })
  );

Great stuff! Before moving on, in order to make sure we didn’t break anything, let’s test the application real quick:

Fig 1: register – no problem
Fig 2: login – no problem
Fig 3: getUser – no problem
Fig 4: getUsers – no problem

Great! Our application is still working properly. Now, if you notice, index.ts has become quite junky now. It’s time to split up our code.

Refactoring

First, we need a file to export all necessary types, rather than defining them all inside index.ts. Let’s create a new file called types.ts inside src folder:

touch types.ts

As of right now, we only have only one type for User. Later on, we will have a few more but first, let’s move the User class from ./src/index.ts to ./src/types.ts:

// eslint-disable-next-line import/prefer-default-export
export 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;
  }
}

Since we moved out the User class, we need to import it back inside ./src/index.ts:

import { User } from "./types";

Great. Coming up next, let’s create a new folder named db. Inside that new folder, also create a new file called index.ts. This will be responsible for database configuration & functionality encapsulation.

// make sure you are inside src folder
mkdir db
touch db/index.ts

Next, let’s move our DB class from ./src/index.ts into our newly created ./src/db/index.ts:

import argon2 from "argon2";
import { User } from "../types";

export default class DB {
  database: { [id: string]: User };

  constructor(database: { [id: string]: User }) {
    this.database = database;
  }

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

  findAll(): User[] {
    return Object.keys(this.database).map((id) => this.database[id]);
  }

  async insert(input: {
    username: string;
    password: string;
  }): Promise<User | null> {
    const { username, password } = input;
    const isValid =
      Object.values(this.database).filter((user) => user.username === username)
        .length === 0;
    if (!isValid) {
      return null;
    }
    const newId = (Object.keys(this.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(),
    });
    this.database[newId] = user;

    return user;
  }

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

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

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

Once again, don’t forget to import it back inside ./src/index.ts:

import DB from "./db"

Next, let’s move the schema definition out. Create a new file called schema.ts and fill in like below:

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
    name(id: String): String
  }

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

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

export default schema;

About to say “don’t forget the import”, right? We got it, Trung!

import schema from "./schema";

The last bit we need to do to make index.ts clean, is to move rootValue object to a separate file. Here’s where things get interesting. Let’s take one more look at rootValue object again:

      rootValue: {
        getUser: ({ id }: { id: string }): User | null => db.findOne(id),
        getUsers: (): User[] => db.findAll(),
        login: async (input: {
          username: string;
          password: string;
        }): Promise<User | null> => db.update(input),
        register: async (input: {
          username: string;
          password: string;
        }): Promise<User | null> => db.insert(input),
      },

We got some situation here: rootValue object is depending on the db object, which we created at the beginning of our main function. Theoretically, we could do something like this:

createRootValue = (db: DB) => ({
  getUser: ({ id }: { id: string }): User | null => db.findOne(id),
  getUsers: (): User[] => db.findAll(),
  login: async (input: {
    username: string;
    password: string;
  }): Promise<User | null> => db.update(input),
  register: async (input: {
    username: string;
    password: string;
  }): Promise<User | null> => db.insert(input),
})

...

rootValue: createRootValue(db)

That’ll work without any problems, but there is a better approach: using context.

What the heck is context and how can we use it?

Going into details on this would be too long and I think I’m gonna write another post for it. Now, to give a short answer to the questions above, let’s recap a little bit. In GraphQL schemas, we have Type (Query and Mutation are special types, User is a custom type). Within each type, we have Field (id, username, password, createdAt, lastLogin are User‘s fields, getUser and getUsers are Query’s fields, etc). The resolver functions that we implemented in the last post are fully called field resolvers (to distinguish with type resolvers). And we can see from our schema definition, fields can be either a value (like id or username) or a function (like getUser or register).

In case some field resolver is a function, we defined them as a function having one args parameter (getUser, register, and login) or no parameter (getUsers). In fact, (if we defined our resolver functions in rootValue), their full signature should be: anyFieldResolver(args, context, info).

If you’re curious, you can look through graphql package’s code to understand the logic behind it (don’t afraid to go into node_modules folder. I even put a bunch of console.log in there when debugging 😉). Eventually, you would be looking at something like below. The line we want to focus is source[info.fieldName](args, contextValue, info).

Fig 5: the full signature of field resolver functions (if defined in rootValue)

In order to test the theory, let’s modify the getUser resolver to print out the context object, like this:

      rootValue: {
        getUser: ({ id }: { id: string }, context: any): User | null => {
          console.log(context);
          return db.findOne(id);
        },
        ...
      }

We’re gonna see a gigantic context object get printed out:

Fig 6: context object (a part of)

Alright, enough with all the theory. So we have a context object, how does that benefit us?

Well, we can put some necessary information (like the db object) into it, which means that we can pull that information out from the inside via the context object.

So here’s what we’re gonna do. graphqlHTTP has an option called context so that we can overwrite the context object. Let’s create a context object with just one element: the db object:

  app.use(
    "/graphql",
    graphqlHTTP({
      schema,
      rootValue: {...},
      graphiql: true,
      context: { db },     // put in any additional context information
    })
  );

Now if we run the getUser resolver again, we will see our new context object containing only the db object as expected:

[nodemon] [nodemon] restarting due to changes...
[nodemon] [nodemon] starting `node dist/index.js`
[nodemon] server is listening at http://localhost:8000. Check it outtttttt!
[nodemon] { db: DB { database: {} } }

Great! We have created a new context object. Since we created it, we know what’s inside. Therefore, inside types.ts, let’s define a new type for the new context object so that Typescript can stop complaining.

By the way, let’s also create a new type for the User’s input too, so that we can make the input type of login & register more generic.

import DB from "./db";

// eslint-disable-next-line import/prefer-default-export
export class User {...}

export interface MyContext {
  db: DB;
}

export interface UserInput {
  username: string;
  password: string;
}

And now, we can rewrite all resolvers to get the db object from the context object, instead of accessing it directly. Our rootValue will now look like this:

import { MyContext, User } from "./types";

...

      rootValue: {
        getUser: ({ id }: { id: string }, context: MyContext): User | null =>
          context.db.findOne(id),
        getUsers: (_args: null, context: MyContext): User[] =>
          context.db.findAll(),
        login: async (input: UserInput, context: MyContext): Promise<User | null> =>
          context.db.update(input),
        register: async (
          input: UserInput,
          context: MyContext
        ): Promise<User | null> => context.db.insert(input),
      },

Fantastic! rootValue object can now be moved away from index.ts without having to worry about dependency on db object. Let’s create a new file called resolvers.ts and fill it in there:

import { MyContext, User } from "./types";

// eslint-disable-next-line import/prefer-default-export
export const rootValue = {
  getUser: ({ id }: { id: string }, context: MyContext): User | null =>
    context.db.findOne(id),
  getUsers: (_args: null, context: MyContext): User[] => context.db.findAll(),
  login: async (input: UserInput, context: MyContext): Promise<User | null> =>
    context.db.update(input),
  register: async (
    input: UserInput,
    context: MyContext
  ): Promise<User | null> => context.db.insert(input),
};

And right now inside index.ts, after cleaning up some unnecessary imports, what we have is a super clean file like this:

import express from "express";
import { graphqlHTTP } from "express-graphql";
import DB from "./db";
import schema from "./schema";
import { rootValue } from "./resolvers";

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

  const db = new DB({});

  app.use(
    "/graphql",
    graphqlHTTP({
      schema,
      rootValue,
      graphiql: true,
      context: { db },
    })
  );

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

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

main();

We’re almost done! Let’s do one final test to make sure everything still works as before. If four resolvers can produce the same results as above, congratulations! We have finished refactoring our source code! If you encounter 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 2-refactoring-source-code

Conclusions

We made it until the end. Great job, everyone! In this post, we have spent time refactoring our code from a junky big fat indext.ts into a neatly organized code base. Not only it looks much better to the eyes but in the next post, we’re also gonna find it so much easier to replace the in-memory database with a real one which is PostgreSQL. Setting up database is a real nightmare but I will show you how to utilize the power of docker to get it done in a few simple steps. Until then, take care and stay tuned. I’ll see you guys in the next post!

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.