giorgiosaints
5/4/2018 - 2:46 PM

Nodejs API Guide

Nodejs+Restify+MongoDB+TypeScript+Jest

Guia definitivo da API NODE

Instalação do Node.JS no Linux/Mac

Depenências:

  • Node.JS: Para quem quiser dar uma olhada na documentação
  • NVM: Controla e gerencia as versões do node
  • Nodemon: Para instalar, rode: sudo npm nodemon -g
  • TypeScript: Vamos utilizar o TypeScript no backend
  • Restify: Biblioteca que será utilizada na nossa API
  • MongoDB: Banco no-sql

Instalando o NVM

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

Instalando o Node.JS via 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

Instalando o TypeScript

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"
    ]
}

Vamos entender o código a cima:

  • 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

Automatizando as builds 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:

  • @types/node: Agora o TS vai reconhecer o require por exemplo
    • Rode npm i @types/node -D

Instalando e configurando o Restify

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:

Pronto tudo ok!

Instalando e configurando o MongoDB

...

Instalando e configurando o Mongoose

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

Estruturando a nossa API

Criando o arquivo api.ts

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
  });

Criando o arquivo enviroment.ts

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' }
}

Criando a classe Server

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);
      }
}

Criando a a classe Router

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()
        }
    }
}

Criando ...

Criando rotas para nossos recursos:

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()

Criando o Schema do Mongoose

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)