Welcome you to my blog series on user authentication in the NestJS framework. This series is a set of several tutorials that extend the official documentation.
If you are programming an enterprise application, you will most likely want to include full user support, i.e. email account confirmation, password reminder capability, etc.
This post is the first part in a series and below you can find a list of all the other articles on this topic.
Note: Weβll be getting started programming on the Nest.js prepared official typescript starter, which you can find at this link. Also remember that all the source code from this article is available on my GitHub profile.
The application uses the following technology stack:
We will use my own software architecture, because in my opinion it is better than the one presented in the official framework template.
.
βββ node_modules
βββ src
β βββ app
β β βββ constants
β β β βββ app.constant.ts
β β β βββ index.ts
β β βββ index.ts
β βββ authentication
β β βββ controllers
β β β βββ authentication.controller.ts
β β β βββ index.ts
β β βββ dtos
β β β βββ authentication.dto.ts
β β β βββ create-authentication.dto.ts
β β β βββ index.ts
β β β βββ registration.dto.ts
β β βββ entities
β β β βββ authentication.entity.ts
β β β βββ index.ts
β β βββ exceptions
β β β βββ index.ts
β β β βββ user-already-exist.exception.ts
β β βββ providers
β β β βββ authentication.provider.ts
β β β βββ index.ts
β β βββ repositories
β β β βββ authentication.repository.ts
β β β βββ index.ts
β β βββ services
β β β βββ authentication.service.ts
β β β βββ index.ts
β β βββ subscribers
β β β βββ authentication.subscriber.ts
β β β βββ index.ts
β β βββ index.ts
β βββ common
β β βββ dtos
β β β βββ abstract.dto.ts
β β β βββ index.ts
β β βββ entities
β β βββ abstract.entity.ts
β β βββ index.ts
β βββ database
β β βββ constraints
β β β βββ errors.constraint.ts
β β β βββ index.ts
β β βββ strategies
β β β βββ index.ts
β β β βββ snake-naming.strategy.ts
β β βββ index.ts
β βββ user
β β βββ dtos
β β β βββ create-user.dto.ts
β β β βββ index.ts
β β β βββ user.dto.ts
β β βββ entities
β β β βββ index.ts
β β β βββ user.entity.ts
β β βββ repositories
β β β βββ index.ts
β β β βββ user.repository.ts
β β βββ services
β β β βββ index.ts
β β β βββ user.service.ts
β β βββ index.ts
β βββ util
β β βββ index.ts
β β βββ setup-swagger.util.ts
β βββ main.ts
βββ .env
βββ .eslintrc.js
βββ .gitignore
βββ .prettierrc
βββ nest-cli.json
βββ package.json
βββ README.md
βββ tsconfig.build.json
βββ tsconfig.json
βββ yarn.lock
We will need some external packages that we will use to program the authorization.
Note: I use package manager yarn instead of npm by default, so you can remove your package-lock.json file if you chose the typescript starter.
yarn add pg @hapi/joi @nestjs/config @nestjs/typeorm typeorm class-transformer class-validator bcrypt
yarn add @types/bcrypt @types/hapi__joi --dev
Your application will use environment variables that will remain completely private and cannot be stored in the repository. We also installed the hapi library, which will always check that the data is in the correct format before compiling the code.
We also installed the @hapi/joi library, which will always check that the data is in the correct format before compiling the code.
Create an .env file in the root directory:
# Application Settings
PORT=9000
NODE_ENV=development
# Database Settings
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_USER=pietrzakadrian
POSTGRES_PASSWORD=
POSTGRES_DB=nestjs-authentication-full
This will keep our code more readable. In the src/app/constants directory I create a new file app.constant.ts:
export enum NODE_ENV {
DEVELOPMENT = "development",
PRODUCTION = "production",
}
Now we can define a validator in the index.ts file of the app module. The implementation looks like this:
import * as Joi from "@hapi/joi";
import { Module } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
import { NODE_ENV } from "./constants";
@Module({
imports: [
ConfigModule.forRoot({
validationSchema: Joi.object({
PORT: Joi.number().required(),
NODE_ENV: Joi.string()
.required()
.valid(NODE_ENV.DEVELOPMENT, NODE_ENV.PRODUCTION),
POSTGRES_HOST: Joi.string().required(),
POSTGRES_PORT: Joi.number().required(),
POSTGRES_USER: Joi.string().required(),
POSTGRES_PASSWORD: Joi.string().required().allow(""),
POSTGRES_DB: Joi.string().required(),
}),
}),
],
})
export class AppModule {}
Note: My database password is an empty string, therefore I allow it to be left empty.
We can also use them elsewhere in the application, i.e. in the main.ts file:
import { ConfigService } from "@nestjs/config";
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const configService = app.get(ConfigService);
const PORT = +configService.get<number>("PORT");
await app.listen(PORT);
}
void bootstrap();
Environment variables have been implemented. Brilliant! π
Itβs time to program the database module. This module, as the name suggests, is responsible for communicating with postgres.
We will include the snake_naming strategy and add a constraint for the unique value.
In the src/database/constraints directory, create the errors.constraint.ts file:
export enum PostgresErrorCode {
UniqueViolation = "23505",
}
This is needed to catch an error in the controller if the specified email address repeats.
Defined in the typescript model, a field such as firstName will be stored as first_name in the db. This will improve code readability and keep it in titleCase format.
Create snake-naming.strategy.ts in src/database/strategies:
import { DefaultNamingStrategy, NamingStrategyInterface } from "typeorm";
import { snakeCase } from "typeorm/util/StringUtils";
export class SnakeNamingStrategy
extends DefaultNamingStrategy
implements NamingStrategyInterface
{
tableName(className: string, customName: string): string {
return customName ? customName : snakeCase(className);
}
columnName(
propertyName: string,
customName: string,
embeddedPrefixes: string[],
): string {
return (
snakeCase(embeddedPrefixes.join("_")) +
(customName ? customName : snakeCase(propertyName))
);
}
relationName(propertyName: string): string {
return snakeCase(propertyName);
}
joinColumnName(relationName: string, referencedColumnName: string): string {
return snakeCase(relationName + "_" + referencedColumnName);
}
joinTableName(
firstTableName: string,
secondTableName: string,
firstPropertyName: string,
_secondPropertyName: string,
): string {
return snakeCase(
firstTableName +
"_" +
firstPropertyName.replace(/\./gi, "_") +
"_" +
secondTableName,
);
}
joinTableColumnName(
tableName: string,
propertyName: string,
columnName?: string,
): string {
return snakeCase(
tableName + "_" + (columnName ? columnName : propertyName),
);
}
classTableInheritanceParentColumnName(
parentTableName: string,
parentTableIdPropertyName: string,
): string {
return snakeCase(`${parentTableName}_${parentTableIdPropertyName}`);
}
}
Finally, create a base class by importing the required dependencies:
import { Module } from "@nestjs/common";
import { ConfigModule, ConfigService } from "@nestjs/config";
import { TypeOrmModule } from "@nestjs/typeorm";
import { NODE_ENV } from "src/app/constants";
import { SnakeNamingStrategy } from "./strategies";
@Module({
imports: [
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
type: "postgres",
host: configService.get("POSTGRES_HOST"),
port: configService.get("POSTGRES_PORT"),
username: configService.get("POSTGRES_USER"),
password: configService.get("POSTGRES_PASSWORD"),
database: configService.get("POSTGRES_DB"),
entities: [__dirname + "/../**/*.entity{.ts,.js}"],
namingStrategy: new SnakeNamingStrategy(),
synchronize: configService.get("NODE_ENV") === NODE_ENV.DEVELOPMENT,
logging: configService.get("NODE_ENV") === NODE_ENV.DEVELOPMENT,
extra: { charset: "utf8mb4_unicode_ci" },
}),
}),
],
})
export class DatabaseModule {}
Note: Define charset utf8mb4 if you want to be able to store emoji in database.
To make your code complicated, import this module to the main application module.
+ import { DatabaseModule } from 'src/database';
@Module({
imports: [
ConfigModule.forRoot({
...
}),
+ DatabaseModule,
],
})
export class AppModule {}
Thatβs right, now the database is used in our application. Getting closer to completion! π¨
Our data models will contain common parts such as id column, uuid, created_at etc. This is an ideal opportunity to create an abstract class from which the other models will inherit.
Create a new file in the src/common/entities directory called abstract.entity.ts.
import { Exclude } from "class-transformer";
import {
Column,
CreateDateColumn,
Generated,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from "typeorm";
export abstract class AbstractEntity {
@PrimaryGeneratedColumn()
@Exclude()
public id: number;
@Column()
@Generated("uuid")
public uuid: string;
@CreateDateColumn()
@Exclude()
public createdAt: Date;
@UpdateDateColumn()
@Exclude()
public updatedAt: Date;
}
We use the @Exclude() decorator to not return sensitive data in the controllers response. For this to work, we need to make some changes to the main.ts application file:
+ import { ClassSerializerInterceptor, ValidationPipe } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const reflector = app.get(Reflector);
const configService = app.get(ConfigService);
const PORT = +configService.get<number>('PORT');
+ app.useGlobalPipes(new ValidationPipe({ transform: true }));
+ app.useGlobalInterceptors(new ClassSerializerInterceptor(reflector));
await app.listen(PORT);
}
void bootstrap();
Now everything is ok. Validation will be tested at the end of this article.
We will be registering a new user, so it would be a good idea to create a module responsible for processing user data. Sensitive data such as email, password, tokens will be stored in another entity and will be bound by a one to one relationship.
Note: This will keep the database more organized, but when creating a new user we will have to use a transaction.
First, define the data model. As I mentioned earlier, authorization information such as email or password will be stored in another table. Therefore, we will make a one to one relationship.
So in the src/users/entities directory we create a file called user.entity.ts:
import { AuthenticationEntity } from "src/authentication/entities";
import { AbstractEntity } from "src/common/entities";
import { Column, Entity, JoinColumn, OneToOne, Index } from "typeorm";
@Entity({ name: "users" })
export class UserEntity extends AbstractEntity {
@Column()
public firstName: string;
@OneToOne(
() => AuthenticationEntity,
(authentication: AuthenticationEntity) => authentication.user,
{ eager: true, nullable: false, onDelete: "CASCADE" },
)
@JoinColumn()
@Index()
public authentication: AuthenticationEntity;
}
βοΈ Note: At this point, Authentication Entity is not yet defined. Weβll do that in a moment when we get to the authentication section.
This is also what a repository is made for, so letβs make it similar to what we just did. In the repositories directory, letβs create the file user.repository.ts:
import { Repository } from "typeorm";
import { EntityRepository } from "typeorm/decorator/EntityRepository";
import { UserEntity } from "../entities";
@EntityRepository(UserEntity)
export class UserRepository extends Repository<UserEntity> {}
Letβs also prepare a Data Transform Object class that will contain validations. You can put these decorators simply in the user.entity.ts file, but it wonβt be stylish. It is better to make user.dto.ts file in src/user/dtos directory.
import { IsNotEmpty, IsString } from "class-validator";
import { CreateAuthenticationDto } from "src/authentication/dtos";
export class CreateUserDto extends CreateAuthenticationDto {
@IsString()
@IsNotEmpty()
readonly firstName: string;
}
Note: CreateAuthenticationDto is also not defined yet, but weβll do that in a moment.
From this point on, when creating a new entity, firstName will be required and without it, the code will not execute.
Services are responsible for executing the logic. The controller uses service dependencies.
As I mentioned earlier, we will be using transactions, so letβs already prepare a function that provides all the required parameters.
Create a file user.service.ts in the src/user/services directory:
import { Injectable } from "@nestjs/common";
import { CreateUserDto } from "src/user/dtos";
import { AuthenticationEntity } from "src/authentication/entities";
import { QueryRunner } from "typeorm";
import { UserEntity } from "../entities";
import { UserRepository } from "../repositories";
@Injectable()
export class UserService {
constructor(private readonly _userRepository: UserRepository) {}
async createUser(
createUserDto: CreateUserDto,
authentication: AuthenticationEntity,
queryRunner: QueryRunner,
): Promise<UserEntity> {
const user = this._userRepository.create({
...createUserDto,
authentication,
});
return queryRunner.manager.save(user);
}
}
Now we can put all the dependencies together. In the main module class, instantiate the services:
import { Module } from "@nestjs/common";
import { TypeOrmModule } from "@nestjs/typeorm";
import { UserRepository } from "./repositories";
import { UserService } from "./services";
@Module({
imports: [TypeOrmModule.forFeature([UserRepository])],
providers: [UserService],
exports: [UserService],
})
export class UserModule {}
Also, donβt forget to add a module to the base class:
+ import { UserModule } from 'src/user';
@Module({
imports: [
ConfigModule.forRoot({
...
}),
DatabaseModule,
+ UserModule,
],
})
export class AppModule {}
Now the user module has been programmed in its entirety. β
Letβs talk about the authentication module. In this directory, you will store all the logic associated with it.
As I wrote earlier, there will be a data model here that will store sensitive information such as email address or password.
Create the authentication.entity.ts file in the src/authentication/entities directory:
import { Exclude } from "class-transformer";
import { AbstractEntity } from "src/common/entities";
import { UserEntity } from "src/user/entities";
import { Column, Entity, OneToOne } from "typeorm";
@Entity({ name: "authentications" })
export class AuthenticationEntity extends AbstractEntity {
@Column({ unique: true })
public emailAddress: string;
@Column()
@Exclude()
public password: string;
@OneToOne(() => UserEntity, (user: UserEntity) => user.authentication)
@Exclude()
public user: UserEntity;
}
Note: You can now bind these 2 entities with a one to one relationship.
For the model, you traditionally create a repository:
import { Repository } from "typeorm";
import { EntityRepository } from "typeorm/decorator/EntityRepository";
import { AuthenticationEntity } from "../entities";
@EntityRepository(AuthenticationEntity)
export class AuthenticationRepository extends Repository<AuthenticationEntity> {}
And letβs finally create the missing dto class:
import { IsEmail, IsNotEmpty, IsString, MinLength } from "class-validator";
export class CreateAuthenticationDto {
@IsEmail()
@IsNotEmpty()
readonly emailAddress: string;
@IsString()
@IsNotEmpty()
@MinLength(6)
readonly password: string;
}
You need a class that is responsible for performing the hash service.
A good practice is to create authentication.provider.ts in the src/authentication/providers directory:
import * as bcrypt from "bcrypt";
export class AuthenticationProvider {
static async generateHash(password: string): Promise<string> {
return bcrypt.hash(password, 10);
}
}
Letβs deal with the subscriber now. You should already know that sensitive data such as a password should not be a simple string of characters. If the data is leaked from the database, all your accounts will be stolen.
TypeORM has the ability to perform the operation before the insertion, and this is an ideal place to convert a password from a string to a hash.
Create authentication.subscriber.ts in the directory src/authentication/subscribers:
import {
EntitySubscriberInterface,
EventSubscriber,
InsertEvent,
UpdateEvent,
} from "typeorm";
import { AuthenticationEntity } from "../entities";
import { AuthenticationProvider } from "../providers";
@EventSubscriber()
export class AuthenticationSubscriber
implements EntitySubscriberInterface<AuthenticationEntity>
{
listenTo() {
return AuthenticationEntity;
}
async beforeInsert({
entity,
}: InsertEvent<AuthenticationEntity>): Promise<void> {
if (entity.password) {
entity.password = await AuthenticationProvider.generateHash(
entity.password,
);
}
if (entity.emailAddress) {
entity.emailAddress = entity.emailAddress.toLowerCase();
}
}
async beforeUpdate({
entity,
databaseEntity,
}: UpdateEvent<AuthenticationEntity>): Promise<void> {
if (entity.password) {
const password = await AuthenticationProvider.generateHash(
entity.password,
);
if (password !== databaseEntity?.password) {
entity.password = password;
}
}
}
}
Note: This is a good opportunity to save your email address in lower case format.
Remember to run the subscriber in the subscribers array in the database module:
+ import { AuthenticationSubscriber } from 'src/authentication/subscribers';
@Module({
imports: [
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
...
+ subscribers: [AuthenticationSubscriber],
}),
}),
],
})
export class DatabaseModule {}
Here we will write the logic that the controller will execute.
As I mentioned, we will be using transactions because we are inserting data in two tables (users and authentications).
Now look at the code you put in the authentication.service.ts file:
import { Injectable, InternalServerErrorException } from "@nestjs/common";
import { PostgresErrorCode } from "src/database/constraints";
import { UserEntity } from "src/user/entities";
import { UserService } from "src/user/services";
import { Connection, QueryRunner } from "typeorm";
import { CreateAuthenticationDto } from "../dtos";
import { RegistrationDto } from "../dtos/registration.dto";
import { AuthenticationEntity } from "../entities";
import { UserAlreadyExistException } from "../exceptions";
import { AuthenticationRepository } from "../repositories";
@Injectable()
export class AuthenticationService {
constructor(
private readonly _authenticationRepository: AuthenticationRepository,
private readonly _userService: UserService,
private readonly _connection: Connection,
) {}
async registration(registrationDto: RegistrationDto): Promise<UserEntity> {
let user: UserEntity;
const queryRunner = this._connection.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
const authentication = await this._createAuthentication(
registrationDto,
queryRunner,
);
user = await this._userService.createUser(
registrationDto,
authentication,
queryRunner,
);
await queryRunner.commitTransaction();
} catch (error) {
await queryRunner.rollbackTransaction();
if (error?.code === PostgresErrorCode.UniqueViolation) {
throw new UserAlreadyExistException();
}
throw new InternalServerErrorException();
} finally {
await queryRunner.release();
}
return user;
}
private async _createAuthentication(
createAuthenticationDto: CreateAuthenticationDto,
queryRunner: QueryRunner,
): Promise<AuthenticationEntity> {
const authentication = this._authenticationRepository.create(
createAuthenticationDto,
);
return queryRunner.manager.save(authentication);
}
}
Remember that the email address must be unique. It is good to be able to catch this error, so we create user-already-exist.exception.ts in the exceptions directory:
import { BadRequestException } from "@nestjs/common";
export class UserAlreadyExistException extends BadRequestException {
constructor(error?: string) {
super("User with that email already exists", error);
}
}
Additionally, we use RegistrationDto make the code look more stylish. This is basically the same as CreateUserDto, but named differently:
import { CreateUserDto } from "src/user/dtos";
export class RegistrationDto extends CreateUserDto {}
The service is ready, it can now be exposed to the controller for use. β
The registration logic is located at /Authentication/registration.
Note: Ideally, it should be hosted at /Users with the POST method. But this is hard to do, because then it will make circular dependency. User module will use authentication dependency and vice versa. It can be solved this way, but it is an antipattern. Itβs not worth doing.
Create an authentication.controller.ts file in the controlers directory:
import { Body, Controller, HttpCode, HttpStatus, Post } from "@nestjs/common";
import { UserEntity } from "src/user/entities";
import { RegistrationDto } from "../dtos";
import { AuthenticationService } from "../services";
@Controller("Authentication")
export class AuthenticationController {
constructor(private readonly _authenticationService: AuthenticationService) {}
@Post("registration")
@HttpCode(HttpStatus.OK)
async registration(
@Body() registrationDto: RegistrationDto,
): Promise<UserEntity> {
return this._authenticationService.registration(registrationDto);
}
}
Now everything is merged into a single whole using the module.
import { Module } from "@nestjs/common";
import { TypeOrmModule } from "@nestjs/typeorm";
import { UserModule } from "src/user";
import { AuthenticationController } from "./controllers";
import { AuthenticationRepository } from "./repositories";
import { AuthenticationService } from "./services";
@Module({
imports: [UserModule, TypeOrmModule.forFeature([AuthenticationRepository])],
providers: [AuthenticationService],
controllers: [AuthenticationController],
})
export class AuthenticationModule {}
You must declare this module in the main application module as well:
+ import { AuthenticationModule } from 'src/authentication';
@Module({
imports: [
ConfigModule.forRoot({
...
}),
DatabaseModule,
+ AuthenticationModule,
UserModule,
],
})
export class AppModule {}
Yes sir, everything is ready. Good job how you made it here. π Letβs see if this masterpiece even works. π
It is now time to test our application. Letβs send a POST request to the controller with the address /Authentication/registration with the body attached:
{
"firstName": "Adrian",
"emailAddress": "contact@pietrzakadrian.com",
"password": "123456"
}
The controller will perform the following SQL operation:
START TRANSACTION
INSERT INTO "authentications"("uuid", "created_at", "updated_at", "email_address", "password") VALUES (DEFAULT, DEFAULT, DEFAULT, $1, $2) RETURNING "id", "uuid", "created_at", "updated_at" -- PARAMETERS: ["contact@pietrzakadrian.com","$2b$10$x0oV4oPS7ehhCSp537ygruWKxKpSX4MXlluqvxzSibRFCh2kMSS7i"]
INSERT INTO "users"("uuid", "created_at", "updated_at", "first_name", "authentication_id") VALUES (DEFAULT, DEFAULT, DEFAULT, $1, $2) RETURNING "id", "uuid", "created_at", "updated_at" -- PARAMETERS: ["Adrian",1]
COMMIT
Note: You can see that the password has been encoded by the subscriber before insert record to database.
And in response, we get the created object, without sensitive data such as id or password:
{
"uuid": "0cc8f6cd-44f4-4d73-9bef-9f3b872180c4",
"firstName": "Adrian",
"authentication": {
"uuid": "b3c257e6-85b4-49b8-b0a0-877e3c936a4e",
"emailAddress": "contact@pietrzakadrian.com"
}
}
Letβs still check that the validation is working. I will now send a request without a firstName:
{
"statusCode": 400,
"message": ["firstName should not be empty", "firstName must be a string"],
"error": "Bad Request"
}
I will still try to add the same email address:
{
"statusCode": 400,
"message": "User with that email already exists",
"error": "Bad Request"
}
Perfect! The effect that was wanted was obtained. π
Technical documentation is necessary when producing professional software. It is also very useful for a developer. The NestJS framework provides a tool that makes it very easy to generate OpenAPI documentation. We will now add this to the project, this will allow us to keep the source code transparent later.
Install the required dependencies into your project:
yarn add @nestjs/swagger swagger-ui-express
Create setup-swagger.util.ts file in the utils directory:
import type { INestApplication } from "@nestjs/common";
import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger";
export function setupSwagger(app: INestApplication): void {
const options = new DocumentBuilder()
.setTitle("NestJS-Authentication-Full")
.setContact(
"Adrian Pietrzak",
"https://pietrzakadrian.com",
"contact@pietrzakadrian.com",
)
.build();
const document = SwaggerModule.createDocument(app, options);
SwaggerModule.setup("documentation", app, document);
}
Now you can compile the documentation along with the application.
+ import { setupSwagger } from './util';
async function bootstrap() {
...
+ if (configService.get<string>('NODE_ENV') === NODE_ENV.DEVELOPMENT) {
+ setupSwagger(app);
+ }
await app.listen(PORT);
}
void bootstrap();
Note: Remember to make it available only in development mode.
Now you need to add some decorators to your dto and controller classes. Keep track now of the changes I make to the files:
import { ApiProperty } from "@nestjs/swagger";
export class AbstractDto {
@ApiProperty({ format: "uuid" })
readonly uuid: string;
}
+ import { ApiProperty } from '@nestjs/swagger';
export class CreateUserDto extends CreateAuthenticationDto {
@IsString()
@IsNotEmpty()
+ @ApiProperty()
readonly firstName: string;
}
+ import { ApiProperty } from '@nestjs/swagger';
- export class UserDto {
+ export class UserDto extends AbstractDto {
+ @ApiProperty()
readonly firstName: string;
+ @ApiProperty({ type: () => AuthenticationDto })
readonly authentication: AuthenticationDto;
}
+ import { ApiProperty } from '@nestjs/swagger';
export class CreateAuthenticationDto {
@IsEmail()
@IsNotEmpty()
+ @ApiProperty()
readonly emailAddress: string;
@IsString()
@IsNotEmpty()
@MinLength(6)
+ @ApiProperty()
readonly password: string;
}
import { ApiProperty } from '@nestjs/swagger';
import { AbstractDto } from 'src/common/dtos';
- export class AuthenticationDto {
+ export class AuthenticationDto extends AbstractDto {
+ @ApiProperty()
readonly emailAddress: string;
}
+ import { ApiBadRequestResponse, ApiInternalServerErrorResponse, ApiOkResponse, ApiTags } from '@nestjs/swagger';
+ import { UserDto } from 'src/user/dtos';
@Controller('Authentication')
+ @ApiTags('Authentication')
export class AuthenticationController {
constructor(private readonly _authenticationService: AuthenticationService) {}
@Post('registration')
@HttpCode(HttpStatus.OK)
+ @ApiOkResponse({ type: UserDto, description: 'Successfully created user' })
+ @ApiBadRequestResponse({ description: 'User with that email already exists.' })
+ @ApiInternalServerErrorResponse({ description: 'Internal server error' })
async registration(
@Body() registrationDto: RegistrationDto,
): Promise<UserEntity> {
return this._authenticationService.registration(registrationDto);
}
}
When you go to /documentation, you will get a properly generated Swagger document:
A very quick change and simplifies the process of documenting software application. π
This article presents a complete implementation of new user registration based on advanced programming techniques of TypeScript language and NestJS framework. Environment variables were defined and validated. The subscriber operation in TypeORM was explained and transactions in postgres was applied.
You can also find this article on medium.com where I share my solutions to the problems I encountered during my software engineer career.