Next x Nest – GraphQL for REST API developers – Part 2
Tags: webdev, typescript, nestjs, graphql
Published: 2023-12-19 18:17:52

It's been quite a while since I last published. Sorry for being absent. I decided to dedicate some time to personal development. But now I feel like I need to complete this series I had started. So I’m back with yet another article in the Next x Nest series. This one would be a continuation of the last article. In part 1 of the “GraphQL for REST API developers” there were a couple of topics and resolvers left out in favour of length. Let’s complete those. Compared to the last article, this one would be shorter and after this, the frontend side of things would begin.

## Nuances of using a Code-first Approach
But before that, I feel it's important to discuss a few things that are relevant to taking a “code-first” approach to GraphQL API development. These become more significant in the context of NestJS. These are:
1. Code-first approach is easier to relate for a REST API developer but less intuitive than the original GraphQL model.
2. The use of the Code first approach allows us to easily place code sanitization (Nestjs transform and validation decorators) in the data transfer object layer. This is the layer that receives the input from the incoming traffic and processes it in objects which are passed to our handlers/controllers.
3. _Circular dependency_ can catch you from behind if you are not careful designing in NestJS using this approach. 
4. Contrary to popular belief or confusion of newbies, authentication and authorization can be easily implemented in GraphQL. It is easier still when using NestJS for GraphQL.

I am not advocating for NestJS here. I am always in favour of opinionated frameworks as they offer a way to reduce the “chaos in codebase”. I would prefer a framework that forces things to be done in a certain way rather than relying on “community-adopted” best practices. Because at the end of the day, while the latter seems natural, it is often laborious to ingrain in new learners. I have been part of projects where the “previous team” did things in a specific way which was not best practice and that had to be carried forward because it would have upset the timeline to do a refactor. This is why I would advocate for the use of frameworks rather than just libraries.

## Remaining Resolvers
Returning to our little project, the modules that remain are the board and board user modules. So without further ado…

### Board
Before we dive into the module, resolver, and service code, we need a moment to discuss the `Entity`. A board will be represented by a class which we will call `Board` (no surprises there). This class will be annotated by the `@ObjectType()` decorator which will tell GraphQL to use this class to generate the properties of a Board in the GraphQL schema.

```typescript
import { ObjectType, Field, ID, GraphQLISODateTime } from '@nestjs/graphql';
import { BoardUser } from 'src/board-user/entities/board-user.entity';
import { Column } from 'src/column/entities/column.entity';
import { User } from 'src/user/entities/user.entity';

@ObjectType()
export class Board {
  @Field(() => ID)
  id: string;

  @Field(() => GraphQLISODateTime)
  createdAt: string;

  @Field(() => GraphQLISODateTime)
  updatedAt: string;

  @Field(() => String)
  boardName: string;

  @Field(() => String)
  boardDescription: string;
  
  @Field(() => User)
  createdByUser: User;

  @Field(() => [Column], { nullable: 'itemsAndList' })
  columns: Column[];

  @Field(() => [BoardUser], { nullable: 'itemsAndList' })
  boardUsers: BoardUser[];
}


```

Now, as can be seen from the above, albeit fields `createdByUser`, `columns` and `boardUsers` most of the properties are GraphQL scalars. These custom types will need to have resolvers. We will be needing resolvers as you will see in the later section. This is also where the need for importing other modules comes into play.

#### Module
Now the `BoardModule` will be using the `BoardUserModule` and `ColumnModule`. These would also be using `BoardModule` to provide a way for the frontend to query data about Board from the Columns and Board User resolvers. In the code below, we set up the `BoardModule` with `forwardRef()` to the aforementioned modules.

```typescript
import { Module, forwardRef } from '@nestjs/common';
import { BoardService } from './board.service';
import { BoardResolver } from './board.resolver';
import { BoardUserModule } from 'src/board-user/board-user.module';
import { ColumnModule } from 'src/column/column.module';
import { UserModule } from 'src/user/user.module';

@Module({
  imports: [
    forwardRef(() => BoardUserModule),
    forwardRef(() => ColumnModule),
    forwardRef(() => UserModule),
  ],
  providers: [BoardResolver, BoardService],
  exports: [BoardService],
})
export class BoardModule {}

```
#### Resolver
Now, for the resolver, we will need the DTOs in place. These DTOs are for mutations. Specifically, a DTO for board creation and another for board updating. In the DTO below, we decorate the DTO class with the `@InputType()` tag. This tells NestJS to use it as a GraphQL input type in the schema.

> Keep in mind that `@InputType()` and `@ObjectType()` cannot be placed on the same class.

```typescript
import { InputType, Field } from '@nestjs/graphql';
import { IsNotEmpty, IsString } from 'class-validator';

@InputType()
export class CreateBoardInput {
  @IsNotEmpty()
  @IsString()
  @Field(() => String)
  boardName:string

  @IsNotEmpty()
  @IsString()
  @Field(() => String)
  boardDescription:string
}


```

In the above DTO, we place the validations that the mutation must have a board name and description. You might be wondering “What about the access controls?” – we design it in such a manner that the authenticated user sending the mutation is the board creator. The ID of the user is automatically decoded and picked up by the resolver as you will see in the code for the resolver. This creator is the first board user as well. 

For updating a board, we design it in such a way that only the board name and/or board description may be changed. The mutation must provide the board ID. The actual logic for the update is in the service which we will discuss in the next section. The code for the update DTO is given below.

```typescript
import { IsNotEmpty, IsUUID } from 'class-validator';
import { CreateBoardInput } from './create-board.input';
import { InputType, Field, Int, PartialType } from '@nestjs/graphql';

@InputType()
export class UpdateBoardInput extends PartialType(CreateBoardInput) {
  @IsNotEmpty()
  @IsUUID()
  @Field(() => String)
  id: string;
}


```

Finally, moving to the Resolver, we have 3 mutations – one to create called `createBoard`, another to update called `updateBoard` and one to remove the board given the `id` called `removeBoard`. These mutation names are automatically derived from the functions and as you can see by the `@Mutation()` annotations above their respective functions, they return the created, updated and deleted boards respectively. Pay close attention to how these mutations also pick up the authenticated user details using the `@GraphQLUser()` tag we discussed in the previous articles.

```typescript
import {
  Resolver,
  Query,
  Mutation,
  Args,
  Int,
  ResolveField,
  Parent,
} from '@nestjs/graphql';
import { BoardService } from './board.service';
import { Board } from './entities/board.entity';
import { CreateBoardInput } from './dto/create-board.input';
import { UpdateBoardInput } from './dto/update-board.input';
import { GraphQLUser } from 'src/decorators';
import { UserJwt } from 'src/auth/dto/user-jwt.dto';
import { BoardUserService } from 'src/board-user/board-user.service';
import { BoardUser } from 'src/board-user/entities/board-user.entity';
import { Column } from 'src/column/entities/column.entity';
import { ColumnService } from 'src/column/column.service';
import { User } from 'src/user/entities/user.entity';
import { UserService } from 'src/user/user.service';

@Resolver(() => Board)
export class BoardResolver {
  constructor(
    private readonly userService: UserService,
    private readonly boardService: BoardService,
    private readonly boardUserService: BoardUserService,
    private readonly columnService: ColumnService
  ) {}

  @Mutation(() => Board)
  createBoard(
    @Args('createBoardInput') createBoardInput: CreateBoardInput,
    @GraphQLUser() user: UserJwt,
  ) {
    return this.boardService.create(createBoardInput, user.sub);
  }

  @Query(() => [Board], { name: 'boards' })
  findAll(@GraphQLUser() user: UserJwt) {
    return this.boardService.findAll({
      boardUsers: {
        some: {
          userId: user.sub,
        },
      },
    });
  }

  @Query(() => Board, { name: 'board' })
  findOne(
    @Args('id', { type: () => String }) id: string,
    @GraphQLUser() user: UserJwt,
  ) {
    return this.boardService.findOne(id, user.sub);
  }

  @Mutation(() => Board)
  updateBoard(
    @Args('updateBoardInput') updateBoardInput: UpdateBoardInput,
    @GraphQLUser() user: UserJwt,
  ) {
    return this.boardService.update(
      updateBoardInput.id,
      updateBoardInput,
      user.sub,
    );
  }

  @Mutation(() => Board)
  removeBoard(
    @Args('id', { type: () => String }) id: string,
    @GraphQLUser() user: UserJwt,
  ) {
    return this.boardService.remove(id, user.sub);
  }

  @ResolveField('boardUsers', (returns) => [BoardUser])
  async boardUsersResolver(@Parent() board) {
    const { id } = board;
    return this.boardUserService.findAll(undefined, id);
  }

  @ResolveField('columns', (returns) => [Column])
  async columnsResolver(@Parent() board) {
    const { id } = board;
    return this.columnService.findAll({boardId: id});
  }

  @ResolveField(undefined, (returns) => User)
  async createdByUser(@Parent() board) {
    const {createdBy} = board
    return this.userService.findOne(createdBy)
  }
}

```

As can be seen from above, we have two queries (analogous to `get` and `get-all` routes in REST API paradigm). We make sure to pick the authenticated user and utilize the board user details to only return board(s) details the current user is a part of.

Finally, we have the **Field Resolvers**. These are special functions which utilize the *services* of the imported *modules* to provide details about custom fields defined in the Entity. The first argument to `@ResolveField()` tag is the name of the field it is supposed to resolve. If not provided (i.e, passed `undefined`) then NestJS takes it that the name of the field and the name of the method are same. The second arg is the return type. The standard format is to write it that way (`(returns) => <returned value>`).
 
Whenever our board entity is getting resolved and the request asks for the custom fields, the specific board details will be passed via the argument decorated by `@Parent()`. This will help us resolve the field details particular the specific board. In the above resolvers, we utilize the service methods from the imported modules to resolve the fields.

#### Service
Last but not least, the `BoardService`. Keep in mind that in this application we are using two different databases. One is Supabase – which is used to maintain our user details. Another is a Postgres Instance hosted on Render. This is being used to maintain the rest of the entity details. In this service, we need to use the Prisma Client which points to our Render Postgres DB as shown by the `PrismaRenderService` below.

```typescript
import { Injectable } from '@nestjs/common';
import { CreateBoardInput } from './dto/create-board.input';
import { UpdateBoardInput } from './dto/update-board.input';
import { PrismaRenderService } from 'src/prisma-render/prisma-render.service';
import { Prisma } from '@prisma/render';

@Injectable()
export class BoardService {
  constructor(private prisma: PrismaRenderService) {}
  create(createBoardInput: CreateBoardInput, userId: string) {
    return this.prisma.board.create({
      data: {
        ...createBoardInput,
        createdBy: userId,
        boardUsers: {
          create: {
            userId,
          },
        },
      },
    });
  }

  findAll(where: Prisma.BoardWhereInput) {
    return this.prisma.board.findMany({
      where
    });
  }

  findOne(id: string, userId?: string) {
    const filter: Prisma.BoardWhereUniqueInput = {id}
    if(userId) filter.boardUsers = { some: { userId } }
    return this.prisma.board.findUniqueOrThrow({
      where: filter,
    });
  }

  update(
    id: string,
    { id: _id, ...updateBoardInput }: UpdateBoardInput,
    createdBy: string,
  ) {
    return this.prisma.board.update({
      where: {
        id,
        createdBy,
      },
      data: updateBoardInput,
    });
  }

  remove(id: string, createdBy: string) {
    return this.prisma.board.delete({
      where: {
        id,
        createdBy,
      },
    });
  }
}


```

The code in the service file is pretty straightforward. This intuitively shows that under the hood, the business logic stays similar regardless of the data presentation layer. We have our methods which act as a wrapper over our Prisma methods to perform CRUD operations relating to the Board entity.

### BoardUser
The reason `BoardUser` module is special is because, under the hood, we have a different DB instance for storing user profiles. So, in the DB instance storing info related to boards (including users allowed to access a board), we cannot establish a relationship to the user rows. In the second DB, we can only have a string type attribute which will have the UUIDs of the users. 

It is at the business logic layer (NestJs resolver and service layers) that we will tie these together. This will give the user a seamless feel as if all the data is coming from a single DB instance. Between this module and the board and user modules, we would have covered almost all there is to GraphQL in NestJS and in general.

Before diving into the module code, let’s have a look at the `BoardUser` entity below. As you can see, we only have two custom fields – one denoting a user entity (`User`) called `user` and another denoting the board that the user has access to (`Board` entity type).

```typescript
import { Field, ID, ObjectType } from "@nestjs/graphql";
import { Board } from "src/board/entities/board.entity";
import { User } from "src/user/entities/user.entity";

@ObjectType()
export class BoardUser {
  @Field(() => User)
  user: User;

  @Field(() => Board)
  board: Board
}

```

Now the design can vary. You might choose to have a design such that a `BoardUser` entity represents a one-to-many relationship (one user and all the boards the user has access to). But here, for simplicity’s sake, we go with the above design. Feel free to modify it yourself. 

#### Module

```typescript
import { Module, forwardRef } from '@nestjs/common';
import { BoardUserService } from './board-user.service';
import { BoardUserResolver } from './board-user.resolver';
import { BoardModule } from 'src/board/board.module';
import { UserModule } from 'src/user/user.module';

@Module({
  imports: [forwardRef(() => BoardModule), UserModule],
  providers: [BoardUserResolver, BoardUserService],
  exports: [BoardUserService]
})
export class BoardUserModule {}

```
The code for the `BoardUserModule` is pretty simple. We just need to use the `BoardModule` and the `UserModule`. From the usage of `forwardRef()`, you can easily guess that we have imported `BoardUserModule` in `BoardModule` but not in `UserModule`.

#### Resolver
The resolver is the more interesting part of it. This is where we will need to resolve the custom fields. But at the same time, we will need to have CRUD operations in form of mutations and queries for the `BoardUser` entity itself.

```typescript
import {
  Resolver,
  Query,
  Mutation,
  Args,
  ResolveField,
  Parent,
} from '@nestjs/graphql';
import { BoardUserService } from './board-user.service';
import { AddBoardUserInput } from './dto/add-board-user.input';
import { RemoveBoardUserInput } from './dto/remove-board-user.input';
import { BoardService } from 'src/board/board.service';
import { BoardUser } from './entities/board-user.entity';
import { GraphQLUser } from 'src/decorators';
import { UserJwt } from 'src/auth/dto/user-jwt.dto';
import { UserService } from 'src/user/user.service';

@Resolver(() => BoardUser)
export class BoardUserResolver {
  constructor(
    private readonly boardUserService: BoardUserService,
    private readonly boardService: BoardService,
    private readonly userService: UserService,
  ) {}

  @Mutation(() => BoardUser, { name: 'addBoardUser' })
  create(
    @Args('addBoardUserInput') addBoardUserInput: AddBoardUserInput,
    @GraphQLUser() user: UserJwt,
  ) {
    return this.boardUserService.create(addBoardUserInput, user.sub);
  }

  @Query(() => [BoardUser], { name: 'boardUsers' })
  findAll() {
    return this.boardUserService.findAll();
  }

  @Mutation(() => BoardUser, { name: 'removeBoardUser' })
  remove(
    @Args('removeBoardUser') removeBoardUserInput: RemoveBoardUserInput,
    @GraphQLUser() user: UserJwt,
  ) {
    return this.boardUserService.remove(removeBoardUserInput, user.sub);
  }

  @ResolveField()
  async board(@Parent() boardUser) {
    const { boardId } = boardUser;
    console.log('Board User ID', boardUser);
    return this.boardService.findOne(boardId);
  }

  @ResolveField()
  async user(@Parent() boardUser) {
    const { userId } = boardUser;
    return this.userService.findOne(userId);
  }
}

```

In the above, we have 2 mutation resolvers for mutations called called `addBoardUser` and `removeBoardUser`. The mutation names should convey their function. Note how we are also taking in the authenticated user details for these mutations. These would be used in the actual business layer for authorization checks.

We have a query resolver for `boardUsers` query which returns all the `BoardUser` entities stored in the DB. Then we have the field resolvers annotated by `@ResolveField()` tag. In these field resolvers, note how we do not pass any names. So, the names of the functions are taken as the names of the fields they are supposed to resolve. We also extract `boardId` and `userId` to find the particular `Board` and `User` needed using the `BoardService` and the `UserService`.

As for the DTOs, we have two for adding and removing `BoardUser` entries as shown below. Note how in the `AddBoardUserInput` DTO, we take in `userId` and `boardId`. Everything else we do behind the scenes. So, between the resolver and service, we find the user and the board we need to add the user to.

```typescript
import { Field, InputType } from "@nestjs/graphql";
import { IsNotEmpty, IsUUID } from "class-validator";

@InputType()
export class AddBoardUserInput {
  @IsNotEmpty()
  @IsUUID()
  @Field(() => String)
  userId: string;

  @IsNotEmpty()
  @IsUUID()
  @Field(() => String)
  boardId: string;
}
```

Lastly, for the `RemoveBoardUserInput` DTO, we need the same details as above. But for better semantics, we go ahead a step and create a separate DTO. As shown below, the DTO inherits from `AddBoardUserInput` DTO. Since we do not wrap the `AddBoardUserInput` DTO with `PartialType()` or `OmitType()`, you can understand that the GraphQL API user needs to mandatorily provide the `userId` and the `boardId`.

```typescript
import { InputType } from '@nestjs/graphql';
import { AddBoardUserInput } from './add-board-user.input';

@InputType()
export class RemoveBoardUserInput extends AddBoardUserInput {
}

```

#### Service
Lastly, we need to take a look at the service. This design might seem a bit peculiar. When creating a new `BoardUser` using the `create` method, we first check if the user adding the new user is supposed to have access to the board. You can go ahead and have a finer access control. But this is the depth we are going to go to avoid making the whole thing complex.

```typescript
import { ForbiddenException, Injectable } from '@nestjs/common';
import { RemoveBoardUserInput } from './dto/remove-board-user.input';
import { AddBoardUserInput } from './dto/add-board-user.input';
import { PrismaRenderService } from 'src/prisma-render/prisma-render.service';
import { Prisma } from '@prisma/render';

@Injectable()
export class BoardUserService {
  constructor(private prisma: PrismaRenderService) {}
  async create({ userId, boardId }: AddBoardUserInput, existingUserId: string) {
    try {
      await this.prisma.boardUser.findUniqueOrThrow({
        where: {
          userId_boardId: {
            userId: existingUserId,
            boardId,
          },
        },
      });
    } catch (error) {
      if (error.code === 'P2025')
        throw new ForbiddenException('User not authorized');
    }
    return this.prisma.boardUser.create({
      data: {
        userId,
        board: {
          connect: {
            id: boardId,
          },
        },
      },
    });
  }

  findAll(userId?: string, boardId?: string) {
    var filter: Prisma.BoardUserWhereInput = {}
    if (userId) filter = {...filter, userId}
    if(boardId) filter = {...filter, boardId}
    return this.prisma.boardUser.findMany({
        where: filter
    });
  }

  findAllUsingColumn(userId: string, columnId: string) {
    return this.prisma.boardUser.findMany({
      where: {
        userId,
        board: {
          is: {
            columns: {
              some: {
                id: columnId
              }
            }
          }
        }
      }
    })
  }

  findAllUsingCard(userId: string, cardId: string) {
    return this.prisma.boardUser.findMany({
      where: {
        userId,
        board: {
          is: {
            columns: {
              some: {
                cards: {
                  some: {
                    id: cardId
                  }
                }
              }
            }
          }
        }
      }
    })
  }

  async remove(
    { userId, boardId }: RemoveBoardUserInput,
    existingUserId: string,
  ) {
    try {
      await this.prisma.boardUser.findUniqueOrThrow({
        where: {
          userId_boardId: {
            userId: existingUserId,
            boardId,
          },
        },
      });
    } catch (error) {
      if (error.code === 'P2025')
        throw new ForbiddenException('User not authorized');
    }
    return this.prisma.boardUser.delete({
      where: { userId_boardId: { userId, boardId } },
    });
  }
}

```
After that, we have the `findAll` method which takes in optional arguments of `userId` and `boardId` and returns all the board users who match the filter. The interesting thing to note here is that this turns into a `findOne` when both arguments are specified.
After that, we have a couple of helper methods to find board users by resolving from `Column` and `Card`. These are needed in `ColumnModule` and `CardModule` respectively. Given the `columnId` and `cardId` respectively, these find if the user (denoted by `userId`) is supposed to have access to them.
Last but not least, we have the `remove` method. Here, we make sure that only an existing user can remove another user from the board. Again, you can go even finer-grained in your particular design. This concludes the `BoardUserModule` as a whole

## What’s Next?
With this, we have covered the important modules that could have helped us understand how our backend would be working. Next up we will be delving into frontend design – so back to NextJS it is. I might take a couple of articles break before that because I would love to cover Chainlink CCIP before coming back into this.

So, until then, make sure to follow me to keep updated on the latest articles and WAGMI!