In the past articles in this series, we have explored the frontend and backend side of authentication. If you have been following those, then right about now you’d also have a fully functional frontend in NextJS which uses Next Auth to facilitate Authentication with Google OAuth 2.0 and communicates with a backend built using NestJS which also has a separate but interoperable authentication system using Passport and Google OAuth.
At this stage, we are ready to flesh out our backend. As informed previously, we will be building our backend in GraphQL. So, even though the authN was done with REST APIs, the remainder of the backend will be exposed as GraphQL API. Why was this design decision made? Well, GraphQL allows for flexibility and customization on the frontend. It is one of those things that almost every seasoned backend dev is aware of and likes but tries to sometimes avoid. Since I have a knack for article articles on such things which are often “avoided” and because the Kanban board we are building will require this level of flexibility, the choice of GraphQL with Nest was made.
Just to confirm again, we will be using NestJS to make our lives easier. This article will be aimed at devs who have a good understanding of REST APIs and a basic understanding of NestJS. Keep in mind that NestJS offers 2 ways of using GraphQL
Code-first Approach – Here you use the entities in NestJS to create the schema in a format that would be familiar to someone who has written DTOs in NestJS. NestJS takes these entities and stitches a GraphQL schema. The developer need not really worry about touch Schema Definition Language.
Schema-first Approach – Here you specify the Schema in individual modules first. NestJS then combines these schemas and creates a singular schema definition language file. From this file, NestJS then generates types and entities to be used in the project.
There is a third approach where you can use both paradigms at once in the same project but it is complicated and can lead to exposure of multiple GraphQL endpoints and redundancies in schema definition if not used in a microservice-based architecture. We will explore this paradigm as well once we have explored the above two.
In this article, we will explore the “Code-first Approach”. This is geared towards REST API devs and makes them feel more at home. Apart from this, we will also explore how to use 2 different databases with Prisma in the same NestJS project and manage things like migrations and Prisma Studio between them.
Housekeeping
Before we begin, you will need to provision a new PostgreSQL (different from the Supabase one we have been using thus far). For this tutorial, we will use Render. Render offers free managed PostgreSQL instances for a period of 90 days. If you haven’t created a Render account yet, head over and sign up. You will be redirected to your Render Dashboard. Click on “New” in the Appbar and select PostgreSQL DB. You will see something similar to the image shown below. Fill in the fields and click on Create.
Once the creation is complete, move over to the Info tab of your instance. Here you will get the External Connection URL. As expected, this will be used to connect from your NestJS project using Prisma. Also, make sure that the Access Control is set to 0.0.0.0/0
as shown in the below image. This means anyone on the internet with the Connection string will be able to access the DB instance. This is why it's paramount to keep your connection string a secret. Storing it in a .env
file is a step in that direction.
To use GraphQL in a NestJs project we need to install NestJs’ Apollo and GraphQL packages. For these, run npx i @nestjs/apollo @nestjs/graphql
. Once this finishes, move over to your app.module.ts
. Here we will import the GraphQL config module and make it generally available. To do this, inside the imports
array, add the following:
// Add the following to the imports array
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
include: [CardModule, BoardModule, BoardUserModule, ColumnModule],
autoSchemaFile: join(process.cwd(), 'src/graphql-schema/schema.gql'),
}),
Using Multiple Prisma Schema in a single project
We have set up a Supabase instance and connected it to our NestJS project. That will be used only for Auth. For all other purposes, we will use another PostgreSQL instance. This also means we will have 2 separate schema.prisma
files and Prisma clients.
This article by @micalevisk offers great insight into how to use different versions of the same module in a NestJS project. In our case, we will be using different Prisma Client Modules in the same Nest project. Remove traces of the Prisma module we developed in our last article from our project and then run the following commands:
nest new module prisma-render
followed bynest new service prisma-render
- This will create the first Prisma Module which we will use to connect to our Render Postgres DB instance.nest new module prisma-supabase
followed bynest new service prisma-supabase
- This will create the Module which we will use to connect to our Supabase Postgres instance.
Prisma Supabase Module
Our PrismaSupabaseModule
will be a global module and it will export the service of the module. This will enable other modules like AuthModule
and UserModule
to utilize it with ease. The code below for our prisma-supabase.module.ts
file does exactly that:
// prisma-supabase.module.ts
import { Global, Module } from '@nestjs/common';
import { PrismaSupabaseService } from './prisma-supabase.service';
@Global()
@Module({
providers: [PrismaSupabaseService],
exports: [PrismaSupabaseService]
})
export class PrismaSupabaseModule {}
Before moving to the service, we need to make certain changes to our schema file. Firstly, we need to rename it. As a good practice, we will call our Supabase schema file supabase.schema.prisma
. We need not touch the schema definition in the file. Just a little change in the generator configuration and we are daisy!
Our dependencies are being taken care of by Turbo and the NestJS & NextJS dependencies are both stored globally. The client our Prisma will generate will also need to be put in that location for easy path resolution. For that reason, we will need to change the output
path from default to inside a special folder in the node_modules
as shown below.
// generator for supabase.schema.prisma
generator client {
provider = "prisma-client-js"
output = "../../../node_modules/@prisma/supabase"
}
Now with this change, we need to run npx prisma generate –schema=./prisma/supabase.schema.prisma
. Note how we specify the name of the schema file since there is no default schema.prisma
file for Prisma to pick up automatically. This will generate our Supabase Prisma Client inside the Prisma dependency of the global node_modules
. We can now move to create our Supabase service.
Inside our prisma-supabase.service.ts
file of our prisma-supabase
module, we will need to import the PrismaClient
from @prisma/supabase
instead of our default @prisma/client
. This is because our Supabase-specific Prisma client is now generated inside our custom location. The remainder of the code will remain similar to the steps we took in our previous article when we created our Prisma module. So hopefully the code below for our prisma-supabase.service.ts
needs no further explanations.
// prisma-supabase.service.ts
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PrismaClient } from '@prisma/supabase'; //NOTE HERE
@Injectable()
export class PrismaSupabaseService extends PrismaClient {
constructor(configService: ConfigService) {
super({
datasources: {
db: {
url: configService.get('SUPABASE_DATABASE_URL'),
},
},
});
}
}
Finally, we will change the imports of our AuthModule
which needs to use this PrismaSupabaseModule
. Inside auth.service.ts
we will change to PrismaSupabaseService
instead of our old PrismaService
(which no longer exists). The minor changes are shown below:
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import { GoogleLoginUserDto } from './dto/google-login.dto';
import { PrismaSupabaseService } from 'src/prisma-supabase/prisma-supabase.service'; // NOTE HERE
@Injectable()
export class AuthService {
constructor(
private jwtService: JwtService,
private configService: ConfigService,
private prisma: PrismaSupabaseService, // NOTE HERE
) {}
/*
*
* Remainder of the service remains same
*
/
}
Prisma Render Module
Next up, we will need modifications to our PrismaRenderModule
. But before that, we need to discuss the Schema for our project. Given below is the Schema for our Kanban Board. These are the contents of our render.schema.prisma
file that we need to create inside our prisma
folder where our supabase.schema.prisma
file is stored. Explanations follow.
generator client {
provider = "prisma-client-js"
output = "../../../node_modules/@prisma/render"
}
datasource db {
provider = "postgresql"
url = env("RENDER_DATABASE_URL")
}
model Board {
id String @id @default(uuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
boardName String
boardDescription String
createdBy String
columns Column[]
boardUsers BoardUser[]
}
model BoardUser {
userId String
board Board @relation(fields: [boardId], references: [id], onDelete: Cascade)
boardId String
@@unique([userId, boardId])
}
model Column {
id String @id @default(uuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
columnName String
columnDescription String
board Board @relation(fields: [boardId], references: [id])
boardId String
cards Card[]
}
model Card {
id String @id @default(uuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
cardNumber Int
cardName String
cardDescription String
createdBy String
assignedTo String
storyPoints Int
priority Priority
status Status
startDate DateTime
endDate DateTime
comments Comment[]
column Column @relation(fields: [columnId], references: [id])
columnId String
}
model Comment {
id String @id @default(uuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
createdBy String
body String
card Card @relation(fields: [cardId], references: [id])
cardId String
}
enum Priority {
CRITICAL
HIGH
MEDIUM
LOW
}
enum Status {
TODO
IN_PROGRESS
DONE
}
Just like our Supabase client, we will generate our Render client in a custom location right alongside our Supabase client. For this, we change the output
location of our generator. We will be connecting to our Render DB instance using the RENDER_DATABASE_URL
env variable. So, keep in mind to include it in the .env file and in the Environment Variable Schema check inside ConfigModule
imported in app.module.ts
.
We have 5 models in our Kanban board project:
Board
- Represents a Kanban board. Will have a name, description a list of users who have access to the board. We also record the creator of the board for admin functions.BoardUser
- Represents the table which records user access to a specific board. We are not having RBAC here. We assume all users in this table have equal rights and rank lower than the board creator.Column
- This model represents the columns inside our board. While the default ones a board might have are “Backlog”, “To-Do”, “Under Progress” and “Done”, we are not enforcing them. So, every new board has 0 columns. The users need to create these columns by themselves. A column will have a list of Cards.Card
- These represent the user stories or tasks. A card will have a globally unique ID, a number that will be unique in a board, name, description, story points, assigned to user ID, start & end date, a status (enum which can beTODO
,IN_PROGRESS
andDONE
) and a priority (enum which can beCRITICAL
,HIGH
,MEDIUM
andLOW
). A card will have a list of comments.Comment
- Comments that a board user might leave on a card. This can range from a PR created for the tasks to any notes for other users. A Comment will always have a corresponding Card.
Keep in mind that we are not going full gaga over the functionality. The aim of this mini-project is to demonstrate the various moving parts in a Nest + Next project ecosystem. As such, if you want you may include your touches to the mini-project.
Now we will not be running the generate command for Prisma this time around. Here, we have created our schema but our Render Postgres Instance does not contain that. We are going to migrate it. To migrate to a specific DB when you are using multiple Prisma Schema files, we need to specify the schema file to use for the migration using the --schema
flag. In our case, we need to run npx prisma migrate dev –schema=./prisma/render.schema.prisma
. This will run the migration and generate the client in the custom location we have specified in the above schema.
Finally, inside prisma-render.service.ts
, we need to follow the steps we have done for our Supabase client and import the newly generated Render Prisma Client. The code below exactly does that.
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PrismaClient } from '@prisma/render'; // NOTE HERE
@Injectable()
export class PrismaRenderService extends PrismaClient {
constructor(configService: ConfigService) {
super({
datasources: {
db: {
url: configService.get('RENDER_DATABASE_URL'),
},
},
});
}
}
GraphQL Resolvers
Now with the schema above, you’d expect to create separate modules and write resolvers. In fact, that’s what we need to do. But for the sake of keeping the article short, I am going to cover the 3 more important types of resolvers. The remainder will be inside the GitHub repo on the main
branch.
Resolvers in GraphQL are your REST API endpoint counterpart. There are no “GET”, “POST”, “PUT” or “DELETE” verbs to work with in GraphQL. You create “resolvers” for your data model (basically those tables or mongodb collections). These resolvers are then invoked from the frontend. Inside the resolvers, you specify which attributes (table column or document field in case of mongodb) of the model you want to return while coding. Typically, all the attributes that may be required are sent as response of a resolver. On the frontend, a query is written using the resolver which basically tells your GraphQL API which parts or attributes from the resolver response the frontend wants. Only those attributes are sent back. That’s how the whole system works.
We will cover the UserModule
, BoardModule
and BoardUserModule
. Between these three, most of the important points can be covered. Before we get started, let’s generate the three modules. Consecutively run the following. Keep in mind that you need to tell NestJs to generate CRUD endpoints and when asked, select “GraphQL (Code First)”.
nest g resource user
- This will generate ourUserModule
. This module will use the Supabase Prisma Client and act as common ground for resolving user info from other resolvers.nest g resource board
- This will generate ourBoardModule
. This module will use Render Prisma Client under the hood.nest g resource board-user
- This will generate ourBoardUserModule
. While this will primarily use the Render Prisma Client, it will use theUserModule
to resolve user details of a board user.
Circular Dependency
Circular dependency is a dreaded problem in the programming world. But unfortunately (or fortunately), circular dependencies are quite common in GraphQL. This will be clear as we go on. For now, remember that NestJS offers an easy way out of it using forwardRef()
.
Basically, you take the imports
arrays of the two modules having circular dependencies on one another. Inside the imports
array, you wrap the imported module with forwardRef()
as shown below. You do this on both modules. This solves the issue. NestJs will now use the ref of the module to solve the circular dependency.
// Inside board.module.ts
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 { UserModule } from 'src/user/user.module';
@Module({
imports: [
forwardRef(() => BoardUserModule), //NOTE HERE
forwardRef(() => UserModule) //NOTE HERE
],
providers: [BoardResolver, BoardService],
exports: [BoardService],
})
export class BoardModule {}
Likewise importing on the other side (import in the BoardUserModule as well):
import { Module, forwardRef } from '@nestjs/common';
import { UserService } from './user.service';
import { UserResolver } from './user.resolver';
import { BoardModule } from 'src/board/board.module';
@Module({
imports: [forwardRef(() => BoardModule)], // NOTE HERE
providers: [UserResolver, UserService],
exports: [UserService],
})
export class UserModule {}
User Module
While you might think the first thing, we need to define in the module is the module file itself, the entity file is more important. There are also the DTOs we need to define in the module as we will see in the later modules. In this module, the User data is just read so we won’t need any DTOs here as there are no creation or updation happening.
User Entity
In NestJS, the entity has an @ObjectType()
decorator. This tells the NestJS GraphQL engine that what it is reading needs to be provided when this entity is “resolved”. The entity itself is represented by a class
and its properties closely mirror the DB model and parts of the DB model you want to expose (or how you want to expose them). Take for instance the below User Entity class:
import { ObjectType, Field, ID, GraphQLISODateTime } from '@nestjs/graphql';
import { Board } from 'src/board/entities/board.entity';
@ObjectType()
export class User {
@Field(() => ID)
id: string;
@Field(() => String)
name: string;
@Field(() => String)
email: string;
@Field(() => GraphQLISODateTime)
emailVerified: Date;
@Field(() => String)
image: string;
@Field(() => [Board])
boardsCreated: [Board]
}
In the above entity class, we say that User
has an id. This id is of type string. In GraphQL, ID
is actually a datatype. Every property in an entity needs to be decorated by the @Field()
decorator. This @Field()
decorator tells GraphQL the type of the property and whether it can be nullable or not. By default, nothing is nullable.
We say that the user will have a name, email, and an image link. These will be string type (denoted by their type inside the class and inside the @Field()
decorator). The property will be a Date
type. GraphQL has an inbuilt type of GraphQLISODateTime
to represent such Date formats as shown by the corresponding field decorator.
Lastly, we say that when a user is resolved, it must also show the boards that the user has created with the boardsCreated
property. Note how we specify another entity as the type inside the corresponding @Field()
decorator. This is a very handy way to represent the Board
inside other types.
Throwback to when I said that the entity object closely mirrors the DB and what/how you want to expose the data.
We don’t have a Board
model in our User
model. The User
model is described in our Supabase instance while the Board
model is in our Render instance. When we write our business logic using our resolver files and service files, we will create a resolver for the board inside our user module. On the frontend, anyone querying the API will not notice the difference in DB storage. Thus, we have two different Database instances’ data being exposed through GraphQL queries. This will also allow the frontend user to write complicated queries which involve both models.
We can go a few steps further and add fields like comments
, cardCreated
and cardsAssigned
. But the main point here is to help you understand how resolving a field works. Once you understand that, feel free to go wild with it.
Module
Our Module here is plain and simple. Since we need to resolve the boards
field in User
entity, we need to include BoardService
which comes from BoardModule
. But we also need to make sure to avoid Circular dependency. So, forwardRef()
comes in handy. In the code below, we do exactly that.
// user.module.ts
import { Module, forwardRef } from '@nestjs/common';
import { UserService } from './user.service';
import { UserResolver } from './user.resolver';
import { BoardModule } from 'src/board/board.module';
@Module({
imports: [forwardRef(() => BoardModule)],
providers: [UserResolver, UserService],
exports: [UserService],
})
export class UserModule {}
Resolver
Now, a resolver is the REST controller counterpart in GraphQL. It stands to reason that it will invoke methods in the service file of the module to handle requests and send back responses. In GraphQL, a “query” is any read-type request, and a “mutation” is a write-type request. So, any creation, updation or deletion would be a “mutation”. We need to define these queries and mutations as shown in the code below. Explanations follow.
// user.resolver.ts
import {
Resolver,
Query,
Mutation,
Args,
Int,
ResolveField,
Parent,
} from '@nestjs/graphql';
import { UserService } from './user.service';
import { User } from './entities/user.entity';
import { GraphQLUser } from 'src/decorators';
import { Board } from 'src/board/entities/board.entity';
import { BoardService } from 'src/board/board.service';
@Resolver(() => User)
export class UserResolver {
constructor(
private readonly userService: UserService,
private readonly boardService: BoardService,
) {}
@Query(() => [User], { name: 'users' })
findAll() {
return this.userService.findAll({});
}
@Query(() => User, { name: 'user' })
findOne(@Args('id', { type: () => String }) id: string) {
return this.userService.findOne(id);
}
@Mutation(() => User)
removeUser(@GraphQLUser('sub') id: string) {
return this.userService.remove(id);
}
@ResolveField('boardsCreated', (returns) => [Board])
getBoardsCreatedByUser(@Parent() user: User) {
const { id } = user;
return this.boardService.findAll({createdBy: id});
}
}
In the constructor at lines 19-20, we use the shorthand to define the services we are going to invoke. We are going to need the underlying UserService
and the BoardService
which comes from BoardModule
which we imported into the module.
The first query that we define at line 23 is the counterpart of “GET all” route in a REST API. We say that it will return a list of User
’s. The second option to the @Query()
decorator is the name of the query. So, to get a list of all users, the users
query will be invoked and the fields required will be passed into it.
Second, we have the “GET by ID” REST counterpart GraphQL query at line 28. As you might have guessed, it is supposed to return a single User
. To get the user, one would have to use the user(id)
query and pass in the list of attributes required.
We won’t be needing a “PUT” or “PATCH” counterpart here. Will be exploring it in the next module. So, we jump to the first mutation at line 33. Note how we do not pass a {name: <mutation name>}
here. Keep in mind that we do not have that, NestJS will use the function name as the name of the mutation. The same goes for the queries. This “DELETE by ID” REST API counterpart tells us that when this mutation is invoked, we will take the user ID (which is in the sub
field of the JWT) and remove the user.
Lastly, we have the @ResolveField()
decorator. This is used to resolve any outside fields which may exist in the entity. In our case, that’s the boardsCreated
field in the User
entity. This is a powerful feature because now, GraphQL can resolve the board and one can pass in the attributes needed from the boards and get back only those attributes. The @Parent()
decorator is used to get the user whose created boards we want to access. The user
argument in the parameter list of the getBoardsCreatedByUser()
function receives the user details. We have defined a findAll()
function inside BoardService
which is then invoked to filter out boards by the createdBy
field on the Board
model.
Service
We will make short work of our user service. We just need three functions in the service that will invoke Prisma API functions for our models defined in the supabase.schema.prisma
file. As shown in the code below, we just the findAll
, findOne
, and remove
functions. Note how we import the PrismaSupabaseService
. We didn’t need to import the PrismaSupabaseModule
because we had declared it as a global module.
// user.service.ts
import { Injectable } from '@nestjs/common';
import { Prisma } from '@prisma/supabase';
import { PrismaSupabaseService } from 'src/prisma-supabase/prisma-supabase.service';
@Injectable()
export class UserService {
constructor(private prisma: PrismaSupabaseService) {}
findAll(where: Prisma.usersWhereInput) {
return this.prisma.users.findMany({where});
}
findOne(id: string) {
return this.prisma.users.findUniqueOrThrow({where: {id}});
}
remove(id: string) {
return this.prisma.users.delete({where: {id}});
}
}
In line 8, we define our findAll()
function. This is a weak wrapper around Prisma’s model-specific findMany()
function. This is more of a personal bias. This helps in importing this service and invoking the findAll
with filters which would translate to the where
clause of the Prisma API. It is a handy trick I like to use.
Next, we have the findOne()
function which takes in an id
and either returns the user with that ID or throws an exception. Of course, this translated to an Internal Server Error. So, it is always recommended to have NestJS Global Exception filters in place. I like to throw such an error from the service and have it handled in a custom Prisma Exception NestJS filter.
Lastly, we have the remove()
function which takes in the id
and returns the deleted user as a response. This concludes our service.
What’s Next?
Now I wanted to cover the board and board user modules in this article itself but that would make it 5k + words. That’s way too long for developers with the attention span of a goldfish. Being someone who has a similar attention span, I would never want to inflict that on a fellow developer. So, we will cover those modules and then some in the next part.
That's why I have to bring this article to an abrupt end. Keep in mind that the code discussed here has been open-sourced at the following repo: %[github.com/abhik-99/Trivial-Kanban]
Until then, keep building awesome stuff and WAGMI!