Ariel Costas' Blog

Refactor Typescript Graphql Server

For the last few days I’ve been refactoring my current project’s GraphQL backend, removing A LOT of spaghetti code and many inconsistencies. Here are some lessons I’ve learned:

Choose a naming strategy… and stick to it

When you maintain many resolvers with various queries and mutations each, field resolvers, and a bunch of entities; it’s a good idea to have a naming convention (using either the language’s, the framework’s or your own) for your fields, queries, mutations, enums, classes and files.

In my case, I’ve chosen to follow these guidelines created by myself.

├── .env
├── .gitignore
├── package.json
├── README.md
├── src
│   ├── middleware
│   ├── models
│   ├── resolvers
│   ├── types
│   ├── database.ts
│   ├── index.ts
│   └── server.ts
└── test

But these conventions are up to you.

Adding files to your project is FREE

It’s easier to maintain having 2 files for 2 functions that keeping both in the same, with a million imports. Let me give you an example:

function sum(num1: number, num2: number): number {
	return num1 + num2;
}

function subtract(num1: number, num2: number): number {
	return num1 - num2;
}

export { sum, subtract };

We have two functions, one that sums two numbers and another one that subtracts them. The file has 9 lines in total, but the only thing in common between these two functions is they are both mathematical operations. The first problem is how do I call this file? You could call it mathOps.ts, but it doesn’t export a function called mathOps nor has all the mathematical operations you would expect. Instead, it’s better to create a folder called “math” and have a sum.ts that exports the first function and a subtract.ts that exports the other one.

Use DTOs when you have multiple input variables

Let’s say you have a createEmployee operation, which receives the following arguments to create a user: Login ID, First Name, Last Name, Email, Password, Birthday and Position. On Type-GraphQL it would result in the following method:

	@Authorized("CEO", "HR") // Who can run the mmutation
	@Mutation(() => Employee)
	public async createEmployee(
		@Ctx() {db}: MyContext,
		@Arg("login_id") login_id: string,
		@Arg("first_name") first_name: string,
		@Arg("last_name") last_name: string,
		@Arg("email") email: string,
		@Arg("password") plain_password: string,
		@Arg("birthday") birthday: string,
		@Arg("position") position: string,
	): Promise<Employee> {
		// ...
	}

14 whole lines only to define a method to register an employee, and we still have to validate that all the fields are how they should be (email is an actual email and not just “asdasd”, password has at least 8 characters, login_id is not already in use, birthday is an actual date)…

Instead, what you could do is define a DTO (Data Transfer Object) and have your fields there. You could also use a package like class-validator to validate the input.

If you’re using an ORM, take advantage of what it provides you.

In this project, I’m using MikroORM, which has a findOneOrFail method, which works like findOne but throws an error if the entity you’re looking for doesn’t exist.

Until this big refactor I’m doing, I didn’t know about the existence of this method, so I was doing this ugly thing:

const employee = await em.findOne(Employee, login_id);
if (!employee) {
	throw new NotFoundError("Employee not found");
}

Instead, I could do the following, which is much cleaner and saves us several lines repeated through all the codebase:

const employee = await em.findOneOrFail(Employee, login_id);

Add tests before doing big changes (or deploying them to production)

Doesn’t it happen to you that you’re refactoring the whole codebase and changing things, and when you (or your team) try to make it work well with the rest of the application (i.e. frontend) it just doesn’t work for some reason?

That’s where testing comes in place, you create a test, your code passes them. You change your code, run the tests again, and they still work.

Also, don’t forget to document everything you do, because when merging you can read the code, but it’s much better to see what you say you did (or what you think you did). Here’s a guide on how to keep a changelog.

Conclusion

Instead of spending 2 hours adding some small feature, spend a day or two refactoring and fixing stuff, as it will make your codebase much cleaner and easier to maintain. Apart from the tips I mentioned here, I recommend applying SOLID principles, writing Clean Code and avoiding to write the same functionality multiple times (aka DRY).