In this Post, we will cover
- create and build docker images
- create and setup Nest app
- configure Elasticsearch with Nest app
- create and setup Vue app
let's start
Project root directory structure:
.
├── client
├── server
└── dev.yml
1. Setup Nest and Elasticsearch
Server directory structure:
.
├── src
│ ├── config
│ │ ├── config.module.ts
│ │ └── config.service.ts
│ ├── modules
│ │ ├── movie
│ │ │ ├── movie.controller.ts
│ │ │ ├── movie.module.ts
│ │ │ └── movie.service.ts
│ │ └── search
│ │ ├── search.module.ts
│ │ └── search.service.ts
│ ├── app.controller.ts
│ ├── app.module.ts
│ ├── main.ts
│ └── typings.d.ts
├── Dockerfile
├── .env
├── movies.json
├── package.json
└── tslint.json
- ConfigModule and ConfigService
in the ConfigModule we have to import env variables in project
-
config.module.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersimport { Module } from '@nestjs/common'; import { ConfigService } from './config.service'; @Module({ providers: [ { provide: ConfigService, useValue: new ConfigService('.env'), }, ], exports: [ConfigService], }) export class ConfigModule {} -
config.service.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersimport * as dotenv from 'dotenv'; import * as fs from 'fs'; export class ConfigService { private readonly envConfig: { [key: string]: string }; constructor(filePath: string) { this.envConfig = dotenv.parse(fs.readFileSync(filePath)); } get(key: string): string { return this.envConfig[key]; } } SearchModule and SearchService
in the SearchModule we have to configure Elasticsearch and fill
movie-index
with data frommovies.json
movie-index
automatically created when you start a project.
-
search.module.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersimport { Module, OnModuleInit } from '@nestjs/common'; import { SearchService } from './search.service'; import { ElasticsearchModule } from '@nestjs/elasticsearch'; import { ConfigModule } from '../../config/config.module'; import { ConfigService } from '../../config/config.service'; @Module({ imports: [ ElasticsearchModule.registerAsync({ imports: [ConfigModule], useFactory: async (configService: ConfigService) => ({ node: configService.get('ELASTICSEARCH_NODE'), maxRetries: 10, requestTimeout: 60000, pingTimeout: 60000, sniffOnStart: true, }), inject: [ConfigService], }), ConfigModule, ], providers: [SearchService], exports: [SearchService], }) export class SearchModule implements OnModuleInit { constructor(private searchService: SearchService) {} onModuleInit() { this.searchService.createIndex().then(); } } -
search.service.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersimport { Injectable } from '@nestjs/common'; import { ElasticsearchService } from '@nestjs/elasticsearch'; import * as moviesJson from '../../../movies.json'; import { ConfigService } from '../../config/config.service'; interface MoviesJsonResponse { title: string; year: number; cast: string[]; genres: string[]; } @Injectable() export class SearchService { constructor(private readonly esService: ElasticsearchService, private readonly configService: ConfigService) {} async createIndex() { const checkIndex = await this.esService.indices.exists({ index: this.configService.get('ELASTICSEARCH_INDEX') }); if (checkIndex.statusCode === 404) { this.esService.indices.create( { index: this.configService.get('ELASTICSEARCH_INDEX'), body: { settings: { analysis: { analyzer: { autocomplete_analyzer: { tokenizer: 'autocomplete', filter: ['lowercase'], }, autocomplete_search_analyzer: { tokenizer: 'keyword', filter: ['lowercase'], }, }, tokenizer: { autocomplete: { type: 'edge_ngram', min_gram: 1, max_gram: 30, token_chars: ['letter', 'digit', 'whitespace'], }, }, }, }, mappings: { properties: { title: { type: 'text', fields: { complete: { type: 'text', analyzer: 'autocomplete_analyzer', search_analyzer: 'autocomplete_search_analyzer', }, }, }, year: { type: 'integer' }, genres: { type: 'nested' }, actors: { type: 'nested' }, }, }, }, }, (err) => { if (err) { console.error(err); } }, ); const body = await this.parseAndPrepareData(); this.esService.bulk( { index: this.configService.get('ELASTICSEARCH_INDEX'), body, }, (err) => { if (err) { console.error(err); } }, ); } } async search(search: string) { let results = []; const { body } = await this.esService.search({ index: this.configService.get('ELASTICSEARCH_INDEX'), body: { size: 12, query: { match: { 'title.complete': { query: search, }, }, }, }, }); const hits = body.hits.hits; hits.map(item => { results.push(item._source); }); return { results, total: body.hits.total.value }; } async parseAndPrepareData() { let body = []; const listMovies: MoviesJsonResponse[] = moviesJson; listMovies.map((item, index) => { let actorsData = []; item.cast.map(actor => { const splited = actor.split(' '); actorsData.push({ firstName: splited[0], lastName: splited[1] }); }); body.push( { index: { _index: this.configService.get('ELASTICSEARCH_INDEX'), _id: index } }, { title: item.title, year: item.year, genres: item.genres.map(genre => ({ genre })), actors: actorsData, }, ); }); return body; } } MovieModule, MovieService and MovieController
we create MovieController, MovieService and import SearchModule for access method
search
in SearchService
-
movie.module.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersimport { Module } from '@nestjs/common'; import { MovieService } from './movie.service'; import { MovieController } from './movie.controller'; import { SearchModule } from '../search/search.module'; @Module({ imports: [SearchModule], providers: [MovieService], controllers: [MovieController], }) export class MovieModule {} -
movie.controller.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersimport { Controller, Get, Query } from '@nestjs/common'; import { MovieService } from './movie.service'; @Controller() export class MovieController { constructor(public readonly movieService: MovieService) {} @Get('movies') async getMovies(@Query('search') search: string) { if (search !== undefined && search.length > 1) { return await this.movieService.findMovies(search); } } } -
movie.service.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersimport { Injectable } from '@nestjs/common'; import { SearchService } from '../search/search.service'; @Injectable() export class MovieService { constructor(readonly esService: SearchService) {} async findMovies(search: string = '') { return await this.esService.search(search); } } AppModule
in this step we will create healthcheck endpoint in AppController for docker health.
we have to import MovieModule, ConfigModule and SearchModule into AppModule
-
app.module.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersimport { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { ConfigModule } from './config/config.module'; import { SearchModule } from './modules/search/search.module'; import { MovieModule } from './modules/movie/movie.module'; @Module({ imports: [ConfigModule, SearchModule, MovieModule], controllers: [AppController], }) export class AppModule {} -
app.controller.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersimport { Controller, Get } from '@nestjs/common'; @Controller() export class AppController { private start: number; constructor() { this.start = Date.now(); } @Get('healthcheck') async healthcheck() { const now = Date.now(); return { status: 'API Online', uptime: Number((now - this.start) / 1000).toFixed(0), }; } } -
main.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersimport { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { ConfigService } from './config/config.service'; async function bootstrap() { const configService = new ConfigService('.env'); const app = await NestFactory.create(AppModule); app.enableCors(); app.setGlobalPrefix(configService.get('GLOBAL_PREFIX')); await app.listen(configService.get('NODE_PORT')); } bootstrap(); create server/Dockerfile
docker image for Nest app
FROM node:13
WORKDIR /app/server
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
EXPOSE 3000/tcp
CMD [ "node", "dist/main.js" ]
Create .env file in server directory
#App
APP_ENV=dev
GLOBAL_PREFIX=api
#Elastic
ELASTICSEARCH_NODE=http://dockerip:9200
NODE_PORT=3000
ELASTICSEARCH_INDEX=movies-index
how to find elastic url in docker:
docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' nest-elasticsearch
Replace the given dockerIp with
ELASTICSEARCH_NODE=http://dockerIp:9200
in .env
2. Setup Vue app
- Client directory structure:
.
├── Dockerfile
├── package.json
├── public
│ └── index.html
└── src
├── App.vue
├── asset
├── components
│ └── Home.vue
├── main.js
├── plugins
│ └── boostrap-vue.js
└── router.js
App.vue
<router-view>
The component is a functional component that renders the matched component for the given path. Components rendered in<router-view>
can also contain their own<router-view>
, which will render components for nested paths.
<template> | |
<div id="app"> | |
<router-view/> | |
</div> | |
</template> |
Home.vue
<template> | |
<b-container> | |
<br> | |
<b-list-group> | |
<h2 align="center">Search Movies by title</h2> | |
<b-input-group-text> | |
<b-input label="Search" type="text" v-model="search" placeholder="Search in 91,770 movies" /> | |
<span class="input-group-text">Total: {{total}}</span> | |
</b-input-group-text> | |
<br> | |
</b-list-group> | |
<br> | |
<b-row> | |
<b-card-group columns> | |
<b-col v-for="movie in movies" v-bind:key="movie.id"> | |
<b-card :bg-variant="movie.variant" text-variant="black" :header="movie.title"> | |
<b-card-body> | |
<b-card-text >Year: {{ movie.year}}</b-card-text> | |
<b-card-text>Actors:</b-card-text> | |
<b-card-text v-for="actor in movie.actors" v-bind:key="actor"> | |
<li>{{ actor.firstName }} {{ actor.lastName }}</li> | |
</b-card-text> | |
<b-card-footer > | |
<span v-for="genre in movie.genres" v-bind:key="genre"> | |
<span> #{{ genre.genre }} </span> | |
</span> | |
</b-card-footer> | |
</b-card-body> | |
</b-card> | |
</b-col> | |
</b-card-group> | |
</b-row> | |
</b-container> | |
</template> | |
<script> | |
import axios from "axios"; | |
export default { | |
name: 'AutocompletePage', | |
data() { | |
return { | |
movies: [], | |
search: '', | |
total: 0, | |
}; | |
}, | |
watch:{ | |
search() { | |
return this.getSearch(); | |
} | |
}, | |
methods: { | |
getSearch() { | |
axios.get(`http://localhost:3000/api/movies?search=` + this.search) | |
.then(response => { | |
this.movies = response.data.results; | |
this.total = response.data.total; | |
}) | |
} | |
}, | |
}; | |
</script> | |
<style> | |
.col { | |
min-width: 360px; | |
} | |
</style> |
boostrap-vue.js
setup boostrap with vue
import Vue from "vue"; | |
import BootstrapVue from "bootstrap-vue"; | |
import "bootstrap/dist/css/bootstrap.min.css"; | |
import "bootstrap-vue/dist/bootstrap-vue.css"; | |
Vue.use(BootstrapVue); |
main.js
import "@babel/polyfill"; | |
import Vue from "vue"; | |
import "./plugins/boostrap-vue"; | |
import App from "./App.vue"; | |
import router from "./router"; | |
Vue.config.productionTip = false; | |
new Vue({ | |
router, | |
render: h => h(App) | |
}).$mount("#app"); |
-
router.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersimport Vue from 'vue' import Router from 'vue-router' import Home from "@/components/Home"; Vue.use(Router) export default new Router({ mode: 'history', routes: [ { path: '/', redirect: { name: 'home' } }, { path: '/home', name: 'home', component: Home }, ] }); -
create client/Dockerfile
docker for Vue app
FROM node:11.1-alpine as develop-stage
WORKDIR /app/client
COPY package*.json ./
RUN npm install
COPY . .
FROM develop-stage as build-stage
RUN npm run build
COPY --from=build-stage /app/client/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
3. Setup Docker for project
why use Docker?
Docker is a tool designed to make it easy to create, deploy and run application by using containers.
create dev.yml
version: "3.7" | |
services: | |
nest-app: | |
build: server | |
container_name: nest-app | |
healthcheck: | |
test: ["CMD-SHELL", "curl --silent --fail localhost:3000/api/healthcheck || exit 1"] | |
interval: 50s | |
timeout: 30s | |
retries: 5 | |
depends_on: | |
- nest-elasticsearch | |
command: "npm run start:dev" | |
volumes: | |
- ./server/src:/app/server/src/ | |
ports: | |
- 3000:3000 | |
nest-elasticsearch: | |
container_name: nest-elasticsearch | |
image: docker.elastic.co/elasticsearch/elasticsearch:7.0.1 | |
healthcheck: | |
test: ["CMD-SHELL", "curl --silent --fail localhost:9200/_cat/health?h=st || exit 1"] | |
interval: 50s | |
timeout: 30s | |
retries: 5 | |
environment: | |
- cluster.name=movies-cluster | |
- bootstrap.memory_lock=true | |
- "ES_JAVA_OPTS=-Xms512m -Xmx512m" | |
- discovery.type=single-node | |
ports: | |
- 9300:9300 | |
- 9200:9200 | |
nest-vue: | |
build: | |
target: 'develop-stage' | |
context: client | |
container_name: nest-vue | |
volumes: | |
- ./client:/app/client | |
ports: | |
- 8080:8080 | |
command: /bin/sh -c "npm run serve" |
Check out the full project on GitHub
That's it!
Feel free to ask questions, make comments or suggestions, or just say hello in the comments below.
Top comments (4)
Hi, thanks for the tutorial
I'm following along but i get a connection timeout error when creating the index. I created an index with a
curl
command and it seems that theindices.exists(...)
is working fine. Can you point me towards a solution?(I can provide more info if needed)
how to get the docker IP?
Service name (
nest-elasticsearch
in above case) is used when you use docker-compose for ochestrating everything. Otherwise, bind the port to the host and uselocalhost
. For example:Then you can access the elasticsearch node using
http://localhost:9200
github.com/kop7/nest-elasticsearch...