DEV Community

loading...
Cover image for HOW TO BUILD A NESTJS | FASTIFY | GRAPHQL | MONGO AUTH API

HOW TO BUILD A NESTJS | FASTIFY | GRAPHQL | MONGO AUTH API

fernandoboza profile image Fernando Boza Originally published at fernandoboza.com ・5 min read

We're going to build a NestJS server, on top of Fastify framework, pushing out a GraphQL API hosted on a MongoDB server. We'll create a User login and register model route and secure with passport and bcrypt, we'll go with the passport strategy JWT.

For reference here is the video companion and the github

https://github.com/FernandoBoza/auth-nest-graphql

 nest new project-name
Enter fullscreen mode Exit fullscreen mode

yarn add

  • @nestjs/graphql graphql
  • @nestjs/jwt
  • @nestjs/mongoose mongoose
  • @nestjs/passport passport passport-jwt
  • @nestjs/platform-fastify
  • bcrypt

yarn add -D

NestJS
Expres => Fastify main.ts

import { NestFactory } from '@nestjs/core';
import {
  FastifyAdapter,
  NestFastifyApplication,
} from '@nestjs/platform-fastify';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create<NestFastifyApplication>(
    AppModule,
    new FastifyAdapter(),
  );


  await app.listen(3000);
}
bootstrap().then((r) => r);
Enter fullscreen mode Exit fullscreen mode

Change app.controller => app.resolver

import { AppService } from './app.service';
import { Query, Resolver } from '@nestjs/graphql';

@Resolver()
export class AppResolver {
  constructor(private readonly appService: AppService) {}

  @Query(() => String)
  getHello(): string {
    return this.appService.getHello();
  }
}
Enter fullscreen mode Exit fullscreen mode

App module remove controller from @Module directory

MongooseModule.forRoot('mongodb://localhost:27017/nest-auth'),
GraphQLModule.forRoot({
    autoSchemaFile: true,
    playground: true,
    debug: false,
}),
Enter fullscreen mode Exit fullscreen mode

cli

nest g mo user
nest g s user
nest g r user
cd src/user 
nest g gu user
touch user.entity.ts
touch jwt.strategy.ts
touch user-inputs.dto.ts
touch user.guard.ts
Enter fullscreen mode Exit fullscreen mode

USER.TS => USER.ENTITY.TS

import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
import { Document, Types } from 'mongoose';

export enum Roles {
  Admin = 'Admin',
  Basic = 'Basic',
}

registerEnumType(Roles, {
  name: 'Roles',
  description: 'Admin create projects & tasks, Basic create tasks',
});

@ObjectType()
@Schema()
export class User {
  @Field(() => String)
  _id: Types.ObjectId;

  @Field()
  @Prop()
  firstName: string;

  @Field()
  @Prop()
  lastName: string;

  @Field()
  @Prop()
  password: string;

  @Field()
  @Prop({ unique: true })
  email: string;

  @Field({ nullable: true })
  @Prop()
  imageURL?: string;

  @Field((type) => Roles, { defaultValue: Roles.Admin, nullable: true })
  @Prop()
  role?: Roles;

  @Field()
  @Prop()
  createdAt: string = new Date().toISOString();
}

export type UserDocument = User & Document;

export const UserSchema = SchemaFactory.createForClass(User);

Enter fullscreen mode Exit fullscreen mode

JWT.STRATEGY.TS

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

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: 'hard!to-guess_secret',
    });
  }

  async validate(payload: any) {
    const { _id, email } = payload;
    return { _id, email };
  }
}
Enter fullscreen mode Exit fullscreen mode

USER MODULE


import { MongooseModule } from '@nestjs/mongoose';
import { User, UserSchema } from './user.entity';
import { JwtStrategy } from './jwt.strategy';
import { JwtModule } from '@nestjs/jwt';

@Module({
  imports: [
    JwtModule.register({
      secret: 'hard!to-guess_secret',
      signOptions: { expiresIn: '24h' },
    }),
    MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]),
  ],
  providers: [UserResolver, UserService, JwtStrategy],
})
export class UserModule {}
Enter fullscreen mode Exit fullscreen mode

user.guard.ts =>

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

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

user-inputs.dto.ts

import { InputType, Field, OmitType, PartialType } from '@nestjs/graphql';

@InputType()
export class CreateUserInput {
  @Field()
  firstName: string;

  @Field()
  lastName: string;

  @Field()
  password: string;

  @Field()
  // @IsEmail()
  email: string;

  @Field()
  createdAt: string = new Date().toISOString();
}


@InputType()
export class UpdateUserInput extends PartialType(
  OmitType(CreateUserInput, ['password'] as const),
) {}


Enter fullscreen mode Exit fullscreen mode

User.services


import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model, Types } from 'mongoose';
import { User, UserDocument } from './user.entity';
import { CreateUserInput, UpdateUserInput } from './user-inputs.dto';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt';
import { GraphQLError } from 'graphql';

@Injectable()
export class UserService {
  constructor(
    private jwtService: JwtService,
    @InjectModel(User.name) private UserModel: Model<UserDocument>,
  ) {}

  async create(createUserInput: CreateUserInput) {
    try {
      const isUser = await this.UserModel.findOne({
        email: createUserInput.email,
      });
      if (isUser) {
        throw new GraphQLError('Nah Bro, you already exist 🤡');
      } else {
        createUserInput.password = await bcrypt
          .hash(createUserInput.password, 10)
          .then((r) => r);
        return await new this.UserModel(createUserInput).save();
      }
    } catch (err) {
      console.error(err);
    }
  }

  async login({ password, email }) {
    try {
      const res = await this.UserModel.findOne({ email });
      return res && (await bcrypt.compare(password, res.password))
        ? this.getToken(email, res._id).then((result) => result)
        : new GraphQLError('Nah Bro, you already exist 🤡');
    } catch (err) {
      console.error(err);
    }
  }

  async getToken(email, _id): Promise<string> {
    try {
      return await this.jwtService.signAsync({ email, _id });
    } catch (err) {
      console.error(err);
    }
  }

  async findAll() {
    try {
      return await this.UserModel.find().exec();
    } catch (err) {
      console.error(err);
    }
  }

  async findOne(_id: Types.ObjectId) {
    try {
      return await this.UserModel.findById(_id).exec();
    } catch (err) {
      console.error(err);
    }
  }

  async update(_id, updateUserInput: UpdateUserInput) {
    try {
      return await this.UserModel.findByIdAndUpdate(_id, updateUserInput, {
        new: true,
      }).exec();
    } catch (err) {
      console.error(err);
    }
  }

async updatePassword(_id, userPass, newPass) {
    try {
      const User = await this.UserModel.findById({ _id: _id });
      if (await bcrypt.compare(userPass, User.password)) {
        User.password = await bcrypt.hash(newPass, 10);
        return await new this.UserModel(User).save();
      }
    } catch (err) {
      console.error(err);
    }
  }

  async remove(_id: string) {
    try {
      return await this.UserModel.findByIdAndDelete(_id).exec();
    } catch (err) {
      console.error(err);
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

USER.RESOLVER

import { Resolver, Query, Mutation, Args, Int } from '@nestjs/graphql';
import { UserService } from './user.service';
import { User } from './user.entity';
import { CreateUserInput, UpdateUserInput } from './user-inputs.dto';
import { Types } from 'mongoose';
import { CurrentUser } from './user.decorator';
import { UseGuards } from '@nestjs/common';
import { GqlAuthGuard } from './user.guard';
import { GraphQLError } from 'graphql';

@Resolver(() => User)
export class UserResolver {
  constructor(private readonly userService: UserService) {}

  @Mutation(() => User)
  async createUser(@Args('createUserInput') createUserInput: CreateUserInput) {
    try {
      return await this.userService.create(createUserInput);
    } catch (err) {
      console.error(err);
    }
  }

  @Mutation(() => String)
  async login(
    @Args('email') email: string,
    @Args('password') password: string,
  ): Promise<string | GraphQLError> {
    try {
      return await this.userService.login({ email, password });
    } catch (err) {
      console.error(err);
    }
  }

  @Query(() => [User])
  @UseGuards(GqlAuthGuard)
  async findAll() {
    try {
      return await this.userService.findAll();
    } catch (err) {
      console.error(err);
    }
  }

  @Query(() => User)
  @UseGuards(GqlAuthGuard)
  async findOne(@Args('_id', { type: () => String }) _id: Types.ObjectId) {
    try {
      return await this.userService.findOne(_id);
    } catch (err) {
      console.error(err);
    }
  }

  @Mutation(() => User)
  @UseGuards(GqlAuthGuard)
  async updateUser(
    @CurrentUser() user: User,
    @Args('updateUserInput')
    updateUserInput: UpdateUserInput,
  ) {
    try {
      return await this.userService.update(user._id, updateUserInput);
    } catch (err) {
      console.error(err);
    }
  }

  @Mutation(() => User)
  @UseGuards(GqlAuthGuard)
  async updatePassword(
    @CurrentUser() user: User,
    @Args('currPass') currPass: string,
    @Args('newPass') newPass: string,
  ) {
    try {
      return await this.userService.updatePassword(user._id, currPass, newPass);
    } catch (err) {
      console.error(err);
    }
  }

  @Mutation(() => User)
  @UseGuards(GqlAuthGuard)
  async removeUser(@Args('_id') _id: string) {
    try {
      return await this.userService.remove(_id);
    } catch (err) {
      console.error(err);
    }
  }

  @Query(() => User)
  @UseGuards(GqlAuthGuard)
  async CurrentUser(@CurrentUser() user: User) {
    try {
      return await this.userService.findOne(user._id);
    } catch (err) {
      console.error(err);
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

USER.DECORATOR

import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';

export const CurrentUser = createParamDecorator(
  (data: unknown, context: ExecutionContext) => {
    const { _id, email } = GqlExecutionContext.create(
      context,
    ).getContext().req.user;
    return {
      _id,
      email,
    };
  },
);

Enter fullscreen mode Exit fullscreen mode

brew services start mongodb-community@4.4
yarn run start:dev

Discussion (0)

pic
Editor guide