Nodejs+Restify+MongoDB+TypeScript+Jest
Depenências:
sudo npm nodemon -g
A primeira coisa que iremos fazer é criar o arquivo .bash_profile
na nossa home:
touch ~/.bash_profile
O proximo passo agora é instalar o NVM
curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.11/install.sh | bash
# Depois rode:
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
Pronto agora vamos conferir se á instalação foi bem sucedida rodando command -v nvm
Após a instalação do NVM agora iremos instalar o node apenas rodando nvm install node
, isso irá instalar a versão mais recente do node, caso você queira instalar outras versões é simples:
# Instalando outras versões do node
nvm install 8 # irá instalar a versão 8
# Checar as versões instaladas pelo nvm
nvm ls
# Setar a versão default
nvm use 10 # Versão que eu quero
Para instalar o TypeScript na nossa aplicação, temos que criar o nosso projeto. Vamos criar um projeto node rodando npm init -y
.
Dentro da pasta do projeto crie um arquivo chamado tsconfig.json
, abra o seu editor de preferência e cole o código a baixo dentro do arquivo tsconfig.
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"outDir": "dist"
},
"include": [
"src/**/*.ts"
],
"exclude": [
"node_modules"
]
}
Em compilerOptions: nós estamos passando para o TypeScript utilizar o ES2015 e o CommonJS como output, esse e o mesmo modulo que o Node utiliza.
Em Include nós estamos passando onde ele deve buscar os nossos arquivos .ts e em Exclude onde ele não deve buscar, assim nós garantimos o escopo para ele trabalhar e garantimos que ele não irá fazer um transpile de arquivos indesejados.
Seguindo o nosso arquivo acima, agora precisamos criar o nosso diretório src, como dito antes, será nele que iremos criar os nosso arquivos .ts. Feito isso, precisamos iniciar o nosso projeto com o comando npm init -y, esse arquivo irá criar um novo arquivo chamado package.json para o nosso projeto. Até esse momento nós temos a estrutura a baixo:
Agora vamos baixar o pacote do TypeScript. Para isso, execute o comando a baixo dentro no cmd dentro do caminho do seu projeto:
# Primeiro temos que instalar o TypeScript globalmente
sudo npm install typescript -g # Instalando o typescript globalmente
# Instalando o TypeScript no projeto
npm install typescript@latest -D
# Dentro do projeto rode
tsc --init # Inicia uma aplicação com typescript
A primeira parte do nosso código está OK ;) Mas não seria melhor ter algo que em todas as nossas alterações já criasse os nossos arquivos ou atualizasse eles sem a necessidade de estarmos rodando o comando acima? Se a sua resposta for sim ;) Podemos utilizar o Gulp para isso, ele irá nos auxiliar nessa task, de um modo automático, nós somente iremos precisar baixar ele e configurar uma vez. Para isso, execute o comando a baixo na sua console:
npm install gulp@3.9.1 gulp-typescript@3.1.1 -D -E
Pronto a partir de agora nós podemos criar arquivos .ts
Agora iremos precisar criar o nosso arquivo responsável por automatizar essas builds. Para isso, crie um arquivo chamado gulpfile.js na raiz do seu projeto e cole o código a baixo dentro dele:
const gulp = require('gulp');
const ts = require('gulp-typescript');
const JSON_FILES = ['src/*.json', 'src/**/*.json'];
const tsProject = ts.createProject('tsconfig.json');
gulp.task('scripts', () => {
const tsResult = tsProject.src()
.pipe(tsProject());
return tsResult.js.pipe(gulp.dest('dist'));
});
gulp.task('watch', ['scripts'], () => {
gulp.watch('src/**/*.ts', ['scripts']);
});
gulp.task('assets', function() {
return gulp.src(JSON_FILES)
.pipe(gulp.dest('dist'));
});
gulp.task('default', ['watch', 'assets']);
Eu não irei entrar em detalhes do código acima por ele ser padrão, mas em resumo ele está pegando as configurações em tsProject do nosso arquivo tsconfig e depois indo no diretório src da mesma forma que estávamos trabalhando antes, a novidade é a ultima linha do nosso arquivo que irá ficar monitorando o nosso diretório a procura de novos arquivos ou alterações. Para testar delete o diretório dist que nós criamos no teste anterior, em seguida execute o comando gulp no seu terminal.
Agora é só setar no package.json
a pasta dist (que foi criada pela build) assim:
....
"scripts": {
"start": "nodemon dist/api",
...
}
...
Agora para levantar a aplicação é só rodar: npm start
Agora temos que configurar as definições de tipo:
require
por exemplo
npm i @types/node -D
O Restify é um biblioteca específica para Node.JS que vai ajudar a gente a criar a nossa API, ou seja, ele vai dar subsídios para a gente ficar ouvindo requisições HTTP e efetuando as respostas. Com tudo instalado, vamos instalar o Resify:
npm i restify@latest -P
Agora temos que configurar as definições de tipo novamente:
npm i @types/restify -D
Pronto tudo ok!
...
Para instalar o mongoose é simples, rode: npm i mongoose@latest -P
, e como a gente utiliza TypeScript na nossa instalação,
nós precisamos instalar as definições de tipo do TypeScript npm i @types/mongoose@latest -D
Esse arquivo será o nosso arquivo main da API, é nela onde a aplicação será startada:
import Server from './server/server'
import { usersRouter } from './resources/users/users.router'
const server = new Server() // Recebendo uma instancia do Server para utilizarmos seus métodos
server
.bootstrap([usersRouter]) // Passando um array de rotas para serem startadas e recebendo a instância do servidor configurado
.then(server => {
console.log("====================================");
console.log("Server is listening on: ", server.application.address());
console.log("====================================");
})
.catch(error => {
console.log("Server failed to start");
console.error(error);
process.exit(1); // Derruba o processo, indica que a saída foi anormal
});
Responsável pelas nossas variáveis de ambiente :)
// common/environment
export const environment = {
server: { port: process.env.SERVER_PORT || 3000 },
db: { url: process.env.DB_URL || 'mongodb://localhost/node-api' }
}
Vamos organizar o nosso código criando uma classe server.ts, ela vai encapsular a maior parte da lógica.
Crie uma pasta server/
e dentro dela crie o arquivo server.ts
:
// server/server.ts
import * as restify from 'restify'
import * as mongoose from 'mongoose'
import { mergePatchBodyParser } from './merge-patch.parser'
import { environment } from '../common/environment'
import { Router } from '../common/router'
export default class Server {
application: restify.Server // Instância do restify
// Inicia e conecta com o mongo
initializeDb(): mongoose.MongooseThenable {
(<any>mongoose).Promise = global.Promise // Corrige o 'DeprecationWarning' do mongoose
return mongoose.connect(environment.db.url, {
useMongoClient: true // Nova forma de receber as conexões do client (Mais segura)
})
}
// Retorna uma Promise que vai inicializar o servidor e encadear nossas rotas
initRoutes(routers: Router[]): Promise<any> {
// A configuração do restify não é baseado em Promises, então iremos envolver em uma :)
return new Promise((resolve, reject) => {
try {
// Primeiro passo: Criação do servidor
this.application = restify.createServer({
name: "node-api",
version: "1.0.0"
})
// Segundo passo: Instalação dos plugins
// Middlewares
this.application.use(restify.plugins.queryParser())
this.application.use(restify.plugins.bodyParser())
this.application.use(mergePatchBodyParser)
// Terceiro passo: Nossas rotas
// For que irá interar as rotas aplicar as rotas em nosso servidor
for(let router of routers) {
router.applyRoutes(this.application) // Método
}
// Quarto passo: O nosso listen setando a porta
this.application.listen(environment.server.port, () => {
resolve(this.application) // Resolvendo a promise retornando a nossa instância devidamente configurada
})
// Se tiver algum erro na configuração o código vai cair aqui
} catch (error) {
reject(error); // E automaticamente nós iremos retornar um reject na nossa Promise, derrumando o nosso servidor que é o desejável
}
});
}
// Retorna uma Promise que vai retornar a própria (uma instância) classe Server de forma que ela vai estar configurada
bootstrap(routers: Router[] = []): Promise<Server> {
return this.initializeDb()
.then(() => this.initRoutes(routers))
.then(() => this);
}
}
Crie o arquivo router.ts
dentro da pasta common. A classe abstrata apenas terá um método abstrato, que não terá implementação.
import * as restify from 'restify'
import { EventEmitter } from 'events';
export abstract class Router extends EventEmitter{
// Vamos receber uma instância da aplicação restify, do nosso servidor restify
abstract applyRoutes(application: restify.Server)
// Método que irá centralizar as renderizações do nosso recurso
render(response: restify.Response, next: restify.Next) {
return (document) => {
if(document){
this.emit('beforeRender', document)
response.json(document)
} else {
response.send(404)
}
return next()
}
}
}
Crie uma pasta resources/
e dentro dela é onde ficará os nosso recursos da nossa api. Nesse exemplo iremos criar um recurso
users
para mostrar a estrutura padrão. Crie outra pasta users/
e crie o arquivo users.router.ts
:
import * as restify from 'restify'
import { Router } from '../../common/router'
import { User } from './users.model'
class UsersRouter extends Router {
// Responsável por ouvir o evento dentro do render
constructor() {
super()
// Evento que irá esconder o password quando chamarmos o método render()
this.on('beforeRender', document => {
document.password = undefined
})
}
// Sobrescrevendo o método abstrato da classe abstrata Router e aqui é onde iremos setar as nossas rotas
applyRoutes(application: restify.Server) {
// Listar
application.get('/users', (req, res, next) => {
User.find().then(this.render(res, next)) // Chamando o método render para renderizar o json
})
// Mostrar
application.get('/users/:id', (req, res, next) => {
User.findById(req.params.id).then(this.render(res, next)) // Chamando o método render para renderizar o json
})
// Criar
application.post('/users', (req, res, next) => {
let user = new User(req.body)
user.save().then(this.render(res, next)); // Chamando o método render para renderizar o json
})
// Atualizar
application.put('/users/:id', (req, res, next) => {
const options = {overwrite: true}
User.update({ _id: req.params.id }, req.body, options)
.exec().then(result => {
if(result.n){
return User.findById(req.params.id)
} else {
res.send(404)
}
}).then(this.render(res, next)) // Chamando o método render para renderizar o json
})
// Editar
application.patch('/users/:id', (req, res, next) => {
const options = {new: true}
User.findByIdAndUpdate(req.params.id, req.body, options).then(this.render(res, next)); // Chamando o método render para renderizar o json
})
// Deletar
application.del('/users/:id', (req, res, next) => {
User.remove({_id: req.params.id}).exec().then((cmdResult: any) => {
if(cmdResult.result.n){
res.send(204)
} else {
res.send(404)
}
return next()
})
})
}
}
export const usersRouter = new UsersRouter()
import * as mongoose from 'mongoose'
// Interface que representa o model User
export interface User extends mongoose.Document {
name: string,
email: string,
password: string
}
const userSchema = new mongoose.Schema({
name: {
type: String
},
email: {
type: String,
unique: true
},
password: {
type: String,
select: false
}
})
export const User = mongoose.model<User>('User', userSchema)