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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
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,
};
},
);