Nest
Bo Vandersteene
@reibo_bo
About me
@reibo_bo
https://medium.com/@bo.vandersteene
https://github.com/bovandersteene
reibo.io
Start a node application
Typescript
routing-controllers
Own database integration
Type-orm
Dependency injection?
Project structure?
nest
Nest is a framework for building efficient, scalable Node.js server-side applications. It uses modern JavaScript, is built with TypeScript (preserves compatibility with pure JavaScript) and combines elements of OOP (Object Oriented Programming), FP (Functional Programming), and FRP (Functional Reactive Programming).
Under the hood, Nest makes use of Express, allowing for easy use of the myriad third-party plugins which are available.
const express = require('express')
const app = express()
app.get('/', (req, res) => res.send('Hello World!'))
app.listen(3000, () => console.log('listening on port 3000!'))
Hello world in Express
Hello world in Nest
import { NestFactory } from '@nestjs/core';
import { ApplicationModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(ApplicationModule);
await app.listen(3000);
}
bootstrap();
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
@Module({
imports: [],
controllers: [AppController],
components: [],
})
export class AppModule {}
import { Get, Controller } from '@nestjs/common';
@Controller()
export class AppController {
@Get()
root(): string {
return 'Hello World!';
}
}
Basic application
Application module
Movie Module
Music Module
Book Module
Auth Module
...
Search
Module
Save
Module
...
@Module({
imports: [
MusicModule,
MovieModule,
BookModule,
AuthModule
],
controllers: [
AppController
],
components: []
})
export class ApplicationModule {}
Search module
Search controller
Search service
Movie api
Movie datastore
@Module({
controllers: [MovieSearchController],
components: [MovieSearchService],
})
export class MovieSearchModule {
}
@Component()
export class MovieSearchService {
searchForMovie(key: string, page: number): Observable<TheMovieDbEntity>{
return Observable.create(observer => {
const https = require('https');
const options = {
hostname: 'api.themoviedb.org',
path: `/3/search/movie?api_key=${api_key}&page=${page}&query=${key}`,
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
};
const req = https.request(options, (res: any) => {
res.setEncoding('utf8');
res.on('data', (body: any) => {
if (res.statusCode !== 200) {
observer.error('Search failed');
observer.complete();
} else {
observer.next(body);
observer.complete();
}
});
});
req.on('error', (e: any) => {
observer.error('Search failed');
observer.complete();
});
req.write('');
req.end();
});
}
}
@Controller('/movie/search')
export class MovieSearchController {
constructor(private readonly movieSearchService: MovieSearchService) {
}
@Get()
searchFor(@Query('page') page: number = 1,
@Query('q') key: string): Observable<any> {
return this.movieSearchService.searchForMovie(key, page);
}
}
http://localhost:3001/movie/search?q=simpson&page=3
{
"page": 1,
"total_results": 45,
"total_pages": 3,
"results": [
{
"vote_count": 3118,
"id": 35,
"video": false,
"vote_average": 6.9,
"title": "The Simpsons Movie",
"popularity": 26.831717,
"poster_path": "/eCytnEriVur3rT47NWfkgPXD9qs.jpg",
"original_language": "en",
"original_title": "The Simpsons Movie",
"genre_ids": [
16,
35,
10751
],
"backdrop_path": "/gMjtdTP6HIi7CDilqXwnX8vouxO.jpg",
"adult": false,
"overview": "After Homer accidentally pollutes the town's water supply, Springfield is encased in a gigantic dome by the EPA and the Simpsons are declared fugitives.",
"release_date": "2007-07-25"
},
{
"vote_count": 102,
"id": 116440,
"video": false,
"vote_average": 6.8,
"title": "Maggie Simpson in The Longest Daycare",
"popularity": 6.654532,
"poster_path": "/tTlHQ8XzvAkXn5Liptu822rVUvO.jpg",
"original_language": "en",
"original_title": "Maggie Simpson in The Longest Daycare",
"genre_ids": [
16,
35
],
"backdrop_path": "/iRq40yPgSkEYrNaD1rOWMhChe07.jpg",
"adult": false,
"overview": "Maggie Simpson returns to the Ayn Rand School for Tots. There, Maggie's future prospects are deemed to be minimal, so she is left in one of the daycare's less enjoyable rooms, with just a butterfly for company.",
"release_date": "2012-07-12"
},
We don't need all this data
@Interceptor()
export class MovieSearchInterceptor implements NestInterceptor {
intercept(dataOrRequest, context: ExecutionContext,
stream$: Observable<TheMovieDbEntity>)
: Observable<MovieSearchRespone> {
return stream$.pipe(
map(response => ({
...response,
results: response.results.map(movie => this.mapMovie(movie)),
})),
);
}
private mapMovie(movie: TmbMovie): MovieSearch {
const { id, title, poster_path, overview } = movie;
return { id, title, poster_path, overview };
}
}
@Get()
@UseInterceptors(MovieSearchInterceptor)
searchFor(...): Observable<MovieSearchResponse>
Not only for transform
@Interceptor()
export class LoggingInterceptor implements NestInterceptor {
intercept(dataOrRequest, context: ExecutionContext,
stream$: Observable<any>): Observable<any> {
console.log('Before...');
const now = Date.now();
return stream$.pipe(
tap(() => console.log(`After... ${Date.now() - now}ms`)),
);
}
}
async function bootstrap() {
const app = await NestFactory.create(ApplicationModule);
app.useGlobalInterceptors(new LoggingInterceptor());
await app.listen(3001);
}
unlimited possibilities
Setup
Clone & create manual files
https://github.com/nestjs/typescript-starter.git
@nest/cli
nest new project-name
nest g module movie
- controller
- exception
- guard
- interceptor
- middleware
- module
- pipe
- service
We want to save
const entities = [MovieEntity];
const local: MysqlConnectionOptions = {
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'xxx',
password: 'xxx',
database: 'rbo_collection',
entities,
synchronize: false,
entityPrefix: 'coll_',
logging: false,
};
@Module({
imports: [ TypeOrmModule.forRoot(local),
...
})
export class ApplicationModule {}
@Entity()
export class MovieEntity {
@PrimaryColumn()
id: number;
@Column()
title: string;
@Column()
poster_path: string;
@Column()
overview: string;
}
Save module
Save controller
Save service
Movie datastore
@Component()
export class MovieStoreService {
constructor(@InjectRepository(MovieEntity)
private readonly movieRepsitory: Repository<MovieEntity>) {
}
async saveMovie(movie: MovieEntity): Promise<MovieEntity> {
return await this.movieRepsitory.save(movie);
}
}
@Module({
imports: [TypeOrmModule.forFeature([MovieEntity])],
controllers: [
MovieSaveController,
],
components: [
MovieStoreService,
MovieSaveService,
],
})
export class MovieSaveModule {
}
@Controller('/movie')
export class MovieSaveController {
constructor(private readonly movieSaveService: MovieSaveService) {
}
@Post()
@UsePipes(new ValidationPipe())
searchFor(@Body() movie: MovieDto): Promise<MovieEntity> {
return this.movieSaveService.save(movie);
}
}
Pipes & body ???
export class MovieDto {
@IsInt()
readonly id: number;
@IsString()
readonly title: string;
@IsString()
@IsOptional()
readonly poster_path: string;
@IsString()
@IsOptional()
readonly overview: string;
}
@Pipe()
export class ValidationPipe implements PipeTransform<MovieEntity> {
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 of movie failed');
}
return value;
}
private toValidate(metatype): boolean {
const types = [String, Boolean, Number, Array, Object];
return !types.find((type) => metatype === type);
}
}
Pipes are global or on a controller
Everyone can access the app & overwrite ?? !!
Security is needed!
Device
Passport
Secure route
Login api
JWT
API + JWT
const adminUser: UserEntity = {
id: 1,
firstname: 'Bo',
lastname: 'admin',
admin: true,
};
const normalUser: UserEntity = {
id: 1,
firstname: 'Bo',
lastname: 'Vandersteene',
admin: false,
};
@Component()
export class AuthService {
async createToken(login: string, password: string) {
const expiresIn = 60 * 60, secretOrKey = 'secret';
const user = login === 'admin' ? adminUser : normalUser;
const token = jwt.sign(user, secretOrKey, { expiresIn });
return {
expires_in: expiresIn,
access_token: token,
};
}
async validateUser(signedUser): Promise<boolean> {
// put some validation logic here
// for example query user by id / email / username
return true;
}
}
@Component()
export class JwtStrategy extends Strategy {
constructor(private readonly authService: AuthService) {
super(
{
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
passReqToCallback: true,
secretOrKey: 'secret',
},
async (req, payload, next) => await this.verify(req, payload, next),
);
passport.use(this);
}
public async verify(req, payload, done) {
const isValid = await this.authService.validateUser(payload);
if (!isValid) {
return done('Unauthorized', false);
}
done(null, payload);
}
}
@Controller('/auth')
export class AuthController {
constructor(private readonly authService: AuthService) {
}
@Post()
@HttpCode(HttpStatus.OK)
async getToken(@Req() request,
@Headers('authorization') token: string,
@Headers('user-agent') browser: string):
Promise<{ expires_in: number, access_token: string }> {
const b64auth = (token || '').split(' ')[1] || '';
const [login, password] = new Buffer(b64auth, 'base64').toString().split(':');
return this.authService.createToken(login, password);
}
}
@Module({
components: [
AuthService,
JwtStrategy,
],
controllers: [
AuthController,
],
})
export class AuthModule implements NestModule {
configure(consumer: MiddlewaresConsumer) {
consumer
.apply(passport.authenticate('jwt', { session: false }))
.forRoutes(
{ path: 'movie/*', method: RequestMethod.POST },
);
}
}
Guards
import { Guard, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs/Observable';
@Guard()
export class AdminGuard implements CanActivate {
canActivate(dataOrRequest, context: ExecutionContext):
boolean | Promise<boolean> | Observable<boolean> {
return dataOrRequest.user && dataOrRequest.user.admin;
}
}
@Post('admin')
@UseGuards(AdminGuard)
@UsePipes(new ValidationPipe())
saveAdmin(@Body() movie: MovieDto): Promise<MovieEntity> {
return this.movieSaveService.save(movie);
}
Use user object!
@Post('admin')
@UseGuards(AdminGuard)
@UsePipes(new ValidationPipe())
saveAdmin(@Body() movie: MovieDto,
@User() user: UserEntity): Promise<MovieEntity> {
console.log(user);
return this.movieSaveService.save(movie);
}
Run your frontend and backend in one
const allowedExt = ['.js', '.ico', ...];
const resolvePath = (file: string) => path.resolve(`../dist/${file}`);
@Middleware()
export class FrontendMiddleware implements NestMiddleware {
resolve(...args: any[]): ExpressMiddleware {
return (req, res, next) => {
const { url } = req;
if (url.indexOf(ROUTE_PREFIX) === 1) {
// it starts with /api --> continue with execution
next();
} else if (allowedExt.filter(ext => url.indexOf(ext) > 0).length > 0) {
// it has a file extension --> resolve the file
res.sendFile(resolvePath(url));
} else {
// in all other cases, redirect to the index.html!
res.sendFile(resolvePath('index.html'));
}
};
}
}
export class ApplicationModule implements NestModule {
configure(consumer: MiddlewaresConsumer): void {
consumer.apply(FrontendMiddleware).forRoutes(
{
path: '/**', // For all routes
method: RequestMethod.ALL, // For all methods
},
);
}
}
What was not covered in this talk, but offered
Websockets
Microservices
CQRS
Swagger
Testing
And a lot more
Why use nest?
Same language frontend and backend
Fast and scalable
Application structure
Asynchronous model
High performance
Separation of concerns
@reibo_bo
http://slides.com/bovandersteene/nestjs
nestjs
By Bo Vandersteene
nestjs
- 618