Hamzath Anees
Hamzath Anees

Follow

Hamzath Anees

Follow

NestJs Startup Project Configuration

Nestjs with postgreql, graphql, prisma and redis

Hamzath Anees's photo
Hamzath Anees
·Sep 3, 2022·

12 min read

NestJs Startup Project Configuration

Initiate a Nest project. The tutorial expects that nodejs is setup in your deveelopment enviroment. To start its always to nest-cli, that way you will be able to generate awesome code snippets thats helpful.

npm i -g @nestjs/cli

After installing nest-cli lets initiate our nestjs project using the awesome cli Nest framework has provided to us. I will call the project as nest-skeleton

nest new backend

after the installation please check if the project starts correctly.

Configure Environment Variables

Now lets install a package that provides support for managing environment variables and configure in NestJS project. Internally @nestjs/config package uses dotenv

npm i --save @nestjs/config

after installing the package change the app.module.ts file as below

import { Module } from "@nestjs/common";
import { AppController } from "./app.controller";
import { AppService } from "./app.service";
import { ConfigModule } from "@nestjs/config";

@Module({
  imports: [ConfigModule.forRoot()],
  controllers: [AppController],
  providers: [AppService]
})

export class AppModule {}

Its always good to test after changing the configuration. Therefore create a .env file in the root directory and and below variable to the file

GREETING_MESSAGE=Greeting and Glad tidings to you

we can now access above variable by using ConfigService. By default @nestjs/config looks for the .env file in the root directory. However we can also specify another path like .env.dev. So now rename the .env to env.dev . After changing .env file name change package.json scripts as below and change ConfigModule.forRoot in app.module.ts as shown below

"scripts": {
    "prebuild": "rimraf dist",
    "build": "nest build",
    "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
    "start": "NODE_ENV=dev nest start",
    "start:dev": "NODE_ENV=dev nest start --watch",
    "start:debug": "NODE_ENV=dev nest start --debug --watch",
    "start:prod": "NODE_ENV=prod node dist/main",
    "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
    "test": "jest",
    "test:watch": "jest --watch",
    "test:cov": "jest --coverage",
    "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
    "test:e2e": "jest --config ./test/jest-e2e.json",
},
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from '@nestjs/config';

@Module({
  imports: [
    ConfigModule.forRoot(
      {
        envFilePath: `${process.cwd()}/.env.${process.env.NODE_ENV}`,
        isGlobal: true,
      }
    )
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

Graphql with Prisma Setting

Install below packages and change graphql setup on app.module.ts as below

npm install --save prisma apollo-server graphql pg graphql-tools @nestjs/graphql @nestjs/apollo nestjs-prisma

Now lets test if the graphql works by generating a resource

nest g resource users

Pick code-first approach and CRUD option yes when prompted.

import { Module, Logger, Global } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from '@nestjs/config';
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloDriver } from '@nestjs/apollo';
import { join } from 'path';
import { PrismaModule } from 'nestjs-prisma';
import { UsersModule } from './users/users.module';

export async function graphqlModuleFactory(logger:Logger) {
  return {
    traceing: false,
    sortSchema: true,
    debugger: true,
    autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
    installSubscriptionHandlers: true,
    subcription: {
      keepAlive: 5000
    },
    context: (data: any) => {
      return {
        token: undefined as string | undefined,
        req: data.req as Request,
      }
    }
  }
}

@Module({
  imports: [
    GraphQLModule.forRootAsync({
      driver: ApolloDriver,
      useFactory: graphqlModuleFactory,
    }),
    ConfigModule.forRoot({
        envFilePath: `${process.cwd()}/.env.${process.env.NODE_ENV}`,
        isGlobal: true,
    }),
    UsersModule
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

Setting Up Prisma with PostgreSQL Install the following package and initiate prisma

npm install prisma -D
npx prisma init

when prisma is initiated lets add prisma related scripts to package.json file

"migrate:dev": "NODE_ENV=dev prisma migrate dev --preview-feature",
    "migrate:dev:create": "NODE_ENV=dev prisma migrate dev --create-only --preview-feature",
    "migrate:reset": "NODE_ENV=dev prisma migrate reset --preview-feature --force",
    "migrate:deploy": "NODE_ENV=dev npx prisma migrate deploy --preview-feature",
    "migrate:status": "NODE_ENV=dev npx prisma migrate status --preview-feature",
    "migrate:resolve": "NODE_ENV=dev npx prisma migrate resolve --preview-feature",
    "prisma:studio": "NODE_ENV=dev npx prisma studio",
    "prisma:generate": "NODE_ENV=dev npx prisma generate",
    "prisma:generate:watch": "NODE_ENV=dev npx prisma generate --watch"

after adding the scripts add a sample model class to the schema.prisma file

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

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

model User {
  id    Int     @id @default(autoincrement())
  email String  @unique
  name  String?
}

after adding the model run migration and then run npm run prisma:generate

npm run migrate:dev
npm run prisma:generate

now lets add the following code to create-user dto and user entities of users

// ./src/users/dto/create-user.input.ts
import { InputType, Int, Field } from '@nestjs/graphql';

@InputType()
export class CreateUserInput {
  @Field(() => String, { description: 'Email' })
  email: string;

  @Field(() => String, { description: 'Name'})
  name: string;
}
// ./src/users/entities/user.entity.ts
import { ObjectType, Field, Int } from '@nestjs/graphql';

@ObjectType()
export class User {
  @Field(() => String, { description: 'Email' })
  email: string;

  @Field(() => String, { description: 'Name'})
  name: string;
}

In order to test graphql lets add create User function to UsersService

// ./src/users/users.service.ts
import { Injectable } from '@nestjs/common';
import { CreateUserInput } from './dto/create-user.input';
import { UpdateUserInput } from './dto/update-user.input';
import { User } from './entities/user.entity';
import { PrismaService } from 'nestjs-prisma';


@Injectable()
export class UsersService {

  constructor(private prisma: PrismaService) {}

  async create(createUserInput: CreateUserInput): Promise<User> {
    const { email, name } = createUserInput;

    const user = await this.prisma.user.create({
      data: {
        name: name,
        email: email
      }
    })

    return user;
  }

After adding the service lets add the function to the users resolver

// ./src/users/users.resolver.ts
import { Resolver, Query, Mutation, Args, Int } from '@nestjs/graphql';
import { UsersService } from './users.service';
import { User } from './entities/user.entity';
import { CreateUserInput } from './dto/create-user.input';
import { UpdateUserInput } from './dto/update-user.input';

@Resolver(() => User)
export class UsersResolver {
  constructor(private readonly usersService: UsersService) {}

  @Mutation(() => User, { name: 'createUser', nullable: true })
  createUser(@Args('createUserInput') createUserInput: CreateUserInput) {
    return this.usersService.create(createUserInput);
  }

then run prisma generate to make sure all dto's and entities are synced

npm run prisma:generate

Now lets create a test user using graphql playground on localhost:3000/graphql using the following mutation

mutation {
  createUser(createUserInput: {email: "test@test.com", name: "TestUser"}){
    email, name
  }
}

when everything is working fine the return will be as below

fifth.png

Caching Using Redis

In order to use redis for caching install the following packages

npm install redis

now lets create a module, service and a resolver for redisCache

nest g module redisCache
nest g service redisCache --no-spec
nest g resolver redisCache --no-spec

now change redis-cache.service.ts file as below. You may change the PREFIX as you like.

import { Injectable, Global } from '@nestjs/common';
import { createClient, RedisClientType } from 'redis';


@Injectable()
export class RedisCacheService {
    PREFIX = 'backend';
  async connect(): Promise<any> {
    const client = createClient();
    client.on('error', (err) => console.log('Redis Client Error', err));
    await client.connect();
    return client;
  }

  parseWithDate(jsonString: string): any {
    const reDateDetect = /(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})/; // startswith: 2015-04-29T22:06:55
    const resultObject = JSON.parse(jsonString, (key: any, value: any) => {
      if (typeof value == 'string' && reDateDetect.exec(value)) {
        return new Date(value);
      }
      return value;
    });
    return resultObject;
  }

  async get(key): Promise<any> {
    const client = await this.connect();
    const valueJSON = await client.get(`${this.PREFIX}:${key}`);
    const value = this.parseWithDate(valueJSON);
    client.quit();
    return value;
  }

  async set(key, value, ttl) {
    const client = await this.connect();
    const valueJSON = JSON.stringify(value);
    await client.set(`${this.PREFIX}:${key}`, valueJSON, { EX: ttl });
    client.quit();
  }

  async setFor10Sec(key, value) {
    await this.set(key, value, 10);
  }

  async setForDay(key, value) {
    const secondsInDay = 24 * 60 * 60;
    await this.set(key, value, secondsInDay);
  }

  async setForMonth(key, value) {
    const secondsInMonth = 30 * 24 * 60 * 60;
    await this.set(key, value, secondsInMonth);
  }

  async getKeys() {
    const client = await this.connect();
    const keysWithPrefix = await client.keys(`${this.PREFIX}:*`);
    client.quit();
    return keysWithPrefix.map((k) => k.split(`${this.PREFIX}:`)[1]);
  }

  async getKeysPattern(pattern: string) {
    const client = await this.connect();
    const keysWithPrefix = await client.keys(`${this.PREFIX}:${pattern}`);
    client.quit();
    return keysWithPrefix.map((k) => k.split(`${this.PREFIX}:`)[1]);
  }

  async del(key) {
    const client = await this.connect();
    await client.del(`${this.PREFIX}:${key}`);
    client.quit();
  }

  async delPattern(pattern: string) {
    const keys = await this.getKeysPattern(pattern);
    for (const key of keys) {
      await this.del(key);
    }
  }

  async deleteAll() {
    const keys = await this.getKeys();
    keys.forEach(async (key) => {
      await this.del(key);
    });
    console.log('Redis cache flushed.');
  }
}

After changing redis-cache.service.ts file change redis-cache.resolver.ts file as below

import { Resolver, Query, Mutation, Args } from '@nestjs/graphql';
import { RedisCacheService } from './redis-cache.service';

@Resolver()
export class RedisCacheResolver {
    constructor(private readonly redisCacheService: RedisCacheService) { }

    @Query(() => [String], { name: 'redisKeys' })
  async getKeys(): Promise<string[]> {
    return await this.redisCacheService.getKeys();
  }

  @Query(() => String, { name: 'redisGet', nullable: true })
  async redisGet(
    @Args('key', { type: () => String }) key: string
  ): Promise<string> {
    const value = await this.redisCacheService.get(key);
    return JSON.stringify(value);
  }

  @Mutation(() => String, { name: 'flushRedis' })
  async flushRedis(): Promise<string> {
    await this.redisCacheService.deleteAll();
    return 'Redis cache flushed.';
  }
}

We will now check if redis service works in users service. You can change users.module.ts as below

import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
import { UsersResolver } from './users.resolver';
import { RedisCacheModule } from '../redis-cache/redis-cache.module';
import { RedisCacheService } from '../redis-cache/redis-cache.service';

@Module({
  imports: [RedisCacheModule],
  providers: [UsersResolver, UsersService, RedisCacheService],
  exports: [UsersService]
})
export class UsersModule {}

After importing redis module and redis service into users module change the user service file as below to test redis.

// ./src/users/users.service.ts
import { Injectable } from '@nestjs/common';
import { CreateUserInput } from './dto/create-user.input';
import { UpdateUserInput } from './dto/update-user.input';
import { User } from './entities/user.entity';
import { PrismaService } from 'nestjs-prisma';
import { RedisCacheService } from '../redis-cache/redis-cache.service';


@Injectable()
export class UsersService {

  constructor(
    private prismaService: PrismaService,
    private readonly redisCacheService: RedisCacheService
  ) {}


  async create(createUserInput: CreateUserInput): Promise<User> {
    const { email, name } = createUserInput;

    const user = await this.prismaService.user.create({
      data: {
        name: name,
        email: email
      }
    })

    this.redisCacheService.setFor10Sec("user", user);

    const redisUser = await this.redisCacheService.get("user");
    console.log("redisUser", redisUser);

    return user;
  }

  findAll() {
    return `This action returns all users`;
  }

  findOne(id: number) {
    return `This action returns a #${id} user`;
  }

  update(id: number, updateUserInput: UpdateUserInput) {
    return `This action updates a #${id} user`;
  }

  remove(id: number) {
    return `This action removes a #${id} user`;
  }
}

Authentication

In order to authenticate the user using jwt token lets install the needful packages

npm install -D @types/bcrypt @types/passport-jwt
npm install bcrypt @nestjs/passport passport passport-jwt @nestjs/jwt

before going further lets change the user model in our prisma file by adding a password and timestamps to it.

model User {
  id    Int     @id @default(autoincrement())
  email String  @unique
  name  String?
  password String
  role String?

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

since we have changed the model we need to change the relavant entity, and dto's as well

@InputType()
export class CreateUserInput {
  @Field(() => String, { description: 'Email' })
  email: string;

  @Field(() => String, { description: 'Name'})
  name: string;

  @Field(() => String, { description: 'Password' })
  password: string;

  @Field(() => String, { description: 'role', nullable: true })
  role?: string;

  @Field(() => Date, { description: 'createdAt', nullable: true })
  createdAt?: Date;

  @Field(() => Date, { description: 'updatedAt', nullable: true })
  updatedAt?: Date;
}
@InputType()
export class LoginInput {
  @Field(() => String, { description: 'Email' })
  email: string;

  @Field(() => String, { description: 'Password' })
  password: string;
}
@ObjectType()
export class LoggedUserOutput {
  @Field(() => String, { description: 'Generated access_token of the user' })
  access_token: string;

  @Field(() => String, { description: 'Auth Expiries At' })
  expires_in?: string;
}

src/users/entities/user.entity.ts

import { ObjectType, Field, Int } from '@nestjs/graphql';

@ObjectType()
export class User {
  @Field(() => Int, { description: 'ID' })
  id: number;

  @Field(() => String, { description: 'Email' })
  email: string;

  @Field(() => String, { description: 'Name'})
  name: string;

  @Field(() => String, { description: 'Password' })
  password: string;

  @Field(() => String, { description: 'role' })
  role?: string;

  @Field(() => Date, { description: 'createdAt' })
  createdAt?: Date;

  @Field(() => Date, { description: 'updatedAt' })
  updatedAt?: Date;
}

Now lets create auth module, service and resolver. src/auth/auth.module.ts

import { Module, forwardRef } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthResolver } from './auth.resolver';
import { UsersModule } from '../users/users.module';
import { JwtModule } from '@nestjs/jwt';
import { ConfigService, ConfigModule } from '@nestjs/config';

@Module({
  imports: [
    forwardRef(() => UsersModule),
    JwtModule.registerAsync({
      useFactory: (configService: ConfigService) => ({
        secret: configService.get<string>('JWT_SECRET'),
        signOptions: { expiresIn: configService.get<string>('JWT_SECRET_EXPIRES_IN') },
      }),
      inject: [ConfigService],
    }),
  ],
  providers: [AuthService, AuthResolver],
})
export class AuthModule {}

src/auth/auth.service.ts

import { BadRequestException, forwardRef, Inject, Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { UsersService } from '../users/users.service';
import { User } from '../users/entities/user.entity';
import * as bcrypt from 'bcrypt';
import { LoginInput } from '../users/dto/login.input';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class AuthService {
    constructor(
        @Inject(forwardRef(() => UsersService))
        private usersService: UsersService,
        private jwtTokenService: JwtService,
        private configService: ConfigService,
    ) {}

    async validateUser(email: string, password: string): Promise<any>   {
        const user = await this.usersService.findOneByEmail(email);
        if (user) {
            if (await bcrypt.compare(password, user.password)) {
            delete user.password;
            return user;
            }
        }
        return null;
    }

    async generateUserCredentials(user: User) {
        const payload = {
            email: user.email,
            name: user.name,
            role: user.role,
            sub: user.id,
        };

        return {
            access_token: this.jwtTokenService.sign(payload),
        };
    }

    //authenticating user
    async loginUser(loginInput: LoginInput) {
        const user = await this.validateUser(
            loginInput.email,
            loginInput.password,
        );
        if (!user) {
            throw new BadRequestException(`Email or password are invalid`);
        } else {
            const access_token = await (await this.generateUserCredentials(user)).access_token;
            return { "access_token": access_token, "expires_in":  this.configService.get<string>('JWT_SECRET_EXPIRES_IN')};
        }
    }
}

src/auth/auth.resolver.ts

import { Resolver, Mutation, Args } from '@nestjs/graphql';
import { LoggedUserOutput } from '../users/dto/logged-user.output';
import { AuthService } from './auth.service';
import { LoginInput } from '../users/dto/login.input';

@Resolver()
export class AuthResolver {
    constructor(private readonly authService: AuthService) {}

    @Mutation(() => LoggedUserOutput)
    loginUser(@Args('loginUserInput') loginUserInput: LoginInput) {
        return this.authService.loginUser(loginUserInput);
    }
}

Now that we have implemented login lets test if the login works fine by after creating a new user

Create User auth-1-create-user.png

Login User auth-2-login-user.png

Now if you are able to get token you can check the content of the token is valid or not using jwt.io

jwt-check.png

After creating the auth token we must verify if the authentication works. For that reason we will have a jwt strategy and jwt guard as below src/auth/jwt-auth.guard.ts

import { ExecutionContext, Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { GqlExecutionContext } from '@nestjs/graphql';

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
  getRequest(context: ExecutionContext) {
    const ctx = GqlExecutionContext.create(context);
    return ctx.getContext().req;
  }
}

src/auth/jwt-auth.guard.ts

import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { ConfigService } from '@nestjs/config';
import { Injectable } from '@nestjs/common';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
  constructor(private readonly configService: ConfigService) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: configService.get<string>('JWT_SECRET'),
    });
  }

  async validate(payload: any) {
    return { payload, userId: payload.sub };
  }
}

Now make sure that you add the strategy to auth module

import { JwtStrategy } from './jwt.strategy';

@Module({
  imports: [
    forwardRef(() => UsersModule),
    JwtModule.registerAsync({
      useFactory: (configService: ConfigService) => ({
        secret: configService.get<string>('JWT_SECRET'),
        signOptions: { expiresIn: configService.get<string>('JWT_SECRET_EXPIRES_IN') },
      }),
      inject: [ConfigService],
    }),
  ],
  providers: [AuthService, AuthResolver, JwtStrategy],
})
export class AuthModule {}

Now Lets check if the authentication guard works as expected by creating a new function in user resolver. You wil need to import JwtAuthGuard, and UseGuards to user.resolver.ts

import { UseGuards } from '@nestjs/common'; 
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
@UseGuards(JwtAuthGuard)
  @Query(() => String, { name: 'listUsersWithCursor' })
  async findAllWithCursor(
  ) {

    return "Hello World";
  }

After adding the query test it if it works correctly.

When Auth Fails

auth-3-test-login-fail.png

When Auth is Success auth-4-login-sucess.png

 
Share this