Create a project manager app using Next, Nest-Fastify GraphQL Mongo

Published: April 30, 2020

Time to read: 16 min

Create a project manager app using Next, Nest-Fastify GraphQL Mongo

Published: April 30, 2020

Time to read: 16 min

In this article, we will create a project manager application using NestJS, Fastify, GraphQL, and MongoDB. This will be a quick guide to create a backend application for managing projects and tasks.

Setting up the project

First, create a new NestJS project using the command nest new project-name. This command will generate a new NestJS project with the specified name. Once the project is created, navigate to the project directory.

code block is bash

nest new project-name

yarn add
- @nestjs/graphql graphql
- @nestjs/jwt
- @nestjs/mongoose mongoose
- @nestjs/passport passport passport-jwt
- @nestjs/platform-fastify
- bcrypt


yarn add  -D
- apollo-server-fastify@3.0.0-alpha.3

NestJS 	Expres => Fastify main.ts

Next, install the necessary packages using the yarn add command. We'll need several packages for this project, such as GraphQL support, JWT authentication, Mongoose for MongoDB integration, Passport for authentication, Fastify as the web framework, and bcrypt for password hashing.

After installing the required packages, add the development dependencies, such as apollo-server-fastify, using the yarn add -D command. This package provides Fastify integration for Apollo Server.

Configuring Fastify

To replace the default Express platform with Fastify, update the main.ts file. Import the FastifyAdapter and NestFastifyApplication from the @nestjs/platform-fastify package, and then create a new FastifyAdapter instance while creating the application.

code block is 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);

Setting up GraphQL

To set up GraphQL in your application, you'll need to make several changes. First, replace the app.controller.ts file with the app.resolver.ts file, which will be responsible for handling GraphQL queries and mutations. Import the necessary GraphQL decorators and create a basic query to return a "Hello" string.

code block is ts
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();
  }
}

Next, update the app.module.ts file to remove the controller from the @Module decorator and add the necessary imports for GraphQL and Mongoose. Configure the MongooseModule with your MongoDB connection string and set the GraphQLModule options to enable the GraphQL playground and disable debug mode.

code block is ts
MongooseModule.forRoot('mongodb://localhost:27017/nest-auth'),
GraphQLModule.forRoot({
    autoSchemaFile: true,
    playground: true,
    debug: false,
}),

Now, you'll need to generate the necessary files for the User module, such as model, service, and resolver. Additionally, create the user.entity.ts, jwt.strategy.ts, user-inputs.dto.ts, and user.guard.ts files to handle user-related functionality.

code block is bash
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

Defining the User Entity

In the user.entity.ts file, define a User class with the necessary properties, such as first name, last name, email, password, role, and created date. Use the @ObjectType() and @Schema() decorators to define the class as a GraphQL object type and a Mongoose schema. Export the UserDocument and UserSchema for use in other parts of the application.

code block is 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);

Implementing JWT Authentication

Create a JwtStrategy class in the jwt.strategy.ts file. This class will be responsible for validating JWT tokens in the application. Extend the PassportStrategy class and configure it with the necessary options, such as JWT extraction method, expiration, and secret key.

code block is 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 };
  }
}

Configuring the User Module

In the user.module.ts file, import the necessary modules, such as JwtModule and MongooseModule, and register the JWT and Mongoose configurations. Also, add the JwtStrategy to the module's providers.

code block is ts
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 {}

Implementing User Authentication Guard

Create a GqlAuthGuard class in the user.guard.ts file, which extends the AuthGuard class from the @nestjs/passport package. This class will be responsible for protecting GraphQL routes that require authentication. Override the getRequest() method to return the correct request object in a GraphQL context.

code block is 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;
  }
}

Defining User Input DTOs

In the user-inputs.dto.ts file, define the CreateUserInput and ``LoginUserInputclasses, which will represent the input data for creating and logging in users, respectively. Use the@InputType()decorator to define these classes as GraphQL input types, and add the necessary fields, such as first name, last name, email, password, and role. Use the@Field()` decorator to expose the fields as GraphQL input fields.

code block is 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();
}

Implementing User Resolver

In the user.resolver.ts file, create a UserResolver class that will handle user-related GraphQL queries and mutations. Import the necessary decorators, such as @Query(), @Mutation(), and @UseGuards(). Also, inject the UserService to interact with the user entity.

Define the following methods within the UserResolver class:

createUser(): A mutation to create a new user. It takes a CreateUserInput object as an argument and returns a User object. Use the @Args() decorator to extract the input object from the request.

login(): A mutation to authenticate a user. It takes a LoginUserInput object as an argument and returns a JWT token. Use the @Args() decorator to extract the input object from the request.

me(): A query to return the currently authenticated user. Use the @Context() decorator to extract the request object, and retrieve the user from the request. Apply the GqlAuthGuard to protect this route.

code block is ts
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);
    }
  }
}

Implementing User Service

In the user.service.ts file, create a UserService class that will handle the business logic for user-related operations. Inject the Model for the User entity and import the necessary libraries, such as bcrypt for password hashing and jwt for token generation.

Define the following methods within the UserService class:

create(): A method to create a new user. It takes a CreateUserInput object as an argument and returns a User object. Hash the user's password before saving the user to the database.

findOneByEmail(): A method to find a user by their email address. It takes an email address as an argument and returns a User object or null if the user does not exist.

validateUserPassword(): A method to validate a user's password. It takes an email address and a password as arguments and returns a User object or null if the user does not exist or the password is incorrect.

createToken(): A method to generate a JWT token for a user. It takes a User object as an argument and returns a JWT token containing the user's ID and role.

code block is ts
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);
    }
  }
}

Implementing Project and Task Modules

Following the same approach as the User module, create Project and Task modules, which will include their respective entities, services, and resolvers. Implement the necessary queries, mutations, and relationships between the entities.

Testing the Application

Finally, run the application using the yarn start command and open the GraphQL playground in your browser. Test the various queries and mutations to ensure that the application is functioning correctly.

In conclusion, we've built a simple project manager application using NestJS, Fastify, GraphQL, and MongoDB. This article demonstrates the basic concepts of creating a backend application with these technologies and can be expanded upon to create a more complex project management system.

tip: you can use the context to grab the user from the request and use it in your resolvers

code block is ts
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,
    };
  },
);