Series Intro
This series will cover the full implementation of OAuth2.0 Authentication in NestJS for the following types of APIs:
- Express REST API;
- Fastify REST API;
- Apollo GraphQL API.
And it is divided in 5 parts:
- Configuration and operations;
- Express Local OAuth REST API;
- Fastify Local OAuth REST API;
- Apollo Local OAuth GraphQL API;
- Adding External OAuth Providers to our API;
Lets start the second part of this series.
Tutorial Intro
On this tutorial we will continue building on the previous article, by creating an Express REST API.
TLDR: if you do not have 45 minutes to read the article, the code can be found on this repo
Set up
Start by creating controllers for both the auth
and users
:
$ nest g co auth
$ nest g co users
Auth Module
Before implementing the endpoints we need both an auth guard, and some custom decorators:
- Public: to make an endpoint public;
- Origin: to get the origin from cross-origin requests:
- CurrentUser: to get the current logged user id;
Decorators
Before creating decorators, since we will have a user field on our express Request
interface install express-serve-static-core
:
$ yarn add -D @types/express-serve-static-core
And extend express on a types
folder:
// express.d.ts
import { Request as ExpressRequest } from 'express';
declare module 'express-serve-static-core' {
interface Request extends ExpressRequest {
user?: number;
}
}
To be able to create a global auth guard we need to create the Public
decorator first, this decorator can be found here in the docs, on the decorators
folder add:
// public.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
To validate that our tokens come from the correct origin we need to get the domain from the the front-end:
// origin.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { Request } from 'express-serve-static-core';
export const Origin = createParamDecorator(
(_, context: ExecutionContext): string | undefined => {
return context.switchToHttp().getRequest<Request>().headers?.origin;
},
);
Finally we need to be able to get the current logged user ID:
// current-user.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { Request } from 'express-serve-static-core';
export const CurrentUser = createParamDecorator(
(_, context: ExecutionContext): number | undefined => {
return context.switchToHttp().getRequest<Request>()?.user;
},
);
Guards
Auth Guard
We need a global AuthGuard
to protect our endpoints, on a guards
folder, we can create a CanActivate
guard:
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { isJWT } from 'class-validator';
import { Request } from 'express';
import { isNull, isUndefined } from '../../common/utils/validation.util';
import { TokenTypeEnum } from '../../jwt/enums/token-type.enum';
import { JwtService } from '../../jwt/jwt.service';
import { IS_PUBLIC_KEY } from '../decorators/public.decorator';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(
private readonly reflector: Reflector,
private readonly jwtService: JwtService,
) {}
public async canActivate(context: ExecutionContext): Promise<boolean> {
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
const activate = await this.setHttpHeader(
context.switchToHttp().getRequest(),
isPublic,
);
if (!activate) {
throw new UnauthorizedException();
}
return activate;
}
/**
* Sets HTTP Header
*
* Checks if the header has a valid Bearer token, validates it and sets the User ID as the user.
*/
private async setHttpHeader(
req: Request,
isPublic: boolean,
): Promise<boolean> {
const auth = req.headers?.authorization;
if (isUndefined(auth) || isNull(auth) || auth.length === 0) {
return isPublic;
}
const authArr = auth.split(' ');
const bearer = authArr[0];
const token = authArr[1];
if (isUndefined(bearer) || isNull(bearer) || bearer !== 'Bearer') {
return isPublic;
}
if (isUndefined(token) || isNull(token) || !isJWT(token)) {
return isPublic;
}
try {
const { id } = await this.jwtService.verifyToken(
token,
TokenTypeEnum.ACCESS,
);
req.user = id;
return true;
} catch (_) {
return isPublic;
}
}
}
Finally add the guard to the AppModule
:
// ...
import { CacheModule, Module } from '@nestjs/common';
// ...
import { APP_GUARD } from '@nestjs/core';
import { AuthGuard } from './auth/guards/auth.guard';
// ...
@Module({
// ...
providers: [
{
provide: APP_GUARD,
useClass: AuthGuard,
},
],
})
export class AppModule {}
(Optional) Throttler Guard
To protect our auth
endpoints against brute force attacks we should add rate limiting to our API, NestJS has a ThrottlerModule
, start by installing it (and optionally the Redis storage for it):
$ yarn add @nestjs/throttler nestjs-throttler-storage-redis
Add the throttler options to .env
:
# ...
# Throttler config
THROTTLE_TTL=60
THROTTLE_LIMIT=20
To the schema:
import Joi from 'joi';
export const validationSchema = Joi.object({
// ...
THROTTLE_TTL: Joi.number().required(),
THROTTLE_LIMIT: Joi.number().required(),
});
To the config.interface.ts
file:
// ...
import { ThrottlerModuleOptions } from '@nestjs/throttler';
// ...
export interface IConfig {
// ...
throttler: ThrottlerModuleOptions;
// ...
}
And on the index.ts
of the config
folder:
// ...
import { IConfig } from './interfaces/config.interface';
// ...
export function config(): IConfig {
// ...
return {
// ...
throttler: {
ttl: parseInt(process.env.THROTTLE_TTL, 10),
limit: parseInt(process.env.THROTTLE_LIMIT, 10),
},
// ...
};
}
Now add a config class for the throttler:
// throttler.config.ts
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import {
ThrottlerModuleOptions,
ThrottlerOptionsFactory,
} from '@nestjs/throttler';
import { RedisOptions } from 'ioredis';
import { ThrottlerStorageRedisService } from 'nestjs-throttler-storage-redis';
@Injectable()
export class ThrottlerConfig implements ThrottlerOptionsFactory {
constructor(private readonly configService: ConfigService) {}
createThrottlerOptions(): ThrottlerModuleOptions {
return {
...this.configService.get<ThrottlerModuleOptions>('throttler'),
storage: new ThrottlerStorageRedisService(
this.configService.get<RedisOptions>('redis'),
),
};
}
}
Register the ThrottlerModule
on the AuthModule
:
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { ThrottlerModule } from '@nestjs/throttler';
import { ThrottlerConfig } from '../config/throttler.config';
// ...
@Module({
imports: [
// ...
ThrottlerModule.forRootAsync({
imports: [ConfigModule],
useClass: ThrottlerConfig,
}),
],
// ...
})
export class AuthModule {}
Finally add the ThrottlerGuard
to the auth controller:
import { Controller, UseGuards } from '@nestjs/common';
import { ThrottlerGuard } from '@nestjs/throttler';
@Controller('auth')
@UseGuards(ThrottlerGuard)
export class AuthController {}
Controller
Mappers
Some data is private and should never be sent by the response of our API, such as the user password and the refresh token. Therefore, we need to create mapper functions that reflect what we want to send as a response of our API.
Interfaces
There are two interfaces that we need, one with the user data that we want to send and one for the Auth Result.
Auth Response User
// auth-response-user.interface.ts
export interface IAuthResponseUser {
id: number;
name: string;
username: string;
email: string;
createdAt: string;
updatedAt: string;
}
Auth Response
// auth-response.interface.ts
import { IAuthResponseUser } from './auth-response-user.interface';
export interface IAuthResponse {
user: IAuthResponseUser;
accessToken: string;
}
Classes
Now for the mapper we can either add a static method to our controllers or create separate classes with a map static method. I recommend the latter as it is easier to add OpenAPI specifications latter on.
Create a new folder called mappers
and add the following files:
-
auth-response-user.mapper.ts
:
import { IUser } from '../../users/interfaces/user.interface'; import { IAuthResponseUser } from '../interfaces/auth-response-user.interface'; export class AuthResponseUserMapper implements IAuthResponseUser { public id: number; public name: string; public username: string; public email: string; public createdAt: string; public updatedAt: string; constructor(values: IAuthResponseUser) { Object.assign(this, values); } public static map(user: IUser): AuthResponseUserMapper { return new AuthResponseUserMapper({ id: user.id, name: user.name, username: user.username, email: user.email, createdAt: user.createdAt.toISOString(), updatedAt: user.updatedAt.toISOString(), }); } }
-
auth-response.mapper.ts
:
import { IAuthResponse } from '../interfaces/auth-response.interface'; import { IAuthResult } from '../interfaces/auth-result.interface'; import { AuthResponseUserMapper } from './auth-response-user.mapper'; export class AuthResponseMapper implements IAuthResponse { public user: AuthResponseUserMapper; public accessToken: string; constructor(values: IAuthResponse) { Object.assign(this, values); } public static map(result: IAuthResult): AuthResponseMapper { return new AuthResponseMapper({ user: AuthResponseUserMapper.map(result.user), accessToken: result.accessToken, }); } }
Methods
While most of the business logic resides on the service, we still need some logic to be added here on the controller.
Start by injecting the AuthService
and UserService
, and by getting the COOKIE_NAME
and JWT_REFRESH_TIME
from the ConfigService
:
import { Controller, UseGuards } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { UsersService } from '../users/users.service';
import { AuthService } from './auth.service';
// ...
@Controller('api/auth')
@UseGuards(ThrottlerGuard)
export class AuthController {
private readonly cookiePath = '/api/auth';
private readonly cookieName: string;
private readonly refreshTime: number;
private readonly testing: boolean;
constructor(
private readonly authService: AuthService,
private readonly usersService: UsersService,
private readonly configService: ConfigService,
) {
this.cookieName = this.configService.get<string>('REFRESH_COOKIE');
this.refreshTime = this.configService.get<number>('jwt.refresh.time');
this.testing = this.configService.get<string>('NODE_ENV') !== 'production';
}
}
And add two methods:
-
Getting refresh token out of the request:
// ... @Controller('api/auth') @UseGuards(ThrottlerGuard) export class AuthController { // ... private refreshTokenFromReq(req: Request): string { const token: string | undefined = req.signedCookies[this.cookieName]; if (isUndefined(token)) { throw new UnauthorizedException(); } return token; } }
-
Saving the refresh token in a cookie:
@Controller('api/auth') @UseGuards(ThrottlerGuard) export class AuthController { // ... private saveRefreshCookie(res: Response, refreshToken: string): Response { return res.cookie(this.cookieName, refreshToken, { secure: !this.testing, httpOnly: true, signed: true, path: this.cookiePath, expires: new Date(Date.now() + this.refreshTime * 1000), }); } }
Endpoints
The endpoints are the same as the public methods we have on our service.
Sign Up
import {
Body,
//...
Post,
// ...
} from '@nestjs/common';
// ...
import { Origin } from './decorators/origin.decorator';
import { Public } from './decorators/public.decorator';
// ...
import { SignUpDto } from './dtos/sign-up.dto';
@Controller('api/auth')
@UseGuards(ThrottlerGuard)
export class AuthController {
// ...
@Public()
@Post('/sign-up')
public async signUp(
@Origin() origin: string | undefined,
@Body() signUpDto: SignUpDto,
): Promise<IMessage> {
return this.authService.signUp(signUpDto, origin);
}
// ...
}
Sign In
import {
Body,
HttpStatus,
Res,
// ...
} from '@nestjs/common';
import { SignInDto } from './dtos/sign-in.dto';
import { AuthResponseMapper } from './mappers/auth-response.mapper';
@Controller('api/auth')
@UseGuards(ThrottlerGuard)
export class AuthController {
// ...
@Public()
@Post('/sign-in')
public async signIn(
@Res() res: Response,
@Origin() origin: string | undefined,
@Body() singInDto: SignInDto,
): Promise<void> {
const result = await this.authService.signIn(singInDto, origin);
this.saveRefreshCookie(res, result.refreshToken)
.status(HttpStatus.OK)
.json(AuthResponseMapper.map(result));
}
// ...
}
Refresh Access
import {
//...
Req,
// ...
} from '@nestjs/common';
// ...
@Controller('api/auth')
@UseGuards(ThrottlerGuard)
export class AuthController {
// ...
@Public()
@Post('/refresh-access')
public async refreshAccess(
@Req() req: Request,
@Res() res: Response,
): Promise<void> {
const token = this.refreshTokenFromReq(req);
const result = await this.authService.refreshTokenAccess(
token,
req.headers.origin,
);
this.saveRefreshCookie(res, result.refreshToken)
.status(HttpStatus.OK)
.json(AuthResponseMapper.map(result));
}
// ...
}
Logout
import {
//...
HttpCode,
// ...
} from '@nestjs/common';
// ...
@Controller('api/auth')
@UseGuards(ThrottlerGuard)
export class AuthController {
// ...
@Post('/logout')
@HttpCode(HttpStatus.OK)
public async logout(
@Req() req: Request,
@Res() res: Response,
): Promise<void> {
const token = this.refreshTokenFromReq(req);
const message = await this.authService.logout(token);
res
.clearCookie(this.cookieName, { path: this.cookiePath })
.status(HttpStatus.OK)
.json(message);
}
// ...
}
Confirm Email
// ...
@Controller('api/auth')
@UseGuards(ThrottlerGuard)
export class AuthController {
// ...
@Public()
@Post('/confirm-email')
public async confirmEmail(
@Body() confirmEmailDto: ConfirmEmailDto,
@Res() res: Response,
): Promise<void> {
const result = await this.authService.confirmEmail(confirmEmailDto);
this.saveRefreshCookie(res, result.refreshToken)
.status(HttpStatus.OK)
.json(AuthResponseMapper.map(result));
}
// ...
}
Forgot Password
// ...
@Controller('api/auth')
@UseGuards(ThrottlerGuard)
export class AuthController {
// ...
@Post('/forgot-password')
@HttpCode(HttpStatus.OK)
public async forgotPassword(
@Origin() origin: string | undefined,
@Body() emailDto: EmailDto,
): Promise<IMessage> {
return this.authService.resetPasswordEmail(emailDto, origin);
}
// ...
}
Reset Password
// ...
@Controller('api/auth')
@UseGuards(ThrottlerGuard)
export class AuthController {
// ...
@Public()
@Post('/reset-password')
@HttpCode(HttpStatus.OK)
public async resetPassword(
@Body() resetPasswordDto: ResetPasswordDto,
): Promise<IMessage> {
return this.authService.resetPassword(resetPasswordDto);
}
// ...
}
Update Password
import {
// ...
Patch,
// ...
} from '@nestjs/common';
import { CurrentUser } from './decorators/current-user.decorator';
import { ChangePasswordDto } from './dtos/change-password.dto';
// ...
@Controller('api/auth')
@UseGuards(ThrottlerGuard)
export class AuthController {
// ...
@Patch('/update-password')
public async updatePassword(
@CurrentUser() userId: number,
@Origin() origin: string | undefined,
@Body() changePasswordDto: ChangePasswordDto,
@Res() res: Response,
): Promise<void> {
const result = await this.authService.updatePassword(
userId,
changePasswordDto,
origin,
);
this.saveRefreshCookie(res, result.refreshToken)
.status(HttpStatus.OK)
.json(AuthResponseMapper.map(result));
}
// ...
}
Me
This one is necessary to get the current logged user
import {
// ...
Get,
// ...
} from '@nestjs/common';
import { IAuthResponseUser } from '../auth/interfaces/auth-response-user.interface';
import { AuthResponseUserMapper } from '../auth/mappers/auth-response-user.mapper';
// ...
@Controller('api/auth')
@UseGuards(ThrottlerGuard)
export class AuthController {
// ...
@Get('/me')
public async getMe(@CurrentUser() id: number): Promise<IAuthResponseUser> {
const user = await this.usersService.findOneById(id);
return AuthResponseUserMapper.map(user);
}
// ...
}
User Module
User Service
Before adding our controller we need to make some changes to the service:
-
Add a
PasswordDto
:
// password.dto.ts import { IsString, MinLength } from 'class-validator'; export abstract class PasswordDto { @IsString() @MinLength(1) public password: string; }
-
Update the
ChangeEmailDto
:
// change-email.dto.ts import { IsEmail, IsString, Length } from 'class-validator'; import { PasswordDto } from './password.dto'; export abstract class ChangeEmailDto extends PasswordDto { @ApiProperty({ description: 'The email of the user', example: 'someone@gmail.com', minLength: 5, maxLength: 255, type: String, }) @IsString() @IsEmail() @Length(5, 255) public email: string; }
-
Add password verification to
delete
method:
// ... import { BadRequestException, // ... } from '@nestjs/common'; import { compare, hash } from 'bcrypt'; // ... import { PasswordDto } from './dtos/password.dto'; import { UserEntity } from './entities/user.entity'; @Injectable() export class UsersService { // ... public async delete(userId: number, dto: PasswordDto): Promise<UserEntity> { const user = await this.findOneById(userId); if (!(await compare(dto.password, user.password))) { throw new BadRequestException('Wrong password'); } await this.commonService.removeEntity(this.usersRepository, user); return user; } // ... }
-
Add a method to find by id or username:
// ... @Injectable() export class UsersService { // ... public async findOneByIdOrUsername( idOrUsername: string, ): Promise<UserEntity> { const parsedValue = parseInt(idOrUsername, 10); if (!isNaN(parsedValue) && parsedValue > 0 && isInt(parsedValue)) { return this.findOneById(parsedValue); } if ( idOrUsername.length < 3 || idOrUsername.length > 106 || !SLUG_REGEX.test(idOrUsername) ) { throw new BadRequestException('Invalid username'); } return this.findOneByUsername(idOrUsername); } // ... }
User Controller
Mappers
The user return by the user controller should not have the email, so create a new interface:
// response-user.interface.ts
export interface IResponseUser {
id: number;
name: string;
username: string;
createdAt: string;
updatedAt: string;
}
And implement it:
// response-user.mapper.ts
import { IResponseUser } from '../interfaces/response-user.interface';
import { IUser } from '../interfaces/user.interface';
export class ResponseUserMapper implements IResponseUser {
public id: number;
public name: string;
public username: string;
public createdAt: string;
public updatedAt: string;
constructor(values: IResponseUser) {
Object.assign(this, values);
}
public static map(user: IUser): ResponseUserMapper {
return new ResponseUserMapper({
id: user.id,
name: user.name,
username: user.username,
createdAt: user.createdAt.toISOString(),
updatedAt: user.updatedAt.toISOString(),
});
}
}
Endpoints
Start by adding the cookiePath
and cookieName
for the delete route:
import { Controller } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { UsersService } from './users.service';
@Controller('api/users')
export class UsersController {
private cookiePath = '/api/auth';
private cookieName: string;
constructor(
private readonly usersService: UsersService,
private readonly configService: ConfigService,
) {
this.cookieName = this.configService.get<string>('COOKIE_NAME');
}
}
User controller is comprised mostly by user Read, Update and Delete operations.
Get User
Start by creating a dto for the params:
// get-user.params.ts
import { IsString, Length } from 'class-validator';
export abstract class GetUserParams {
@IsString()
@Length(1, 106)
public idOrUsername: string;
}
And add it as the type of the Param
decorator:
import {
// ...
Get,
Param,
// ...
} from '@nestjs/common';
// ...
import { GetUserParams } from './dtos/get-user.params';
// ...
@Controller('api/users')
export class UsersController {
// ...
@Public()
@Get('/:idOrUsername')
public async getUser(@Param() params: GetUserParams): Promise<IResponseUser> {
const user = await this.usersService.findOneByIdOrUsername(idOrUsername);
return ResponseUserMapper.map(user);
}
// ...
}
Update Email
Since it updtes the email we need to return AuthResponseUserMapper
:
import {
Body,
// ...
Patch,
} from '@nestjs/common';
// ...
import { ChangeEmailDto } from './dtos/change-email.dto';
// ...
@Controller('api/users')
export class UsersController {
// ...
@Patch('/email')
public async updateEmail(
@CurrentUser() id: number,
@Body() dto: ChangeEmailDto,
): Promise<IAuthResponseUser> {
const user = await this.usersService.updateEmail(id, dto);
return AuthResponseUserMapper.map(user);
}
// ...
}
Update User
// ...
import { UpdateUserDto } from './dtos/update-user.dto';
// ...
@Controller('api/users')
export class UsersController {
// ...
@Patch()
public async updateUser(
@CurrentUser() id: number,
@Body() dto: UpdateUserDto,
): Promise<IResponseUser> {
const user = await this.usersService.update(id, dto);
return ResponseUserMapper.map(user);
}
// ...
}
Delete User
import {
// ...
Delete,
// ...,
HttpStatus,
// ...
} from '@nestjs/common';
import { PasswordDto } from './dtos/password.dto';
// ...
@Controller('api/users')
export class UsersController {
// ...
@Delete()
@ApiNoContentResponse({
description: 'The user is deleted.',
})
@ApiBadRequestResponse({
description: 'Something is invalid on the request body, or wrong password.',
})
@ApiUnauthorizedResponse({
description: 'The user is not logged in.',
})
public async deleteUser(
@CurrentUser() id: number,
@Body() dto: PasswordDto,
@Res() res: Response,
): Promise<void> {
await this.usersService.delete(id, dto);
res
.clearCookie(this.cookieName, { path: this.cookiePath })
.status(HttpStatus.NO_CONTENT)
.send();
}
// ...
}
Main file
Finally we need to set-up our main file, start by installing cookie-parser and helmet:
$ yarn add cookie-parser helmet
$ yarn add -D @types/cookie-parser
And add them, as well as a validation pipe to the main file:
import { ValidationPipe } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { NestFactory } from '@nestjs/core';
import cookieParser from 'cookie-parser';
import helmet from 'helmet';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const configService = app.get(ConfigService);
app.use(cookieParser(configService.get<string>('COOKIE_SECRET')));
app.use(helmet());
app.enableCors({
credentials: true,
origin: `https://${configService.get<string>('domain')}`,
});
app.useGlobalPipes(
new ValidationPipe({
transform: true,
}),
);
await app.listen(configService.get<number>('port'));
}
bootstrap();
(Optional) Swagger
How will our front-end team know how to use our API? This is where documentation is important, to create documentation automatic we can use NestJS Swagger, start by installing it:
$ yarn add @nestjs/swagger
And add descriptions to all DTOS, Mappers and endpoints, I will exemplify with the users
, but is exactly the same process for the auth
.
DTOs
Add ApiProperty
decorators all dtos:
Username DTO
import { ApiProperty } from '@nestjs/swagger';
import { IsString, Length, Matches } from 'class-validator';
import { SLUG_REGEX } from '../../common/consts/regex.const';
export abstract class UsernameDto {
@ApiProperty({
description: 'The username of the user',
minLength: 3,
maxLength: 106,
example: 'my-username',
type: String,
})
@IsString()
@Length(3, 106)
@Matches(SLUG_REGEX, {
message: 'Username must be a valid slugs',
})
public username: string;
}
Password DTO
import { ApiProperty } from '@nestjs/swagger';
import { IsString, MinLength } from 'class-validator';
export abstract class PasswordDto {
@ApiProperty({
description: 'The password of the user',
minLength: 1,
type: String,
})
@IsString()
@MinLength(1)
public password: string;
}
Change Email DTO
import { ApiProperty } from '@nestjs/swagger';
import { IsEmail, IsString, Length } from 'class-validator';
import { PasswordDto } from './password.dto';
export abstract class ChangeEmailDto extends PasswordDto {
@ApiProperty({
description: 'The email of the user',
example: 'someone@gmail.com',
minLength: 5,
maxLength: 255,
type: String,
})
@IsString()
@IsEmail()
@Length(5, 255)
public email: string;
}
Get User Params*
import { ApiProperty } from '@nestjs/swagger';
import { IsString, Length } from 'class-validator';
export abstract class GetUserParams {
@ApiProperty({
description: 'The id or username of the user',
type: String,
example: "1 or 'username'",
})
@IsString()
@Length(1, 106)
public idOrUsername: string;
}
Mappers
Add ApiProperty
decorators the ResponseUserMapper
:
import { ApiProperty } from '@nestjs/swagger';
import { IResponseUser } from '../interfaces/response-user.interface';
import { IUser } from '../interfaces/user.interface';
export class ResponseUserMapper implements IResponseUser {
@ApiProperty({
description: 'User id',
example: 123,
minimum: 1,
type: Number,
})
public id: number;
@ApiProperty({
description: 'User name',
example: 'John Doe',
minLength: 3,
maxLength: 100,
type: String,
})
public name: string;
@ApiProperty({
description: 'User username',
example: 'john.doe1',
minLength: 3,
maxLength: 106,
type: String,
})
public username: string;
@ApiProperty({
description: 'User creation date',
example: '2021-01-01T00:00:00.000Z',
type: String,
})
public createdAt: string;
@ApiProperty({
description: 'User last update date',
example: '2021-01-01T00:00:00.000Z',
type: String,
})
public updatedAt: string;
constructor(values: IResponseUser) {
Object.assign(this, values);
}
public static map(user: IUser): ResponseUserMapper {
return new ResponseUserMapper({
id: user.id,
name: user.name,
username: user.username,
createdAt: user.createdAt.toISOString(),
updatedAt: user.updatedAt.toISOString(),
});
}
}
Enpoints
NestJS swagger exports an api response type for each HTTP status code, add one for each response an endpoint can have, with a type of a Mapper for the 200's status codes:
// ...
import {
ApiBadRequestResponse,
ApiNoContentResponse,
ApiNotFoundResponse,
ApiOkResponse,
ApiTags,
ApiUnauthorizedResponse,
} from '@nestjs/swagger';
// ...
@ApiTags('Users')
@Controller('api/users')
export class UsersController {
// ...
@Public()
@Get('/:idOrUsername')
@ApiOkResponse({
type: ResponseUserMapper,
description: 'The user is found and returned.',
})
@ApiBadRequestResponse({
description: 'Something is invalid on the request body',
})
@ApiNotFoundResponse({
description: 'The user is not found.',
})
public async getUser(
// ...
): Promise<IResponseUser> {
// ...
}
@Patch('/email')
@ApiOkResponse({
type: AuthResponseUserMapper,
description: 'The email is updated, and the user is returned.',
})
@ApiBadRequestResponse({
description: 'Something is invalid on the request body, or wrong password.',
})
@ApiUnauthorizedResponse({
description: 'The user is not logged in.',
})
public async updateEmail(
// ...
): Promise<IAuthResponseUser> {
// ...
}
@Patch()
@ApiOkResponse({
type: ResponseUserMapper,
description: 'The username is updated.',
})
@ApiBadRequestResponse({
description: 'Something is invalid on the request body.',
})
@ApiUnauthorizedResponse({
description: 'The user is not logged in.',
})
public async updateUser(
// ...
): Promise<IResponseUser> {
// ...
}
@Delete()
@ApiNoContentResponse({
description: 'The user is deleted.',
})
@ApiBadRequestResponse({
description: 'Something is invalid on the request body, or wrong password.',
})
@ApiUnauthorizedResponse({
description: 'The user is not logged in.',
})
public async deleteUser(
// ...
): Promise<void> {
// ...
}
}
Main file
Finally, add the document builder to your main file:
// ...
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
// ...
async function bootstrap() {
// ...
const swaggerConfig = new DocumentBuilder()
.setTitle('NestJS Authentication API')
.setDescription('A OAuth2.0 authentication API made with NestJS')
.setVersion('0.0.1')
.addBearerAuth()
.addTag('Authentication API')
.build();
const document = SwaggerModule.createDocument(app, swaggerConfig);
SwaggerModule.setup('api/docs', app, document);
// ...
}
bootstrap();
Conclusion
With this you now have a full functioning Authentication Microsservice to use on your REST apps. The github for this tutorial can be found here.
About the Author
Hey there! I am Afonso Barracha, your go-to econometrician who found his way into the world of back-end development with a soft spot for GraphQL. If you enjoyed reading this article, why not show some love by buying me a coffee?
Lately, I have been diving deep into more advanced subjects. As a result, I have switched from sharing my thoughts every week to posting once or twice a month. This way, I can make sure to bring you the highest quality content possible.
Do not miss out on any of my latest articles β follow me here on dev and LinkedIn to stay updated. I would be thrilled to welcome you to our ever-growing community! See you around!
Top comments (6)
Nice article ππ Currently using this on my project.kindly post the rest of the parts as soon as possible.i'm new to nestjs and this is my first time with nestjs.
i just created my account on dev.to just to thanks you.
Thank you so much for this series
Thanks, I will try to update this series once a week, just follow me to get the next update between Friday and Sunday next week.
Note that after I finish this series, the next series will be on the Tokio stack and Rust for TypeScript NodeJS Developer, so it may not interest you.
However, on May or June I will create a series on Advance NestJS so keep tuned for that. It will cover the development of a NestJS Hybrid Monolith API (REST & GraphQL) and transforming it into a microservices architecture.
This looks nice ! Great effort on the detailed imports sections as well as using Swagger documentation in a proper way π π― π I've been implementing such endpoints for job purposes and your code has similarities, so it's good to see !
Also, I'll check the authentication course from NestJS official team and see how I'll implement my next project's authentication and users management. Might maybe use an external provider to handle the job and remove the additional dev to do these tasks π€ Will see ! Anyways, great job again ! ;)
Thank you so much for this. I know passport is not absolutely necessary, however I'd like to understand your reasoning for not using it.
I mainly do not use Passport since I prefer Fastify, as unlike Express, it is actively maintained. So I got used to not have access to it.
That being said, since guards already exist in NestJS I do not see any need for an extra layer of abstraction.
So I added some site for testing and forgot to remove on the article, the same site strict cookie policy should not be used in production.