visit
In this article, I’m going to walk you through developing a simple todo rest application in NestJS and give you an overview of this framework.
npm i -g @nestjs/cli
nest new TodoBackend
Run
npm run start
in todo-backend folder to make sure everything is okay!nest generate module todo
@Module({
imports: [],
providers: [],
controllers: []
})
export class TodoModule {}
nest generate class todo/entities/Todo --no-spec
export class Todo {
public id: number;
public title: string;
public completed: boolean;
public constructor(title: string) {
this.title = title;
this.completed = false;
}
}
Now that we have our entity we just have to persist it through an ORM!
For this guide I have decided to use and setting up a basic connection to an databasenpm i @nestjs/typeorm typeorm sqlite3
We modify AppModule by importing TypeOrmModule with the static method forRoot, inserting the configurations we need:
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import * as path from 'path';
import { TodoModule } from './todo/todo.module';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'sqlite',
autoLoadEntities: true,
synchronize: true,
database: path.resolve(__dirname, '..', 'db.sqlite')
}),
TodoModule
]
})
export class AppModule {}
Let’s add TypeOrmModule also on TodoModule, this time using the forFeature method, specifying Todo as the entity to manage:
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Todo } from './entities';
@Module({
imports: [
TypeOrmModule.forFeature([Todo])
],
providers: [],
controllers: []
})
export class TodoModule {}
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class Todo {
@PrimaryGeneratedColumn()
public id: number;
@Column()
public title: string;
@Column()
public completed: boolean;
public constructor(title: string) {
this.title = title;
this.completed = false;
}
}
You can read further informations about Typeorm and its annotations by consulting the link attached at the beginning of the step.
For TypeOrmModule forRoot and forFeature methods, you can consult the database section in the official NestJS documentation:
To avoid exposing our entities outside our business logic layer, we define a set of classes that will be used to manage the communication in and out of our services: the DTO (Data Transfer Objects).
export class AddTodoDto {
public readonly title: string;
public constructor(opts?: Partial<AddTodoDto>) {
Object.assign(this, opts);
}
}
export class EditTodoDto {
public readonly title: string;
public readonly completed: boolean;
public constructor(opts?: Partial<EditTodoDto>) {
Object.assign(this, opts);
}
}
export class TodoDto {
public readonly id: number;
public readonly title: string;
public readonly completed: boolean;
public constructor(opts?: Partial<TodoDto>) {
Object.assign(this, opts);
}
}
nest generate service todo/services/todo
In the created service, we implement the findAll, findOne, add, edit and delete methods which, through the DTOs, will be consumed by our controller.
To decouple the conversion logic from Entity to DTO (and vice versa) from the business logic, let’s create a TodoMapperService:nest generate service todo/services/TodoMapper
import { Injectable } from '@nestjs/common';
import { Todo } from '../../entities';
import { TodoDto } from '../../dto';
@Injectable()
export class TodoMapperService {
public modelToDto({ id, title, completed }: Todo): TodoDto {
return new TodoDto({ id, title, completed });
}
}
Now let’s implement our TodoService: we inject, through Dependency Injection, the Todo Repository provided by Typeorm and our TodoMapperService:
import { isNullOrUndefined } from 'util';
import { Injectable, NotFoundException } from '@nestjs/common';
import { Repository } from 'typeorm';
import { InjectRepository } from '@nestjs/typeorm';
import { Todo } from '../../entities';
import { TodoDto, AddTodoDto, EditTodoDto } from '../../dto';
import { TodoMapperService } from '../todo-mapper/todo-mapper.service';
@Injectable()
export class TodoService {
public constructor(
@InjectRepository(Todo) private readonly todoRepository: Repository<Todo>,
private readonly todoMapper: TodoMapperService
) {}
public async findAll(): Promise<TodoDto[]> {
const todos = await this.todoRepository.find();
return todos.map(this.todoMapper.modelToDto);
}
public async findOne(id: number): Promise<TodoDto> {
const todo = await this.todoRepository.findOne(id);
if (isNullOrUndefined(todo)) throw new NotFoundException();
return this.todoMapper.modelToDto(todo);
}
public async add({ title }: AddTodoDto): Promise<TodoDto> {
let todo = new Todo(title);
todo = await this.todoRepository.save(todo);
return this.todoMapper.modelToDto(todo);
}
public async edit(id: number, { title, completed }: EditTodoDto): Promise<TodoDto> {
let todo = await this.todoRepository.findOne(id);
if (isNullOrUndefined(todo)) throw new NotFoundException();
todo.completed = completed;
todo.title = title;
todo = await this.todoRepository.save(todo);
return this.todoMapper.modelToDto(todo);
}
public async remove(id: number): Promise<Todo> {
let todo = await this.todoRepository.findOne(id);
if (isNullOrUndefined(todo)) throw new NotFoundException();
todo = await this.todoRepository.remove(todo);
return todo;
}
}
nest generate controller todo/controllers/todo
Let’s implement the methods that will mirror the rest calls we listed at the beginning of the article, decorate them with routing annotations and hook them to the TodoService methods:import { TodoService } from './../services/todo/todo.service';
import { TodoDto, AddTodoDto, EditTodoDto } from './../dto';
import {
Controller,
Get,
Param,
Post,
Put,
Body,
Delete
} from '@nestjs/common';
@Controller('todos')
export class TodoController {
public constructor(private readonly todoService: TodoService) {}
@Get()
public findAll(): Promise<TodoDto[]> {
return this.todoService.findAll();
}
@Get(':id')
public findOne(@Param('id') id: number): Promise<TodoDto> {
return this.todoService.findOne(id);
}
@Put(':id')
public edit(@Param('id') id: number, @Body() todo: EditTodoDto): Promise<TodoDto> {
return this.todoService.edit(id, todo);
}
@Post()
public add(@Body() todo: AddTodoDto): Promise<TodoDto> {
return this.todoService.add(todo);
}
@Delete(':id')
public remove(@Param('id') id: number): Promise<TodoDto> {
return this.todoService.remove(id);
}
}
WARNING: DTO serialization is not active unless you decorate your controller method with
ClassSerializerInterceptor
@Post()
@UseInterceptors(ClassSerializerInterceptor)
public add(@Body() todo: AddTodoDto): Promise<TodoDto> {
In the next step we will deepen this topic by developing a solution that allows us to centralize this interceptor 😉To handle the validation of our fields, NestJS provides a validation pipe that takes advantage of the class-transformer and class-validator libraries. To be able to use it however, we need to install its dependencies in the project:
npm i class-transformer class-validator
Let’s add the ValidationPipe to the global pipes:import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({ transform: true }));
await app.listen(3000);
}
bootstrap();
import { IsNotEmpty } from 'class-validator';
export class EditTodoDto {
@IsNotEmpty()
public readonly title: string;
public readonly completed: boolean;
public constructor(opts?: Partial<EditTodoDto>) {
Object.assign(this, opts);
}
}
WARNING: Once our application has been compiled, all the DTOs we have defined so far will be converted into javascript objects, this means that no type check will be performed on the values of its fields!
So will our validators only work as long as they are passed the right type values? NO.
The class-validator library also has a set of validators specifically designed to type check our fields at runtime:import { IsBoolean, IsNotEmpty, IsString } from 'class-validator';
export class EditTodoDto {
@IsString()
@IsNotEmpty()
public readonly title: string;
@IsBoolean()
public readonly completed: boolean;
public constructor(opts?: Partial<EditTodoDto>) {
Object.assign(this, opts);
}
}
npm run start
If we need to debug our code, we will have to run the command:npm run start:debug
Cross-Origin Resource Sharing (CORS) is a mechanism that uses additional HTTP headers to tell browsers to give a web application running at one , access to selected resources from a different origin. […] For security reasons, browsers restrict cross-origin HTTP requests initiated from scripts. For example, XmlHttpRequest and the Fetch API follow the same-origin policy.
To enable CORS just edit main.ts again by invoking the enableCors() method with all the configuration parameters we need. For simplicity, we will enable everything! (Freedom! 🤟)
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({ transform: true }));
app.enableCors();
await app.listen(3000);
}
bootstrap();
import { Test, TestingModule } from '@nestjs/testing';
import { TodoService } from './todo.service';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Todo } from './../../entities';
import { repositoryMockFactory, MockType } from './../../../utils/test/repository.mock';
import { TodoMapperService } from './../todo-mapper/todo-mapper.service';
import { Repository } from 'typeorm';
describe('TodoService', () => {
let service: TodoService;
let repository: MockType<Repository<Todo>>;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
TodoService,
TodoMapperService,
{ provide: getRepositoryToken(Todo), useFactory: repositoryMockFactory }
],
}).compile();
repository = module.get<Repository<Todo>>(getRepositoryToken(Todo)) as unknown as MockType<Repository<Todo>>;
service = module.get<TodoService>(TodoService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
it('should throws exception when todo not exists', async () => {
repository.findOne.mockReturnValue(Promise.resolve(null));
await expect(service.findOne(1)).rejects.toThrow('Not Found');
});
});
Photo by on