A couple of weeks ago I wrote about the Jenyus graphql-utils
package on my personal blog, showing how we could leverage the GraphQL AST to optimize relational queries and avoid dataloaders entirely. Today we're going to look at how Jenyus uses the nestjs-graphql-utils
package we built on top of it to create scalable and performant APIs such as the one used in Recog.
The Problem
GraphQL provides a lot of flexibility for clients to request related data to an object in the same query. Whereas in REST you would make a request to the /api/posts
endpoint, followed by /api/users/:id
for each post, in GraphQL we can write a very concise query like so that achieves the same thing:
{
posts {
id
title
body
author {
id
username
firstName
lastName
}
}
}
A barebones implementation of a GraphQL API here will first call the posts
resolver, and then the Post.author
resolver, wherein we would do another SELECT
to our database to fetch each post's author. This can be somewhat optimized using dataloader, but it isn't perfect. You can read more about dataloader here.
The reason this approach is flawed is because we still run individual queries for each post's author that our dataloader hasn't cached yet, which introduces a ton of latency and doesn't make use of SQL JOIN
s for one-to-one relations.
Our Boilerplate
We're going to be using the Jenyus NestJS MikroORM Starter Template as our base to understand how we can use the nestjs-graphql-utils
package in combination with a powerful ORM like MikroORM to optimize our SQL queries. You can learn more about this starter here.
Once you have the boilerplate setup, you'll see the available relationships are already optimized using nestjs-graphql-utils
.
The GraphQL Objects
Our Post
entity is mapped to the GraphQL schema with our PostObject
DTO. This allows us to independently define the fields from our post entity, which we want to expose. As you can see, though, we do not define the relationships in here.
src/posts/dto/post.object.ts
:
@ObjectType("Post")
export class PostObject {
@Field(() => Int)
readonly id: number;
@Field()
readonly title: string;
@Field()
readonly body: string;
@Field()
readonly createdAt: Date;
@Field()
readonly updatedAt: Date;
}
The reason we do not define the Post.author
relationship in here is because we can do so directly in the PostsResolver.author
field resolver. This is a method, which we will use to check if the author for the parent post has been already fetched, and as a fallback we can fetch the author directly from our database for deeper nested relationships or if the previous optimization fails.
src/posts/posts.resolver.ts
:
@Resolver(() => PostObject)
export class PostsResolver {
constructor(
private readonly postsService: PostsService,
private usersService: UsersService,
) {}
@ResolveField(() => UserObject)
async author(@Parent() post: Post) {
if (post.author) {
return post.author;
}
return await this.usersService.findOne({ postId: post.id });
}
}
The Solution
The goal now is to have post resolvers already become aware of when a client requests the author, in order to directly do a JOIN
in our SQL query using MikroORM. We can do this using one of the many decorators provided by nestjs-graphql-utils
.
Using the GraphQL-Utils @Selections()
Decorator
The boilerplate uses the most straightforward approach with the @Selections()
decorator. Given a parent field, and subselections, graphql-utils
will recursively find all the selections that were made and return them as an array of strings. This is compatible with many ORMs such as TypeORM and MikroORM, making it very suited for this use-case.
In our PostsResolver.posts
and post
query, we pass on the resolved selections to our PostsService
, which relays that information to MikroORM in our case.
src/posts/posts.resolver.ts
:
@Resolver(() => PostObject)
export class PostsResolver {
constructor(
private readonly postsService: PostsService,
private usersService: UsersService,
) {}
@Query(() => [PostObject])
posts(@Selections("posts", ["author"]) relations: string[]) {
return this.postsService.findAll({ relations });
}
}
src/posts/posts.service.ts
:
interface FindAllArgs {
relations?: string[];
authorId?: number;
}
@Injectable()
export class PostsService {
constructor(
@InjectRepository(Post)
private postsRepository: EntityRepository<Post>,
) {}
findAll(args?: FindAllArgs) {
const { relations, authorId } = args;
let where: FilterQuery<Post> = {};
if (authorId) {
where = { ...where, author: { id: authorId } };
}
return this.postsRepository.find(where, relations);
}
}
Using the GraphQL-Utils @HasFields()
Decorator
The @HasFields()
decorator returns a boolean if it finds the fields in a given query. This can be useful if you want to run further checks, such as calculating complexity, which is a feature NestJS provides out of the box but you can also implement on your own if you want to avoid the overhead of separate packages or gain more control over the feature. You can read more about how NestJS can calculate query complexity here.
The functionality will overall be the same, we will check if the field was requested, and then add it to our relations
array to let the service know the author
needs to be populated as well.
src/posts/posts.resolver.ts
:
@Resolver(() => PostObject)
export class PostsResolver {
constructor(
private readonly postsService: PostsService,
private usersService: UsersService,
) {}
@Query(() => [PostObject])
posts(@HasFields("posts.author") wantsAuthor: boolean) {
let relations: string[] = [];
if (wantsAuthor) {
relations = [...relations, "author"];
}
return this.postsService.findAll({ relations });
}
}
Using the GraphQL-Utils @Fields()
Decorator
@Fields()
and @FieldsMap()
(see below) can be extremely useful if you want to do many things with the GraphQL AST, and be as efficient as possible, since every time @Selections()
or @HasFields()
is called the entire AST needs to be recursively scanned, you can use the more raw approach to improve performance.
@Fields()
returns an array of dot-notated strings, denoting which fields were selected in the query by the client. We can either use it like @HasFields()
to check if the field we're trying to resolve was selected (posts.author
) or create something more sophisticated that can resolve all our relationships. In our current schema we know, post relationships are simply fields that have nested selectors, so we can write a Javascript reducer to do just that.
src/posts/posts.resolver.ts
:
@Resolver(() => PostObject)
export class PostsResolver {
constructor(
private readonly postsService: PostsService,
private usersService: UsersService,
) {}
@Query(() => [PostObject])
posts(@Fields() fields: string[]) {
const relations: string[] = fields.reduce((fields, field) => {
// check if it's a relational field by checking if it has subselections
if (field.split(".").length > 2) {
// we need to check if the relation has already been added to our array
if (!fields.includes(field.split(".")[1])) {
return [...fields, field.split(".")[1]];
}
}
return fields;
}, []);
return this.postsService.findAll({ relations });
}
}
Using the GraphQL-Utils @FieldMap()
Decorator
FieldMap
is the interface used throughout the graphql-utils
library to make utilities like hasFields()
and resolveSelections()
possible. You can read more about it here.
Given all the utilities are built around it, it can also be used to optimize the way we work with the AST. graphql-utils
provides many utilities that we can use to convert this into different data structures or filter certain items.
To solve our current problem, all we need to do here is create a function that works in a similar fashion to the reducer used above, only this time we'll be checking if the object has any subselections instead of parsing the dot-notation, making it even more straightforward.
src/posts/posts.resolver.ts
:
@Resolver(() => PostObject)
export class PostsResolver {
constructor(
private readonly postsService: PostsService,
private usersService: UsersService,
) {}
@Query(() => [PostObject])
posts(@FieldMap() fieldMap: FieldMap) {
const relations: string[] = Object.keys(fieldMap.posts).filter(
(key) => Object.keys(fieldMap.posts[key]).length,
);
return this.postsService.findAll({ relations });
}
}
Verdict
NestJS already allows us to write efficient and easy to understand GraphQL APIs using their unique module architecture and resolvers. Instead of using the generic dataloader method to cache results and mostly only optimize many-to-many relationships, we can leverage native features provided by SQL databases to decrease latency and improve mapping performance through our ORM.
The Jenyus nestjs-graphql-utils
package is a great add-on for GraphQL codebases of any size, and offers a lot of versatility in its implementation. This guide should get you on the track of understanding the GraphQL system and what its capable of, so you can implement your own performant GraphQL APIs.