Fala galera, depois de um primeiro artigo bem teórico, dessa vez iremos colocar a mão na massa. Mas antes, é necessário informar que os códigos a seguir já estão adaptados para a versão final do projeto, isso é, todos os exemplos já serão utilizando Jest e ES6 Module, além de algumas correções que encontrei pelo caminho.
Configurando Docker
Inicialmente você precisa ter configurado tanto o docker quanto o docker compose em sua máquina, até mesmo para termos um padrão no ambiente de desenvolvimento.
Com a máquina já configurada, crie um diretório qualquer, no meu caso foi Two-Factor-Authentication e dentro dele vamos criar os arquivos Dockerfile, sem extensão, e o arquivo docker-compose.yml.
No arquivo Dockerfile, deixaremos ele assim:
FROM node:lts
RUN mkdir /app
WORKDIR /app
COPY --chown=node:node . .
CMD /bin/sh
No arquivo docker-compose.yml, deixaremos ele assim:
version: '3.9'
services:
two_factor_authentication:
container_name: authentication_project
build: .
volumes:
- .:/app:rw
ports:
- "${APP_PORT}:${APP_PORT}"
restart: "no"
command: sh
stdin_open: true
tty: true
environment:
- CHOKIDAR_USEPOLLING=true
depends_on:
- db_two_factor
networks:
- pg-network
adminer:
image: adminer
container_name: adminer_sgbd_two_factor_authentication
ports:
- "8081:8080"
depends_on:
- db_two_factor
restart: always
networks:
- pg-network
db_two_factor:
image: postgres:latest
container_name: db_two_factor_authentication
ports:
- 5432:5432
environment:
- POSTGRES_USER=${DB_USER}
- POSTGRES_PASSWORD=${DB_PASSWORD}
- POSTGRES_DB=${DB_DATABASE}
- POSTGRES_HOST_AUTH_METHOD=trust
networks:
- pg-network
volumes:
- dba:/var/lib/postgresql
volumes:
dba:
networks:
pg-network:
driver: bridge
Só para esclarecer o que estamos fazendo, iremos utilizar a última versão do node lts, usaremos a última versão do postgres, para armazenar os dados, e utilizaremos o adminer, que é uma ferramenta visual para auxiliar no gerenciamento do banco de dados.
Além disso, algumas informações da nossa configuração acima são dinâmicas, como por exemplo as informações de conexão com o banco e a porta da aplicação, para isso, vamos criar um arquivo de configuração chamado .env, com o seguinte conteúdo;
APP_PORT=8001
DB_DIALECT=postgres
DB_HOST=db_two_factor
DB_DATABASE=two_factors
DB_USER=root
DB_PASSWORD=root
Esse arquivo será bem útil posteriormente.
Agora iremos subir todos os nossos serviços. Acesse o terminal e rode o comando abaixo:
docker-compose up --buid
Ou se quiser deixar os serviços rodando em segundo plano, basta rodar o comando abaixo:
docker compose up -d --build
Agora que estamos com nossos serviços rodando, vamos entrar no serviço onde está o node para começarmos a construir nossa aplicação, no terminal, rode o comando abaixo:
docker exec -it authentication_project bash
authentication_project é o nome do meu serviço node, para verificar o seu, basta rodar o comando docker ps.
Iniciando estrutura do projeto
Depois de entrarmos no serviço do node, vamos rodar o seguinte comando para de fato iniciarmos o projeto:
npm init -y
Um arquivo chamado package.json vai ser criado, ele será bem útil, por enquanto o deixaremos de lado. Em relação a organização do projeto, deixei claro que me baseei na arquitetura hexagonal (falarei um pouco mais posteriormente), então vamos criar diretórios que vão facilitar o entendimento do projeto, minha estrutura ficou como o da imagem abaixo:
- src/domain: onde ficará toda a nossa regra de negócio;
- src/infra: onde ficará todas as implementações das dependências do nosso projeto, irá encapsular os recursos de libs, por exemplo;
- tests: onde ficará nossos testes.
Fluxo do projeto
O fluxo do projeto será dividido em duas partes: Primeiro vamos construir a lógica de login "tradicional", com e-mail e senha, depois vamos construir a lógica de logar com o token.
E pensando na primeira lógica do sistema, a gente vai ter uma entrada de e-mail e senha, vamos precisar buscar as informações do usuário pelo e-mail, validar sua senha, gerar um token e mandar por e-mail. Nesta breve descrição, podemos perceber que teremos uma comunicação com o banco de dados, uma validação de senha, comunicação com o serviço de e-mail e geração de token, ou seja, temos 4 dependências.
Vamos primeiramente definir essas "interfaces", lembrando que JS não possui o recurso de interface como conhecemos em outras linguagens, então dentro de src/domain, vamos criar alguns arquivos:
// src/domain/iemail.js
export default class IEmail {
send(user) {
throw new Error('Method must be implemented!');
}
}
// src/domain/irepository.js
export default class IRepository {
findByEmail(email) {
throw Error('Method must be implemented!');
}
update(user) {
throw Error('Method must be implemented!');
}
}
// src/domain/ipassword-hash.js
export default class IPasswordHash {
compare(password, hash) {
throw Error('Method must be implemented!');
}
}
// src/domain/igenerate-token.js
export default class IGenerateToken {
getToken() {
throw Error('Method must be implemented!');
}
getEmailToken() {
throw Error('Method must be implemented!');
}
}
Basicamente definimos nossas "interfaces" de comunicação. Um detalhe muito importante fica por conta da classe IGenerateToken, contendo 2 métodos de geração de token, isso porque pensei na seguinte lógica: quando o usuário logar, ele vai receber por e-mail um token, porém também vai receber um outro token como resposta da requisição (que será armazenada temporariamente pelo cliente), quando o usuário for fazer a segunda autenticação, ele vai precisar mandar os 2 tokens, aqui basicamente é uma forma de aumentar a segurança.
Outra coisa que vamos encapsular, é em relação ao erros, eu basicamente criei 3 classes de erros, isso vai ser útil mais na frente:
// src/domain/domain-error.js
export default class DomainError extends Error {
constructor(message) {
super(message);
}
}
// src/domain/gateway-error.js
export default class GatewayError extends Error {
constructor(message) {
super(message);
}
}
// invalid-argument-error.js
export default class InvalidArgumentError extends Error {
constructor(message) {
super(message);
}
}
Também vamos criar uma ação para trabalhar com esses erros:
// throw-error.js
const throwError = (error, message) => {
throw new error(message);
};
export default throwError;
Agora vamos construir a classe de login:
// src/domain/user-authentication.js
import IRepository from './irepository.js';
import IEmail from './iemail.js';
import IPasswordHash from './ipassword-hash.js';
import IGenerateToken from './igenerate-token.js';
import DomainError from './domain-error.js';
import GatewayError from './gateway-error.js';
import InvalidArgumentError from './invalid-argument-error.js';
import throwError from './throw-error.js';
export default class UserAuthentication {
#repository;
#emailGateway;
#hashService;
#tokenService;
constructor(
repository,
emailGateway,
passwordHash,
tokenService
) {
this.#repository = repository;
this.#emailGateway = emailGateway;
this.#hashService = passwordHash;
this.#tokenService = tokenService;
}
}
Como JS é uma linguagem de tipagem fraca, vamos criar algo que valide a instância dos objetos recebidos, para isso vamos criar um novo arquivo para fazer essa validação:
// src/domain/intanceof.js
const isInstanceOf = (instance, instanceBase) => instance instanceof instanceBase;
export default isInstanceOf;
Agora importamos em UserAuthentication e vamos validar as entradas:
import IRepository from './irepository.js';
import IEmail from './iemail.js';
import IPasswordHash from './ipassword-hash.js';
import IGenerateToken from './igenerate-token.js';
import isInstanceOf from './instanceof.js';
import DomainError from './domain-error.js';
import GatewayError from './gateway-error.js';
import InvalidArgumentError from './invalid-argument-error.js';
import throwError from './throw-error.js';
export default class UserAuthentication {
#repository;
#emailGateway;
#hashService;
#tokenService;
constructor(
repository,
emailGateway,
passwordHash,
tokenService
) {
this.#validateDependencies(repository, emailGateway, passwordHash, tokenService);
this.#repository = repository;
this.#emailGateway = emailGateway;
this.#hashService = passwordHash;
this.#tokenService = tokenService;
}
#validateDependencies(repository, emailGateway, passwordHash, tokenService) {
if (!this.#isInstanceOf(repository, IRepository)) {
throwError(DomainError, 'Invalid repository dependency');
}
if (!this.#isInstanceOf(emailGateway, IEmail)) {
throwError(DomainError, 'Invalid email gateway dependency');
}
if (!this.#isInstanceOf(passwordHash, IPasswordHash)) {
throwError(DomainError, 'Invalid hash service dependency');
}
if (!this.#isInstanceOf(tokenService, IGenerateToken)) {
throwError(DomainError, 'Invalid token service dependency');
}
}
#isInstanceOf(object, instanceBase) {
return isInstanceOf(object, instanceBase);
}
}
Antes de criarmos nosso método de autenticação, vamos criar duas classes, uma que vai encapsular os dados de entrada, e outra que vai encapsular os dados do usuário que vem do banco:
// src/domain/login-payload.js
import InvalidArgumentError from './invalid-argument-error.js';
import throwError from './throw-error.js';
export default class LoginPayload {
#email;
#password;
constructor(email, password) {
if (!email || !password) {
throwError(InvalidArgumentError, 'Fields email and password must be filled!');
}
this.#email = email;
this.#password = password;
}
get email() {
return this.#email;
}
get password() {
return this.#password;
}
}
// src/domain/user.js
import DomainError from'./domain-error.js';
import throwError from './throw-error.js';
export default class User {
token;
emailToken;
#id;
#email;
#password;
#expired;
constructor({id, email, password, token, emailToken, expired}) {
if (!id || !email || !password) {
throwError(DomainError, 'Invalid user data!')
}
this.#id = id;
this.#email = email;
this.#password = password;
this.token = token;
this.emailToken = emailToken;
this.#expired = expired;
}
get id() {
return parseInt(this.#id);
}
get expired() {
return !!this.#expired;
}
get email() {
return this.#email;
}
get password() {
return this.#password;
}
}
Importem essas classes na nossa classe de negócio, e agora vamos implementar nossa lógica de autenticação, observe o fluxo abaixo:
async authenticate(loginPayload) {
this.#validateUserDataInput(loginPayload);
const registeredUser = await this.getUserRegistered(loginPayload);
try {
const user = this.#getUserWithUpdatedData(registeredUser);
await this.#repository.update(user);
this.#emailGateway.send(user);
return user.token;
} catch (error) {
throwError(GatewayError, 'Generic error, see the integrations!');
}
}
#validateUserDataInput(loginPayload) {
if (!this.#isInstanceOf(loginPayload, LoginPayload)) {
throwError(DomainError, 'Invalid payload dependency');
}
}
async getUserRegistered(user) {
let registeredUser = null;
try {
registeredUser = await this.#repository.findByEmail(user.email);
} catch (e) {
throwError(GatewayError, 'Error connection database!');
}
return this.#getUserValidated(registeredUser, user);
}
async #getUserValidated(registeredUser, user) {
if (!registeredUser) {
throwError(InvalidArgumentError, 'User not found with data sent!');
}
if (!this.#isInstanceOf(registeredUser, User)) {
throwError(DomainError, 'Invalid user instance!');
}
const passwordsAreEquals = await this.#hashService.compare(user.password, registeredUser.password);
if (!passwordsAreEquals) {
throwError(InvalidArgumentError, 'Invalid user data!');
}
return registeredUser;
}
#isInstanceOf(object, instanceBase) {
return isInstanceOf(object, instanceBase);
}
#getUserWithUpdatedData(user) {
return new User({
id: user.id,
email: user.email,
...this.#getTokens(),
password: user.password,
expired: false
});
}
#getTokens() {
return {
token: this.#tokenService.getToken(),
emailToken: this.#tokenService.getEmailToken()
}
}
}
Temos o método authenticate que recebe um objeto de entrada, é feita a validação desse objeto, depois é feita a busca do usuário pelo e-mail e novamente validamos o objeto recebido, e por último validamos a senha. Volte ao código acima e veja que a gente envolve algumas chamadas em try/catch e em caso de erro, ou validações mal sucedidas, lançamos sempre as nossas classes de erros personalizados.
Após obtermos o usuário válido com a chamada do método #getUserRegistered, nós vamos criar um outro objeto, contendo tanto as informações do usuário do banco, como também as informações dos tokens. Tudo isso chamando o método #getUserWithUpdatedData, após isso, a gente chama o método de atualização do banco, depois manda os dados necessários por e-mail e retornamos um dos tokens gerados.
Note que o método de envio de e-mail, não esperamos o fim da execução, pois provavelmente o envio de e-mail seja um processo demorado, que pode travar a execução do nosso sistema.
Agora observem o objeto User, e principalmente o campo expired, esse campo é o responsável por verificar se os tokens enviados já foram utilizados ou não (trabalharemos nisso depois). Fazendo com que um par de tokens seja utilizado apenas uma vez.
Por fim, o arquivo user-authentication, deverá ficar com o conteúdo abaixo:
import User from './user.js';
import IRepository from './irepository.js';
import IEmail from './iemail.js';
import IPasswordHash from './ipassword-hash.js';
import IGenerateToken from './igenerate-token.js';
import LoginPayload from './login-payload.js';
import isInstanceOf from './instanceof.js';
import DomainError from './domain-error.js';
import GatewayError from './gateway-error.js';
import InvalidArgumentError from './invalid-argument-error.js';
import throwError from './throw-error.js';
export default class UserAuthentication {
#repository;
#emailGateway;
#hashService;
#tokenService;
constructor(
repository,
emailGateway,
passwordHash,
tokenService
) {
this.#validateDependencies(repository, emailGateway, passwordHash, tokenService);
this.#repository = repository;
this.#emailGateway = emailGateway;
this.#hashService = passwordHash;
this.#tokenService = tokenService;
}
#validateDependencies(repository, emailGateway, passwordHash, tokenService) {
if (!this.#isInstanceOf(repository, IRepository)) {
throwError(DomainError, 'Invalid repository dependency');
}
if (!this.#isInstanceOf(emailGateway, IEmail)) {
throwError(DomainError, 'Invalid email gateway dependency');
}
if (!this.#isInstanceOf(passwordHash, IPasswordHash)) {
throwError(DomainError, 'Invalid hash service dependency');
}
if (!this.#isInstanceOf(tokenService, IGenerateToken)) {
throwError(DomainError, 'Invalid token service dependency');
}
}
async authenticate(loginPayload) {
this.#validateUserDataInput(loginPayload);
const registeredUser = await this.#getUserRegistered(loginPayload);
try {
const user = this.#getUserWithUpdatedData(registeredUser);
await this.#repository.update(user);
this.#emailGateway.send(user);
return user.token;
} catch (error) {
throwError(GatewayError, 'Generic error, see the integrations!');
}
}
#validateUserDataInput(loginPayload) {
if (!this.#isInstanceOf(loginPayload, LoginPayload)) {
throwError(DomainError, 'Invalid payload dependency');
}
}
async #getUserRegistered(user) {
let registeredUser = null;
try {
registeredUser = await this.#repository.findByEmail(user.email);
} catch (e) {
throwError(GatewayError, 'Error connection database!');
}
return this.#getUserValidated(registeredUser, user);
}
async #getUserValidated(registeredUser, user) {
if (!registeredUser) {
throwError(InvalidArgumentError, 'User not found with data sent!');
}
if (!this.#isInstanceOf(registeredUser, User)) {
throwError(DomainError, 'Invalid user instance!');
}
const passwordsAreEquals = await this.#hashService.compare(user.password, registeredUser.password);
if (!passwordsAreEquals) {
throwError(InvalidArgumentError, 'Invalid user data!');
}
return registeredUser;
}
#isInstanceOf(object, instanceBase) {
return isInstanceOf(object, instanceBase);
}
#getUserWithUpdatedData(user) {
return new User({
id: user.id,
email: user.email,
...this.#getTokens(),
password: user.password,
expired: false
});
}
#getTokens() {
return {
token: this.#tokenService.getToken(),
emailToken: this.#tokenService.getEmailToken()
}
}
}
Teste
Agora que nossa lógica foi feita, vamos testar todos os fluxos. Acesse o terminal do container e rode o comando abaixo para instalar o Jest:
npm i --save-dev jest
Instamos essa biblioteca somente como dependência de desenvolvimento. Outra coisa que faremos é alterar o comando padrão de testes, para que ele chame o Jest, então, em nosso arquivo package.json, altere ele modificando o campo script para isso:
"scripts": {
"test": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js ./tests/* --coverage --config='{ \"coverageReporters\": [\"html\"] }'\n"
}
E agora vamos criar nossos testes, dentro do diretório tests, crie um diretório chamado unit, e dentro dele um diretório chamado mocks, essa pasta mocks vai servir para armazenar todas as implementações das interfaces do domínio. Poderíamos utilizar alguma lib para fazer isso, mas eu preferi fazer tudo na mão, então dentro de mocks, crie os arquivos abaixo contendo os seguintes códigos:
// tests/unit/mocks/email-mock.js
import IEmail from '../../../src/domain/iemail.js';
class EmailMock extends IEmail {
throwException = false;
send(user) {
if (this.throwException) {
throw Error();
}
return Promise.resolve('Sent!');
}
}
export default new EmailMock();
// tests/unit/mocks/repository-mock.js
import IRepository from '../../../src/domain/irepository.js';
import User from '../../../src/domain/user.js';
class RepositoryMock extends IRepository {
throwException = false;
throwExceptionUpdate = false;
returnEmptyObject = false;
returnEmpty = false;
constructor() {
super();
}
findByEmail(email) {
if (this.throwException) {
throw Error();
}
if (this.returnEmpty) {
return '';
}
if (this.returnEmptyObject) {
return {};
}
const user = new User({id: 1, email: 'erandir@email.com', password: '123456'});
return Promise.resolve(user);
}
update(user) {
if (this.throwExceptionUpdate) {
throw Error();
}
return Promise.resolve(true);
}
}
export default new RepositoryMock();
// tests/unit/mocks/hash-mock.js
import IPasswordHash from '../../../src/domain/ipassword-hash.js';
class HashMock extends IPasswordHash {
passwordsAreEquals = true;
constructor() {
super();
}
compare(password, hash) {
return this.passwordsAreEquals;
}
}
export default new HashMock();
// tests/unit/mocks/token-mock.js
import IGenerateToken from '../../../src/domain/igenerate-token.js';
class TokenMock extends IGenerateToken {
constructor() {
super();
}
getToken() {
return '13eb4cb6-35dd-4536-97e6-0ed0e4fb1fb3'
}
getEmailToken() {
return '4RV651gR93hDAGiTCYhmhh';
}
}
export default new TokenMock();
Agora dentro de tests/unit, crie um arquivo chamado user-authentication.test.js, é nesse arquivo que escreveremos todos os nosso testes para a classe UserAuthentication:
import UserAuthentication from '../../src/domain/user-authentication.js';
import EmailMock from './mocks/email-mock.js';
import RepositoryMock from './mocks/repository-mock.js';
import HashMock from './mocks/hash-mock.js';
import TokenMock from './mocks/token-mock.js';
import LoginPayload from '../../src/domain/login-payload.js';
const payload = new LoginPayload('erandir@email.com', '1234567');
const userAuthentication = new UserAuthentication(
RepositoryMock,
EmailMock,
HashMock,
TokenMock
);
test('Invalid object repository', function () {
const result = () => new UserAuthentication(
{},
EmailMock,
HashMock,
TokenMock
);
expect(result).toThrow('Invalid repository dependency');
});
test('Invalid object email', function () {
const result = () => new UserAuthentication(
RepositoryMock,
{},
HashMock,
TokenMock
);
expect(result).toThrow('Invalid email gateway dependency');
});
test('Invalid object hash', function () {
const result = () => new UserAuthentication(
RepositoryMock,
EmailMock,
{},
TokenMock
);
expect(result).toThrow('Invalid hash service dependency');
});
test('Invalid object token', function () {
const result = () => new UserAuthentication(
RepositoryMock,
EmailMock,
HashMock,
{}
);
expect(result).toThrow('Invalid token service dependency');
});
test('Invalid object login payload', async () => {
const result = async () => await userAuthentication.authenticate({});
expect(result).rejects.toThrow('Invalid payload dependency');
});
test('Error exception find user', async () => {
RepositoryMock.throwException = true;
const result = async () => await userAuthentication.authenticate(payload);
expect(result).rejects.toThrow('Error connection database!');
});
test('Error find user empty', async () => {
RepositoryMock.throwException = false;
RepositoryMock.returnEmpty = true;
const result = async () => await userAuthentication.authenticate(payload);
expect(result).rejects.toThrow('User not found with data sent!');
});
test('Error user without id', async () => {
RepositoryMock.returnEmpty = false;
RepositoryMock.returnEmptyObject = true;
const result = async () => await userAuthentication.authenticate(payload);
expect(result).rejects.toThrow('Invalid user instance!');
});
test('Error update user', async () => {
RepositoryMock.returnEmptyObject = false;
RepositoryMock.throwExceptionUpdate = true;
const result = async () => await userAuthentication.authenticate(payload);
expect(result).rejects.toThrow('Generic error, see the integrations!');
});
test('Error email exception', async () => {
RepositoryMock.throwExceptionUpdate = false;
EmailMock.throwException = true;
const result = async () => await userAuthentication.authenticate(payload);
expect(result).rejects.toThrow('Generic error, see the integrations!');
});
test('Error user divergent password', async () => {
EmailMock.throwException = false;
HashMock.passwordsAreEquals = false;
const result = async () => await userAuthentication.authenticate(payload);
expect(result).rejects.toThrow('Invalid user data!');
});
test('Get token', async () => {
HashMock.passwordsAreEquals = true;
const result = await userAuthentication.authenticate(payload);
expect(result).toBe('13eb4cb6-35dd-4536-97e6-0ed0e4fb1fb3');
});
Importamos as dependências e fizemos todos os testes em cima da nossa classe de negócios, seja passando dados errados, ou gerando exceção, passando por todos os caminhos errados, para só no final realizarmos o teste de caminho feliz.
Antes de executarmos nossos testes, precisamos configurar a modularização do nosso projeto, que por padrão é a do commonjs, e em nosso caso, utilizamos a do es6 module, então precisamos informar isso nas configurações, modifique o arquivo package.json e deixe-o assim:
{
"name": "nome_do_seu_projeto",
"version": "1.0.0",
"description": "",
"main": "index.js",
"directories": {
"test": "tests"
},
"scripts": {
"test": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js ./tests/* --coverage --config='{ \"coverageReporters\": [\"html\"] }'\n"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"jest": "^29.2.2"
},
"type": "module"
}
Vá ao terminal do serviço node e executar o código abaixo:
npm t
Será executado todos os teste e após isso uma pasta chamada coverage será criada na raiz do projeto. Dentro de coverage, vai ter inúmeros arquivos html, procure o index.html e abra ele no seu navegador. Você notará que temos um relatório de cobertura de testes, onde sabemos o que foi testado, o que falta testar, etc. Isso é muito útil.
Tópicos interessantes
Sabe-se que em JavaScript, podemos definir objetos de algumas formas diferentes, nesse projeto, fiz uso de classes, que é algo mais recente, isso porque tenho mais familiaridade com essa forma. O projeto inteiro poderia ter sido feito utilizando objetos literais, funções construtoras, etc.
Ainda falando sobre classes, perceba que para criar atributos e métodos privados, eu faço uso do caractere #, que também é um recurso recente. Outro detalhe fica por conta das subclasses, que sempre definimos um construtor e que sempre o primeiro comando é chamar o construtor do pai.
Além disso, percebam como estamos isolando nossa camada de regra de negócio, dependemos sempre de interfaces, isso nos possibilita simular comportamentos, o que nos ajudou nos testes.
Se você é um desenvolvedor mais experiente, possivelmente pode ter notado que não implementei uma validação importante, que é se os tokens gerados são únicos, isso foi de certa forma proposital, caso alguém queira mexer no código, alterar e adicionar essa validação.
E sobre a questão da modularização do projeto, caso quiséssemos utilizar a padrão, que é o commonjs, iriamos substituir os comando de exportar e importar, por exemplo:
// es6 Module
export default class InvalidArgumentError extends Error {
constructor(message) {
super(message);
}
}
// E para importar fazemos assim
import InvalidArgumentError from './invalid-argument-error.js';
// Em commonjs fariamos da seguinte forma
module.exports = class InvalidArgumentError extends Error {
constructor(message) {
super(message);
}
}
// E para importar faríamos assim
const InvalidArgumentError = require('./invalid-argument-error.js');
Existem algumas pequenas diferenças entre usar uma ou outra, mas eu gosto de utilizar a modularização do ES6.
Resumo
Neste artigo, começamos a montar nosso projeto, criamos testes, relatório de teste, aprendemos a importar e exportar módulos, validamos objetos, etc. Vale a pena notar que essa aqui foi a minha linha de raciocínio, talvez você tenha pensado em uma forma diferente de fazer tudo que fiz nessa primeira parte, você pode implementar do jeito. Continuaremos desenvolvendo nossa lógica no próximo artigo, até lá.
Top comments (0)