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:
- Setting Up Application
- Refactoring (this post)
- Adding Real Database (TBA)
- Session & Authorization (TBA)
- 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:

register
– no problem
login
– no problem
getUser
– no problem
getUsers
– no problemGreat! 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)
.

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:

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!