“Nest is a framework for building efficient, scalable Node.js web applications.”
$ git clone https://github.com/nestjs/typescript-starter.git heroes
$ cd heroes
$ npm install
$ npm start
$ cd heroes-tour
$ npm install
$ npm start
import { Get, Controller } from '@nestjs/common';
import { Hero } from './interfaces/hero.interface';
@Controller('heroes')
export class AppController {
private HEROES: Hero[] = [];
@Get()
heroes(): Hero[] {
return this.HEROES;
}
}
$ npm install cors bodyparser @types/cors
server.ts
import { NestFactory } from '@nestjs/core';
import { ApplicationModule } from './modules/app.module';
import * as bodyParser from 'body-parser';
import * as cors from 'cors';
async function bootstrap() {
const app = await NestFactory.create(ApplicationModule);
app.use(cors());
app.use(bodyParser.json());
await app.listen(3000);
}
bootstrap();
@Get()
heroes(): Hero[] {
return this.HEROES;
}
@Get(':id')
findById( @Param('id') id: string): Hero {
const heroID = parseInt(id, 10); //Pipe
return this.HEROES.find(hero => hero.id === heroID);
}
@Get('search/:name')
findByName( @Param('name') name: string): Hero[] {
return this.HEROES.filter(hero => hero.name.includes(name));
}
@Delete(':id')
deleteById( @Param('id') id: string): Hero {
const heroID = parseInt(id, 10);
const heroIndex = this.HEROES.findIndex(hero => hero.id === heroID);
const hero = this.HEROES[heroIndex];
this.HEROES.splice(heroIndex, 1);
return hero;
}
@Post()
createHero( @Body('name') name: string) {
const hero: Hero = {
name,
id: this.FAKE_ID,
};
this.HEROES.push(hero);
this.FAKE_ID = this.FAKE_ID + 1;
return hero;
}
@Put()
updateById( @Body('id') id: string, @Body('name') name: string): Hero {
const heroID = parseInt(id, 10);
const hero = this.HEROES.find(hero => hero.id === heroID);
if (hero) {
hero.name = name;
}
return hero;
}
Nest Decorators | Plain express object |
---|---|
@Request() | req |
@Response() | res |
@Next() | next |
@Session() | req.session |
@Param(param?: string | req.params / req.params[param] |
@Body(param?: string) | req.body / req.body[param] |
@Query(param?: string) | req.query / req.query[param] |
@Headers(param?: string) | req.headers / req.headers[param] |
import { Component } from '@nestjs/common';
import { Hero } from './interfaces/hero.interface';
@Component()
export class HeroesService {
private FAKE_ID = 21;
private readonly heroes: Hero[] = [];
findAll(): Hero[] {}
findById(heroID: number): Hero {}
findByName(name: string): Hero[] {}
updateById(heroID: number, name: string): Hero {}
deleteById(heroID: number): Hero {}
create(name: string): Hero {}
}
@Controller('heroes')
export class AppController {
constructor(private readonly heroesService: HeroesService) { }
@Get()
heroes(): Hero[] {
return this.heroesService.findAll();
}
@Get(':id')
findById( @Param('id') id: string): Hero {
const heroID = parseInt(id, 10); //Pipe
return this.heroesService.findById(heroID);
}
@Get('search/:name')
findByName( @Param('name') name: string): Hero[] {
return this.heroesService.findByName(name);
}
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { HeroesService } from './heroes.service';
@Module({
modules: [],
controllers: [AppController],
components: [
HeroesService,
],
})
export class ApplicationModule { }
components | the components that will be instantiated by the Nest injector and may be shared at least across this module. |
controllers | the set of controllers which have to be created |
modules | the list of imported modules that export the components which are necessary in this module |
exports | the subset of components that should be available in the other modules |
import { Module } from '@nestjs/common';
import { HeroesService } from './heroes.service';
import { HeroesController } from './heroes.controller';
@Module({
modules: [],
controllers: [
HeroesController,
],
components: [
HeroesService,
],
})
export class HeroesModule { }
import { Module } from '@nestjs/common';
import { HeroesModule } from './heroes/heroes.module';
@Module({
modules: [
HeroesModule,
],
controllers: [],
components: [],
})
export class ApplicationModule { }
import { Middleware, NestMiddleware } from '@nestjs/common';
import * as cors from 'cors';
@Middleware()
export class CorsMiddleware implements NestMiddleware {
public resolve(
options?: cors.CorsOptions,
): (req: any, res: any, next: any) => void {
return cors(options);
}
}
import { Middleware, NestMiddleware } from '@nestjs/common';
import { json } from 'express';
@Middleware()
export class JSONMiddleware implements NestMiddleware {
public resolve(): (req: any, res: any, next: any) => void {
return json();
}
}
const allRoutes = { method: RequestMethod.ALL, path: '*', };
export class ApplicationModule implements NestModule { public static corsOptions: CorsOptions | undefined = undefined; public static jsonOptions: any | undefined = undefined; configure(consumer: MiddlewaresConsumer): void { consumer.apply(CorsMiddleware) .with(ApplicationModule.corsOptions) .forRoutes(allRoutes) .apply(JSONMiddleware) .with(ApplicationModule.jsonOptions) .forRoutes(allRoutes); } }
findById(heroID: number): Hero {
const hero = this.heroes.find(hero => hero.id === heroID);
if (!hero) {
throw new HttpException('Hero not found', HttpStatus.NOT_FOUND);
}
return hero;
}
findByName(name: string): Hero[] {
const heroes = this.heroes.filter(hero => hero.name.includes(name));
if (!heroes.length) {
throw new NotFoundException('Heroes not found');
}
return heroes;
}
import { NotFoundException } from '@nestjs/common';
export class NotHeroException extends NotFoundException {
constructor(heroID: number) {
const msg = `The Hero: ID - ${heroID} not found`;
super(msg);
}
}
import { ForbiddenException } from '@nestjs/common';
import { Hero } from '../../heroes/interfaces/hero.interface';
export class RepeatHeroException extends ForbiddenException {
constructor(hero: Hero) {
const msg = `The Hero: ${hero.id} - ${hero.name} is repeated`;
super(msg);
}
}
deleteById(heroID: number): Hero {
const heroIndex = this.heroes.findIndex(hero => hero.id === heroID);
if (heroIndex === -1) {
throw new NotHeroException(heroID);
}
const hero = this.heroes[heroIndex];
this.heroes.splice(heroIndex, 1);
return hero;
}
const heroFound = this.heroes.find(hero => hero.name === name);
if (heroFound) {
throw new RepeatHeroException(heroFound);
}
...
import { PipeTransform, Pipe, ArgumentMetadata, HttpStatus } from '@nestjs/common';
import { NotHeroException } from '../exceptions/not-hero.exception';
@Pipe()
export class ParseIntPipe implements PipeTransform<string> {
async transform(value: string, metadata: ArgumentMetadata) {
const val = parseInt(value, 10);
if (isNaN(val)) {
throw new NotHeroException(val);
}
return val;
}
}
@Get(':id')
findById( @Param('id', new ParseIntPipe()) id: number): Hero {
return this.heroesService.findById(id);
}
@Put()
updateById( @Body('id', new ParseIntPipe()) id: number,
@Body('name') name: string): Hero {
return this.heroesService.updateById(id, name);
}
@Delete(':id')
deleteById( @Param('id', new ParseIntPipe()) id: number): Hero {
return this.heroesService.deleteById(id);
}
import { Pipe, PipeTransform, ArgumentMetadata, BadRequestException, HttpStatus } from '@nestjs/common';
import { validate } from 'class-validator';
import { plainToClass } from 'class-transformer';
@Pipe()
export class ValidationHeroPipe implements PipeTransform<any> {
async transform(value, metadata: ArgumentMetadata) {
const { metatype } = metadata;
if (!metatype || !this.toValidate(metatype)) return value;
const object = plainToClass(metatype, value);
const errors = await validate(object);
if (errors.length > 0)
throw new BadRequestException('Validation failed');
return value;
}
private toValidate(metatype): boolean {
const types = [String, Boolean, Number, Array, Object];
return !types.find((type) => metatype === type);
}
}
import { IsString, MinLength, MaxLength } from 'class-validator';
export class HeroDto {
@MinLength(2)
@MaxLength(16)
@IsString()
readonly name: string;
}
@Post()
create( @Body('', new ValidationHeroPipe()) hero: HeroDto): Hero {
return this.heroesService.create(hero.name);
}
import { Guard, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
@Guard()
export class RolesGuard implements CanActivate {
constructor(private readonly reflector: Reflector) { }
canActivate(req, context: ExecutionContext): boolean {
const { parent, handler } = context;
const roles = this.reflector.get<string[]>('roles', handler);
if (!roles) {
return true;
}
req.user = { /* Fake user's rol */
rol: 'user',
};
const user = req.user;
const hasRole = () => !!roles.find((role) => user.rol === role);
return user && user.rol && hasRole();
}
}
@UseGuards(RolesGuard)
@Controller('heroes')
export class HeroesController {
...
@Get()
heroes(): Hero[] {}
@Get(':id')
findById( @Param('id', new ParseIntPipe()) id: number): Hero { }
@Roles('user')
@Get('search/:name')
findByName( @Param('name') name: string): Hero[] {}
@Roles('admin')
@Put()
updateById( @Body('id', new ParseIntPipe()) id: number, @Body('name') name: string): Hero {}
@Roles('admin')
@Delete(':id')
deleteById( @Param('id', new ParseIntPipe()) id: number): Hero {}
@Roles('admin')
@Post()
create( @Body('', new ValidationHeroPipe()) hero: HeroDto): Hero {}
npm install --save typeorm pg
import { createConnection } from 'typeorm';
export const databaseProviders = [
{
provide: 'DbConnectionToken',
useFactory: async () => await createConnection({
type: 'postgres',
host: 'localhost',
port: 5432,
username: 'root',
password: 'toor',
database: 'heroes',
entities: [
__dirname + '../**/*entity.ts',
],
synchronize: true,
}),
},
];
import { Module } from '@nestjs/common';
import { databaseProviders } from './database.providers';
@Module({
components: [...databaseProviders],
exports: [...databaseProviders],
})
export class DatabaseModule { }
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class Hero {
@PrimaryGeneratedColumn()
id: number;
@Column({ length: 16, unique: true })
name: string;
}
import { Module } from '@nestjs/common';
import { HeroesService } from './heroes.service';
import { HeroesController } from './heroes.controller';
import { DatabaseModule } from '../database/database.module';
import { heroesProviders } from './heroes.providers';
@Module({
modules: [
DatabaseModule,
],
controllers: [
HeroesController,
],
components: [
HeroesService,
...heroesProviders,
],
})
export class HeroesModule { }
@Component()
export class HeroesService {
constructor(
@Inject('HeroesRepositoryToken')
private readonly heroesRepository: Repository<Hero>,
) { }
async findAll(): Promise<Hero[]> {
return await this.heroesRepository.find();
}
async findById(heroID: number): Promise<Hero> {
const hero = await this.heroesRepository.findOneById(heroID);
if (!hero) throw new NotHeroException(heroID);
return hero;
}
async findByName(name: string): Promise<Hero[]> {
const [heroes, heroesCount] = await this.heroesRepository
.createQueryBuilder('heroes')
.select()
.where('heroes.name like %:name%', { name })
.getManyAndCount();
if (heroesCount === 0) {
throw new NotFoundException('Heroes not found');
}
return heroes as Hero[];
}
async updateById(heroID: number, name: string): Promise<Hero> {
const hero: Hero = {
name,
id: heroID,
};
await this.heroesRepository.updateById(heroID, hero);
return hero;
}
async deleteById(heroID: number): Promise<Hero> {
const hero = await this.findById(heroID);
await this.heroesRepository.deleteById(heroID);
return hero;
}
async create(name: string): Promise<Hero> {
try {
await this.heroesRepository.insert({ name });
} catch {
throw new RepeatHeroException({ name } as Hero);
}
return this.findOneByName(name);
}
findOneByName(name): PromiseHero<Hero> {
return this.heroesRepository.findOne({
name,
});
Nest has a special package
@nestjs/testing
, which gives a set of utilities to boost the testing process
beforeEach(async () => {
testingModule = await Test.createTestingModule({
components: [
HeroesService,
{
provide: "HeroesRepositoryToken",
useFactory: () => ({
find: jest.fn(),
findOneById: jest.fn(() => true),
}),
},
],
}).compile();
service = testingModule.get(HeroesService);
spyRepository = testingModule.get("HeroesRepositoryToken");
});
describe("#findById", () => {
it("should return an exception if no hero was found", async () => {
spyRepository.findOneById = jest.fn();
await expect(service.findById(1)).rejects.toBeInstanceOf(NotHeroException);
});
it("should not return an exception if a hero was found", () => {
service.findById(1);
expect(spyRepository.findOneById).toHaveBeenCalledTimes(1);
expect(spyRepository.findOneById).toHaveBeenCalledWith(1);
});
});
beforeEach(async () => {
testingModule = await Test.createTestingModule({
controllers: [HeroesController],
components: [
{
provide: HeroesService,
useFactory: () => ({
create: jest.fn(),
findAll: jest.fn(),
}),
},
],
}).compile();
controller = testingModule.get(HeroesController);
spyService = testingModule.get(HeroesService);
});
it("#heroes should call to findAll method of heroes service", () => {
controller.heroes();
expect(spyService.findAll).toHaveBeenCalledTimes(1);
});
it("#create should call to create method of heroes service", () => {
const heroMocked = { name: "Hero mocked!" } as HeroDto;
controller.create(heroMocked);
expect(spyService.create).toHaveBeenCalledTimes(1);
expect(spyService.create).toHaveBeenCalledWith(heroMocked.name);
});
The end to end testing is a great way to verify how the application works from beginning to end.
const server = express();
server.use(bodyParser.json());
beforeEach(async () => {
const testingModule = await Test.createTestingModule({
modules: [HeroesModule],
}).overrideComponent("DbConnectionToken")
.useValue(db)
.overrideGuard(RolesGuard)
.useValue({ canActivate: () => true })
.compile();
app = testingModule.createNestApplication(server);
agent = request(server);
await app.init();
});
it("/GET heroes should return an array of heroes", () => {
return agent
.get("/heroes")
.expect(200)
.expect(({ body: heroes }) => {
expect(heroes).toBeInstanceOf(Array);
});
});
it("/GET heroes/:id should return a hero", () => {
return agent
.get("/heroes/5")
.expect(200)
.expect(({ body: hero }) => {
expect(hero).not.toBeInstanceOf(Array);
});
});
By
Carlos Caballero
and
Antonio Villena