Good day to you. This series will share with you how to build a web application with server, client side, and you will learn many useful things on this project.
As the title you will learn how to
- Use NestJs build a server connect with a MongoDB (use JWT for authentication, how to query on server, create schema, entity, controller, service)
- Use NextJS build a client side with TailwindCss fast build component, Zustand state management (easy manage state), Axios Request API.
After check all this full series, I believe you will have a mindset how to build a website app from backend to frontend. You will understand how to become a full stack developer through this todo web app
Full Tutorial TodoList Apps with NextJS Zustand Axios TailwindCSS React NestJS Mongoose JWT (Day 3)
On day 3: We will focus on how to work with JWT (Json Web Token) for storing User information
Step 0: prepare, check below package.json and go to install it first
{
"name": "server",
"version": "0.0.1",
"description": "",
"author": "",
"private": true,
"license": "UNLICENSED",
"scripts": {
"prebuild": "rimraf dist",
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start: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"
},
"dependencies": {
"@nestjs/common": "^9.0.0",
"@nestjs/config": "^2.2.0",
"@nestjs/core": "^9.0.0",
"@nestjs/jwt": "^9.0.0",
"@nestjs/mongoose": "^9.2.1",
"@nestjs/passport": "^9.0.0",
"@nestjs/platform-express": "^9.0.0",
"@nestjs/swagger": "^6.1.4",
"@nestjs/typeorm": "^9.0.1",
"bcrypt": "^5.1.0",
"moment": "^2.29.4",
"mongoose": "^6.8.4",
"passport": "^0.6.0",
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"rxjs": "^7.2.0",
"typeorm": "^0.3.11"
},
"devDependencies": {
"@nestjs/cli": "^9.0.0",
"@nestjs/schematics": "^9.0.0",
"@nestjs/testing": "^9.0.0",
"@types/bcrypt": "^5.0.0",
"@types/express": "^4.17.13",
"@types/jest": "28.1.8",
"@types/node": "^16.0.0",
"@types/supertest": "^2.0.11",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",
"eslint": "^8.0.1",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
"jest": "28.1.3",
"prettier": "^2.3.2",
"source-map-support": "^0.5.20",
"supertest": "^6.1.3",
"ts-jest": "28.0.8",
"ts-loader": "^9.2.3",
"ts-node": "^10.0.0",
"tsconfig-paths": "4.1.0",
"typescript": "^4.7.4"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}
Step 1: Create structure like this
Step 2: Type command: nest g resource users will auto generate above folder
Step 3: Implement below code on each below file
src/auth/guard/jwt-auth.guard.ts
import { AuthGuard } from '@nestjs/passport';
import { ExecutionContext, Injectable } from '@nestjs/common';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
canActivate(context: ExecutionContext) {
const functionName: string = context.getHandler().name;
if (
functionName === 'register' ||
functionName === 'login' ||
functionName === 'checkAccessToken'
) {
// just allow register
return true;
}
return super.canActivate(context);
}
}
src/auth/constants.ts
export const jwtConstants = {
secret: 'secretKey-vilh',
};
src/jwt.config.ts
import { JwtOptionsFactory, JwtModuleOptions } from '@nestjs/jwt';
import { Injectable, Logger } from '@nestjs/common';
import { jwtConstants } from './constants';
@Injectable()
export class JwtConfig implements JwtOptionsFactory {
constructor() {}
createJwtOptions(): JwtModuleOptions {
return {
secret: jwtConstants.secret,
signOptions: {
expiresIn: '500s', // '100s',
},
};
}
}
src/auth/jwt.strategy.ts
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, HttpStatus, HttpException } from '@nestjs/common';
import { UsersService } from 'src/users/users.service';
import { jwtConstants } from './constants';
export interface IJWT {
id: string;
iat: number;
exp: number;
}
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private usersService: UsersService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
// algorithms: ['RS256'],
secretOrKey: jwtConstants.secret,
});
}
async validate(input: IJWT): Promise<any> {
const user = await this.usersService.validate(input.id);
if (!user) {
throw new HttpException('Unauthorized Access', HttpStatus.UNAUTHORIZED);
}
return user;
}
}
modified src/users/user.controller.ts
@UseGuards(JwtAuthGuard)
// huuvi168@gmail.com
// Zidane - Webzone Tech Tips, all things about web development
import {
Controller,
Get,
Post,
Body,
UseGuards,
Request,
Delete,
} from '@nestjs/common';
import { UsersService } from './users.service';
import { UsersDto } from './dto/users.dto';
import { JwtAuthGuard } from 'src/auth/guard/jwt-auth.guard';
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@UseGuards(JwtAuthGuard)
@Get('getProfile')
public async getProfile(@Request() req: any) {
return await this.usersService.getProfile(req.user.params._id);
}
}
import { ApiErrorResponse } from './../util/api-error-response.util';
import { ApiSucceedResponse } from 'src/util/api-success-response.util';
import { IUser } from './interface/users.interface';
import { Injectable, HttpException } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import mongoose, { Model, Types } from 'mongoose';
import { UsersDto } from './dto/users.dto';
import * as bcrypt from 'bcrypt';
import { JwtService } from '@nestjs/jwt';
@Injectable()
export class UsersService {
constructor(
@InjectModel('User') private readonly userModel: Model<IUser>,
private readonly jwtService: JwtService,
) {}
// https://localhost:8000/users => ok
public async getUsers() {
const users = await this.userModel.find().exec();
if (!users || !users[0]) {
throw new HttpException('User Not Found', 404);
}
return new ApiSucceedResponse("Retrieved all users", users);
}
public async login(user: UsersDto) {
const result = await this.userModel.findOne({
username: user.username,
});
if (!result) {
throw new HttpException('User not found', 404);
}
const isMatch = await bcrypt.compare(user.password, result.password);
if (!isMatch) {
throw new HttpException('Password is not match', 404);
}
const access_token = await this.jwtService.sign({ id: result._id });
return new ApiSucceedResponse('Login succeed', access_token);
}
public async register(newUser: UsersDto) {
// check exist
const checkExist = await this.userModel.findOne({
username: newUser.username
}).exec()
if (checkExist) {
throw new HttpException(`username ${newUser.username} is Exist`, 404)
}
const salt = await bcrypt.genSalt();
const hashPassword = await bcrypt.hash(newUser.password, salt);
newUser.password = hashPassword;
const user = await new this.userModel(newUser);
user.save();
if (user) {
return new ApiSucceedResponse('Registered user successfully', user);
}
return new ApiErrorResponse('Registered user failed', []);
}
async validate(id: string) {
const user = await this.getUserById(id);
return user ? user : null;
}
public async getUserById(id: string) {
const user = await this.userModel
.findById({ _id: new mongoose.Types.ObjectId(id) }) // use this way for get mongo objectID
.exec();
if (!user) {
throw new HttpException('User Not Found!', 404);
}
return new ApiSucceedResponse('Retrieved data successfully', user);
}
public async deleteUserById(id: string) {
const user = await this.userModel
.deleteOne({ _id: new mongoose.Types.ObjectId(id) })
.exec();
if (user.deletedCount === 0) {
throw new HttpException('User Not Found', 404);
}
return new ApiSucceedResponse('User was removed', []);
}
public async putUserById(
id: string,
propertyName: string,
propertyValue: string,
) {
const user = await this.userModel
.findOneAndUpdate(
{ _id: new Types.ObjectId(id) },
{
[propertyName]: [propertyValue],
},
)
.exec();
if (!user) {
throw new HttpException('Not Found', 404);
}
return new ApiSucceedResponse('User was update succeed', user);
}
public async getProfile(id: number) {
let user = await this.userModel
.findById({
_id: id,
})
.exec();
if (!user) {
throw new HttpException('User Not Found', 404);
}
return new ApiSucceedResponse('Retrieved data successfully', user);
}
}
Remember put Users on Module file
src/users/users.module.ts
import { MongooseModule } from '@nestjs/mongoose';
import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
import { UsersSchema } from './schemas/users.schema';
import { jwtConstants } from 'src/auth/constants';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { JwtStrategy } from 'src/auth/jwt.strategy';
@Module({
imports: [
MongooseModule.forFeature([
{
name: 'User',
schema: UsersSchema,
},
]),
PassportModule,
JwtModule.register({
secret: jwtConstants.secret,
signOptions: {
expiresIn: '3600s',
},
}),
],
controllers: [UsersController],
providers: [UsersService, JwtStrategy],
})
export class UsersModule {}