Este é um código espaguete de aplicativo da web nodejs?
Recentemente fui entrevistado para uma função de desenvolvedor de back-end em uma startup envolvida em produtos financeiros, como um graduado de 2020. A tarefa que eles levaram para casa tinha alguns objetivos básicos e eles recomendaram a pilha Nodejs / MongoDB. Recebi o seguinte feedback sobre o meu envio:
1. A visibilidade da arquitetura REST é menor.
2. A estruturação do código poderia ter sido melhor (concordo com isso).
3. Se o idioma escolhido for Nodejs, pelo menos o uso da sintaxe básica deve estar correto. O oposto foi observado.
Minhas perguntas sobre este feedback são:
1. A estrutura de uma API REST não é altamente subjetiva? Como posso tornar meu aplicativo mais compatível com os objetivos REST?
2. O que é "uso incorreto de sintaxe?" Se minha sintaxe estivesse incorreta, o projeto se comportaria mal ou não funcionaria, não é? Publiquei esta questão no subreddit r / codereview e recebi poucos comentários úteis além de um comentário dizendo "espaguete". Eu adoraria se você pudesse me dar dicas sobre como melhorar meu código.
Adoraria aprender com este exercício e estou aberto a todos os comentários / críticas. Meu código reside em github.com/utkarshpant/portfolio-api junto com a documentação. Testei o aplicativo usando o Postman e, como exercício, estou escrevendo testes de unidade / integração para ele.
Estou reproduzindo abaixo uma parte do routes/portfolio_v2.jsarquivo, onde implementei a maioria dos endpoints:
const dbDebugger = require('debug')('app:db');
const apiDebugger = require('debug')('app:api');
const express = require('express');
const router = express.Router();
const Joi = require('joi');
const customError = require('http-errors');
const errorHandlerMiddleware = require('../middleware/errorHandlerMiddleware');
// importing models;
const Portfolio = require('../models/portfolioModel');
// import validations;
const validations = require('../validations/validateRequest');
// get returns on portfolio
router.get('/getReturns/:portfolioName', errorHandlerMiddleware((req, res) => {
const portfolioName = req.params.portfolioName;
(async () => {
const portfolio = await Portfolio.findOne({ "name": portfolioName }).catch(err => res.send(error));
if (!portfolio) {
return res.status(404).send("No portfolio found");
}
const currentPrice = 100;
let returns = 0;
portfolio.securities.forEach(security => {
apiDebugger(`The returns on ${security.ticker} are ${((currentPrice - security.avgBuyPrice) * security.shares)}`);
returns += ((currentPrice - security.avgBuyPrice) * security.shares)
})
console.log("Returns:\t", returns);
res.send({
portfolio: portfolio.name,
cumulativeReturns: returns
});
})();
}));
// place a buy trade
router.post('/buy/:portfolioName', errorHandlerMiddleware((req, res) => {
/*
Request body includes:
Trade object, including ticker, type, quantity, price
TODO:
- validations for
- ticker match
- trade type == sell,
- shares - quantity > 0 always
- resultant shares > 0 always
*/
// validating request body;
const { error } = validations.validateTradeRequest(req);
if (error) {
throw customError(400, "Bad Request; re-check the request body.");
} else {
// mismatch of type;
if (req.body.type != "buy") {
throw customError(400, "Bad request; type must be 'buy'.")
}
const portfolioName = req.params.portfolioName;
const trade = req.body;
(async () => {
// retrieve portfolio and find relevant security;
const portfolio = await Portfolio.findOne({ "name": portfolioName }).catch(err => res.send(err));
if (!portfolio) {
return res.status(404).send("No portfolio found");
}
const security = portfolio.securities.find(security => security.ticker == trade.ticker);
// if the ticker does not exist, return a 404;
if (!security) {
return res.status(404).send("Invalid ticker.");
}
// register sell trade and update shares;
security.trades.push(trade);
let oldShares = security.shares;
security.shares += trade.quantity;
security.avgBuyPrice = (((security.avgBuyPrice) * (oldShares)) + ((trade.price) * (trade.quantity))) / (security.shares);
apiDebugger(`(security.avgBuyPrice): ${security.avgBuyPrice},\nsecurity.shares: ${security.shares},\ntrade.price: ${trade.price},\ntrade.quantity: ${trade.quantity}\n`);
// save portfolio
try {
await portfolio.save().then(res.status(200).send(trade));
} catch (err) {
let errorMessages = [];
ex.errors.forEach(property => errorMessages.push(property));
res.status(500).send("An error occured in saving the transaction.")
}
})();
}
}));
// place a sell trade
router.post('/sell/:portfolioName', errorHandlerMiddleware((req, res) => {
/*
Request body includes:
Trade object, including ticker, type, quantity
TODO:
- validations for
- ticker match
- trade type == sell,
- shares - quantity > 0 always
- resultant shares > 0 always
*/
// validating request body;
const { error } = validations.validateTradeRequest(req);
if (error) {
throw customError(400, "Bad Request; re-check the request body.");
} else {
if (req.body.type != "sell") {
throw customError(400, "Bad Request; type must be 'sell'.");
}
const portfolioName = req.params.portfolioName;
const trade = req.body;
(async () => {
// retrieve portfolio and find relevant security;
const portfolio = await Portfolio.findOne({ "name": portfolioName }).catch(err => res.send(err));
if (!portfolio) {
return res.status(404).send("No portfolio found");
}
const security = await portfolio.securities.find(security => security.ticker == trade.ticker);
// check that resultant share count > 0;
if ((security.shares - trade.quantity) < 0) {
// throw customError(422, `The given Trade will result in ${security.shares - trade.quantity} shares and cannot be processed.`); return res.status(422).send(`Request cannot be serviced. Results in (${security.shares - trade.quantity}) shares.`);
}
// register sell trade and update shares;
security.trades.push({ "ticker": trade.ticker, "type": "sell", quantity: trade.quantity });
security.shares -= trade.quantity;
// save portfolio
try {
await portfolio.save().then(res.status(200).send(trade)).catch();
} catch (err) {
let errorMessages = [];
ex.errors.forEach(property => errorMessages.push(property));
res.status(500).send("An error occured in saving the transaction.")
}
})();
}
}));
function validateRequest(request) {
const tradeRequestSchema = Joi.object({
ticker: Joi.string().required().trim(),
type: Joi.string().required().valid("buy", "sell").trim(),
quantity: Joi.number().required().min(1).positive(),
price: Joi.number().min(1).positive()
})
return tradeRequestSchema.validate(request.body);
}
module.exports = router;
Obrigado!
Respostas
De uma revisão média;
O tratamento de exceções definitivamente levanta sobrancelhas
exnão está definido, entãoex.errors.forEach(property => errorMessages.push(property));irá falharerrorMessagesé um mistério, você não parece fazer nada com isso?Esta
let errorMessages = []; ex.errors.forEach(property => errorMessages.push(property));deveria estar
const errorMessages = err.errors;Da mesma forma,
.catch(err => res.send(error));não vai funcionar
Use uma ferramenta de dica como https://jshint.com/
async () =>cria uma função anymous, você deve usar funções nomeadas por motivos de rastreamento de pilhaEnvolva
console.logem algum tipo de função de filtragem de nível de detalhamento, nunca escrevaconsole.logdiretamenteO cálculo de
returnsseria perfeito para exibirreduce()O grande comentário de
router.post('/buy/:portfolioName'deve estar acima da funçãoPara mim
:portfolioName/buyé muito mais como REST do que'/buy/:portfolioName', eu olho para os grandes jogadores e eles tendem a ir muito mais para substantivo / verbo do que verbo / substantivoSe eu fosse registrar erros para um aplicativo de negociação, eu
- Crie para cada erro um GUID e despeje esse GUID nos logs do servidor
- O Als fornece esse GUID na mensagem 500,
"An error occured in saving the transaction."é tão desprovido de informações que pode fazer com que você seja demitido
Quando eu vejo isso;
if (error) { throw customError(400, "Bad Request; re-check the request body."); } else {- Eu me encolho, por que você
elsedepois de umthrow? - por que você não faz
elsedepois do próximothrow?
- Eu me encolho, por que você
Era o design deles verificar,
typemesmo se o tipo fosse parte da URL? Isso é apenas um design ruimVocê deveria ter envolvido
validateTradeRequestem uma função que- Ligações
validateTradeRequest - Verifica o tipo
- Verifica o tipo de portfólio
- Lança um erro se algo mais der errado
- Reutilizou esta função em ambos
sellebuy
- Ligações
return res.status(404).send("Invalid ticker.");é uma escolha infeliz de código de erro e mensagem http. Em termos de REST, 404 significa que o portfólio não existe, mas existe. Mensagem sábia, imagine se a mensagem dissesseNo security found for ticker ${trade.ticket} in portfolio ${portfolioName}Uma vez você usa
portfolio.securities.finde outra vezawait portfolio.securities.find?? Parei de revisar em profundidade aqui ..Quanto à visibilidade, concordo com o revisor, imagine que seu código começou com
router.get( '/:portfolio/getReturns', getPortfolioReturns); router.post('/:portfolio/buy', postPortfolioBuy); router.post('/:portfolio/sell', postPortfolioSell);o leitor saberia em 1 fração de segundo o que esse código faz (possivelmente ficar incomodado com aquele espaço para alinhar o
getcom opost;)) Agora o leitor tem que se arrastar por linhas e linhas de código para descobrir isso.Para a parte da sintaxe, o uso de
asyncdesencadeia meu senso de aranha, mas toda vez que acho que encontrei algo estou errado. O revisor é muito inteligente ou quer parecer muito inteligente
De modo geral, e raramente digo isso, deveria haver testes. Mais especificamente, deveria haver testes para falhas, e você teria encontrado e corrigido muitos desses problemas.