Bo Vandersteene
@reibo_bo
@reibo_bo
https://medium.com/@bo.vandersteene
https://github.com/bovandersteene
reibo.io
const express = require('express')
const app = express()
app.get('/', (req, res) => res.send('Hello World!'))
app.listen(3000, () => console.log('listening on port 3000!'))
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!';
}
}
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"
},
@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>
@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);
}
https://github.com/nestjs/typescript-starter.git
nest new project-name
nest g module movie
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);
}
}
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);
}
}
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 },
);
}
}
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);
}
@Post('admin')
@UseGuards(AdminGuard)
@UsePipes(new ValidationPipe())
saveAdmin(@Body() movie: MovieDto,
@User() user: UserEntity): Promise<MovieEntity> {
console.log(user);
return this.movieSaveService.save(movie);
}
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
},
);
}
}
@reibo_bo
http://slides.com/bovandersteene/nestjs