NestJS + Prisma + Typescript = Robust, and Powerful GraphQL APIs in less than a day.

Karthick Ragavendran
7 min readAug 14, 2023

--

Code: https://github.com/karthickthankyou/nestjs-prisma-simple-graphql-api

Getting started

// Initialize a new nest application
npx nest new graphql-api --package-manager=yarn

// Install dependencies
yarn add graphql @apollo/server @nestjs/graphql @nestjs/apollo

Add Graphql module in Nestjs

GraphQLModule.forRoot<ApolloDriverConfig>({   // Configuring the GraphQL module using the Apollo driver with a specific configuration type
driver: ApolloDriver, // Specifying the Apollo driver to be used for GraphQL
fieldResolverEnhancers: ['guards'], // Applying guards as field resolver enhancers, providing additional control and security
autoSchemaFile: join(process.cwd(), 'src/schema.gql'), // Automatically generating the schema file from types and resolvers, saving it in the specified path
introspection: true, // Enabling introspection, allowing tools like GraphQL Playground to fetch the schema
}),

Using the Apollo driver, this configuration provides a solid foundation for a GraphQL server in NestJS. It includes automatic schema generation, field-level security through guards, and introspection for development tooling.

Setup env Config

yarn add @nestjs/config

ConfigModule.forRoot(),

The @Module decorator for the AppModule looks like the one below.

@Module({
imports: [
ConfigModule.forRoot(),
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
fieldResolverEnhancers: ['guards'],
autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
introspection: true,
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule implements NestModule {}

Enable nest-cli plugin

The plugin simplifies the development process when working with GraphQL in NestJS, providing automation and addressing some of TypeScript’s limitations. It aids in structuring the code efficiently, making it easier to define and maintain GraphQL schemas.

  "$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true,
"plugins": [{ "name": "@nestjs/graphql" }] // Add this.
}
}

Read more about this here.

The test database

Let's have a Postgres database running locally using docker.

version: '3.8'

services:
db:
container_name: autospace_db
image: postgres
restart: always
ports:
- 1000:5432
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- db_data:/var/lib/postgresql/data
pgadmin:
container_name: autospace_pgadmin
image: dpage/pgadmin4
restart: always
environment:
PGADMIN_DEFAULT_EMAIL: ${PGADMIN_EMAIL}
PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_PASSWORD}
ports:
- '1100:80'
depends_on:
- db
volumes:
db_data:

Add the environment variables POSTGRES_USER, POSTGRES_DB, POSTGRES_PASSWORD, PGADMIN_EMAIL, and PGADMIN_PASSWORD in a .env in the same location as the docker-compose.yml file.

Make sure you have the docker desktop installed and running. Run the database using the below command.

docker compose up -d

Initialize Prisma

yarn add prisma -D

yarn prisma init

Modify the DATABASE_URL in the .env file.

Design Prisma Schema

generator client {
provider = "prisma-client-js"
}

datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}

model User {
uid String @id
createdAt DateTime @default(now())
name String? // Nullable field

posts Post[] // One-to-many relation with Post
comments Comment[] // One-to-many relation with Comment
}

model Post {
id Int @id @default(autoincrement())
title String
body String
published Boolean @default(false) // With default value

author User @relation(fields: [authorId], references: [uid]) // Many-to-one relation with User
authorId String
comments Comment[] // One-to-many relation with Comment
}

model Comment {
id Int @id @default(autoincrement())
text String
post Post @relation(fields: [postId], references: [id]) // Many-to-one relation with Post
postId Int
user User @relation(fields: [userId], references: [uid]) // Many-to-one relation with User
userId String
}

The yarn prisma migrate dev command is used in development to create and apply database migrations based on changes to the Prisma schema. It automates the database schema update, generates the Prisma Client to match the schema, records a migration history, handles potentially destructive changes with interactive prompts, and utilizes a shadow database for safe testing. This command streamlines database iteration and client generation in the development environment, making it an essential tool for managing schema changes.

yarn prisma migrate dev

Prisma module

Refer to the documentation to create a Nest module for Prisma ORM. For our example, create the below files in src/common folder.

// src/common/prisma/prisma.service.ts
import { Injectable, OnModuleInit } from '@nestjs/common'
import { PrismaClient } from '@prisma/client'

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
async onModuleInit() {
await this.$connect()
}
}
// src/common/prisma/prisma.module.ts
import { Global, Module } from '@nestjs/common'
import { PrismaService } from './prisma.service'

@Global()
@Module({
providers: [PrismaService],
exports: [PrismaService],
})
export class PrismaModule {}

Write Nest Models

Including rich filtering, and sorting capabilities in a Graphql is difficult. There are also commonalities in writing the complex Input and Arg types for the everchanging models.

I have written an npm package for code-generationnestjs-prisma-codegen. Also, find the GitHub code here.

In your src/ directory, Just run the command with the required model name.

npx nestjs-prisma-codegen User

That will generate all these files.

├── common
│ ├── dtos
│ │ └── common.input.ts
├── models
│ └── users
│ ├── dto
│ │ ├── create-user.input.ts
│ │ ├── find.args.ts
│ │ ├── order-by.args.ts
│ │ ├── update-user.input.ts
│ │ └── where.args.ts
│ ├── entities
│ │ └── user.entity.ts
│ ├── users.module.ts
│ ├── users.resolver.ts
│ └── users.service.ts

We generated only the essential ones. Let's look at the generated entity file.

import { ObjectType } from '@nestjs/graphql'
import { User as UserType } from '@prisma/client'
import { RestrictProperties } from 'src/common/dtos/common.input'

@ObjectType()
export class User implements RestrictProperties<User, UserType> {
uid: string
createdAt: Date
name: string
}

Notice I use a custom type RestrictProperties that implements the types generated by Prisma so that the Prisma schema and Nestjs types will always be in sync.

export type RestrictProperties<T, U> = {
[K in keyof T]: K extends keyof U ? T[K] : never
} & Required<U>

This will help to avoid adding extra properties or missing the existing ones from the Prisma schema.

Syncing the Nestjs types with the Prisma types will be a breeze after every database migration!

Once you implement the Prisma types to create nest types, You can feel free to add or remove services and resolvers.

Finish the API

Once you are happy with the resolvers and services. Add the modules to the app.module.ts file.

@Module({
imports: [
ConfigModule.forRoot(),
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
fieldResolverEnhancers: ['guards'],
autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
buildSchemaOptions: {
numberScalarMode: 'integer',
},
introspection: true,
}),

PrismaModule,

UsersModule,
PostsModule,
CommentsModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
yarn start:dev

Your API is ready. 🎉

I prefer using apollo explorer for exploring the APIs as it requires much less typing.

Link: https://studio.apollographql.com/sandbox/explorer

Enable Cors

The Graphql API we created is restricted to access from any other domain by default. Update the main.ts with the app.enableCors() adding ALLOWED_ORIGINS from the .env file.

import { NestFactory } from '@nestjs/core'
import { AppModule } from './app.module'

async function bootstrap() {
const app = await NestFactory.create(AppModule)
const allowedOrigins = process.env.ALLOWED_ORIGINS.split(',')

console.log(allowedOrigins)

app.enableCors({
origin: allowedOrigins,
allowedHeaders: '*',
methods: '*',
})
await app.listen(3000)
}
bootstrap()

Sample .env file

ALLOWED_ORIGINS=https://studio.apollographql.com,https://autospace.iamkarthick.com,http://localhost:3001

The in-depth argument types

Graphql is not just about underfetching and overfetching. The nested arg types give us a crazy amount of flexibility.

Look at a sample OrderBy arg type.

@InputType()
export class CommentOrderByWithRelationInput
implements
RestrictProperties<
CommentOrderByWithRelationInput,
Prisma.CommentOrderByWithRelationInput
>
{
@Field(() => Prisma.SortOrder, { nullable: true })
id: Prisma.SortOrder
@Field(() => Prisma.SortOrder, { nullable: true })
text: Prisma.SortOrder
@Field(() => Prisma.SortOrder, { nullable: true })
postId: Prisma.SortOrder
@Field(() => Prisma.SortOrder, { nullable: true })
userId: Prisma.SortOrder
@Field(() => PostOrderByWithRelationInput, { nullable: true })
post: PostOrderByWithRelationInput
@Field(() => UserOrderByWithRelationInput, { nullable: true })
user: UserOrderByWithRelationInput
}

Remember, the nested input types PostOrderByWithRelationInput and UserOrderByWithRelationInput are generated by nestjs-prisma-codegen.

Resolve Fields

Let’s also satisfy the fetch-it-all-together facility Graphql API provides using ResolveField directive.

Inject the PrismaService in the constructor and we can use the powerful Prisma API to return information based on the parent data.

In this example, we return the author and comments that belong to the post.

constructor(
private readonly postsService: PostsService,
private readonly prisma: PrismaService,
) {}

// ...

@ResolveField(() => User)
author(@Parent() parent: Post) {
return this.prisma.user.findUnique({ where: { uid: parent.authorId } })
}

@ResolveField(() => [Comment])
comments(@Parent() parent: Post) {
return this.prisma.comment.findMany({ where: { postId: parent.id } })
}

Demo!

Our API has

  • Mutations with validated Data Transfer Objects.
  • Rich argument types with filtering, sorting, and pagination capabilities.
  • Resolve fields to fetch related data together.

Next time you come across a Graphql API tutorial, notice how extensive the filtering and sorting capabilities of that. 90% of the tutorials don't go beyond implementing skip and limit arguments.

Thanks for reading.

Code: https://github.com/karthickthankyou/nestjs-prisma-simple-graphql-api

Happy coding. 🎉

--

--

Karthick Ragavendran
Karthick Ragavendran

Written by Karthick Ragavendran

Fullstack engineer | React, Typescript, Redux, JavaScript, UI, Storybook, CSS, UX, Cypress, CI/CD.

Responses (1)