Um dos principais problemas com linguagens digitadas dinamicamente é que nem sempre é possível garantir que o fluxo de dados esteja correto porque não é possível forçar um parâmetro ou variável a ser definido com um valor diferente de nulo, por exemplo. Nesses casos, tendemos a usar um código simples:
function foo (mustExist) {
if (!mustExist) throw new Error('Parameter cannot be null')
return ...
}
O problema com essa abordagem é a poluição do código, já que você tem que testar variáveis em todos os lugares e não há como garantir que todos os desenvolvedores irão realmente executar esse teste sempre, especialmente em situações onde uma variável ou parâmetro não pode ser nulo. Muitas vezes nem sabemos que tal parâmetro pode ter o valor undefined ou null - isso geralmente acontece quando diferentes especialistas trabalham nas partes cliente e servidor, ou seja, na grande maioria dos casos.
Para otimizar um pouco esse cenário, comecei a procurar como e com quais estratégias é melhor minimizar o fator surpresa. Foi quando me deparei com um ótimo artigo de Eric Elliott.... O objetivo deste trabalho não é refutar completamente seu artigo, mas adicionar informações interessantes que fui capaz de descobrir ao longo do tempo graças à minha experiência no campo do desenvolvimento de JavaScript.
Antes de começar, gostaria de passar por alguns dos pontos que são abordados neste artigo e expressar minha opinião como desenvolvedor de componentes de servidor, já que outro artigo é mais voltado para o cliente.
Como tudo começou
O problema de processamento de dados pode ser devido a vários fatores. O principal motivo, é claro, é a entrada do usuário. No entanto, existem outras fontes de dados malformados, além das mencionadas em outro artigo:
- Registros de banco de dados
- Funções que retornam implicitamente dados nulos
- APIs externas
Em todos os casos considerados, soluções diferentes serão aplicadas, e posteriormente analisaremos cada uma delas detalhadamente, lembrando que nenhuma é uma panacéia. A maioria dos problemas é causada por erro humano: em muitos casos, as linguagens estão preparadas para trabalhar com dados nulos ou indefinidos (nulos ou indefinidos), mas no processo de transformação desses dados pode-se perder a capacidade de processá-los.
Dados inseridos pelo usuário
Nesse caso, temos muito poucas oportunidades. Se o problema reside na entrada do usuário, pode ser resolvido com a chamada hidratação (em outras palavras, temos que pegar a entrada bruta que o usuário nos envia (por exemplo, como parte de uma carga útil de API) e transformá-la em algo com o qual podemos trabalhar sem erros).
No lado do servidor, ao usar um servidor web como o Express, podemos realizar todas as operações com a entrada do usuário no lado do cliente usando ferramentas padrão como o esquema JSON ou Joi .
Um exemplo do que pode ser feito usando Express ou AJV é dado abaixo:
const Ajv = require('ajv')
const Express = require('express')
const bodyParser = require('body-parser')
const app = Express()
const ajv = new Ajv()
app.use(bodyParser.json())
app.get('/foo', (req, res) => {
const schema = {
type: 'object',
properties: {
name: { type: 'string' },
password: { type: 'string' },
email: { type: 'string', format: 'email' }
},
additionalProperties: false
required: ['name', 'password', 'email']
}
const valid = ajv.validate(schema, req.body)
if (!valid) return res.status(422).json(ajv.errors)
// ...
})
app.listen(3000)
Veja: estamos verificando a parte principal da rota. Por padrão, este é o objeto que obtemos do pacote body-parser como parte da carga útil. Nesse caso, estamos passando pelo esquema JSON , então será validado se uma dessas propriedades é de outro tipo ou formato (no caso de e-mail).
Importante! Observe que estamos retornando um HTTP 422 para um objeto não processado . Muitas pessoas interpretam um erro de consulta, como um corpo ou string de consulta inválido, como erro 400 Consulta inválida - isso é parcialmente verdadeiro, mas neste caso o problema não estava na solicitação em si, mas nos dados que o usuário enviou com ela. Portanto, a resposta ideal para o usuário seria o erro 422: isso significa que a solicitação está correta, mas não pode ser processada porque seu conteúdo não está no formato esperado.
Outra opção (além de usar AJV) é usar a biblioteca que criei com Roz . Chamamos de Expresso e é um conjunto de bibliotecas que torna um pouco mais fácil desenvolver APIs que usam Express. Uma dessas ferramentas é o @ expresso / validator , que essencialmente faz o que demonstramos acima, mas pode ser entregue como middleware.
Parâmetros adicionais com valores padrão
Além do que verificamos anteriormente, descobrimos a possibilidade de passar um valor nulo para o nosso aplicativo caso não seja enviado em um campo opcional. Imagine, por exemplo, que temos uma rota de paginação que leva dois parâmetros, página e tamanho, como strings de consulta. No entanto, eles são opcionais e devem ser padronizados se não forem recebidos.
Idealmente, nosso controlador deve ter uma função que faça algo assim:
function searchSomething (filter, page = 1, size = 10) {
// ...
}
Nota. Assim como com o erro 422 que retornamos em resposta às solicitações de paginação, é importante retornar o código de erro correto, 206 Conteúdo incompleto , sempre que respondemos a uma solicitação para a qual a quantidade de dados retornados faz parte de um todo, retornamos 206. Quando o usuário atingiu a última página e não há mais dados, podemos retornar um código de 200, e quando o usuário tenta encontrar uma página fora do intervalo total de páginas, retornamos o código 204 Sem conteúdo .
Isso resolveria o problema quando obtemos dois valores nulos, mas esse é um aspecto muito controverso do JavaScript em geral. Os parâmetros opcionais assumem um valor padrão apenas se o valor estiver vazio; no entanto, essa regra não funciona para o valor nulo, portanto, se fizermos o seguinte:
function foo (a = 10) {
console.log(a)
}
foo(undefined) // 10
foo(20) // 20
foo(null) // null
e precisamos que as informações sejam tratadas como nulas, não podemos depender apenas de parâmetros opcionais para isso. Portanto, nesses casos, temos duas maneiras:
1. Use as instruções If no controlador
function searchSomething (filter, page = 1, size = 10) {
if (!page) page = 1
if (!size) size = 10
// ...
}
Não parece muito bom e é bastante inconveniente.
2. Use esquemas JSON diretamente na rota
Novamente, podemos usar AJV ou @ expresso / validator para validar esses dados:
app.get('/foo', (req, res) => {
const schema = {
type: 'object',
properties: {
page: { type: 'number', default: 1 },
size: { type: 'number', default: 10 },
},
additionalProperties: false
}
<a href=""></a> const valid = ajv.validate(schema, req.params)
if (!valid) return res.status(422).json(ajv.errors)
// ...
})
Trabalho com valores nulos e indefinidos
Pessoalmente, não estou feliz com a ideia de usar null e undefined em JavaScript para provar que o valor está vazio, por várias razões. Além das dificuldades em trazer esses conceitos para o nível abstrato, não se deve esquecer dos parâmetros opcionais. Se você ainda tem dúvidas sobre esses conceitos, deixe-me dar um ótimo exemplo da prática:
Agora que entendemos as definições, podemos dizer que em 2020 haverá duas funções importantes em JavaScript: o operador de coalescência nulo e encadeamento opcional . Não vou entrar em detalhes agora, pois já escrevi um artigo sobre isso. (está em português), mas observe que essas duas inovações simplificarão muito nossa tarefa, já que podemos nos concentrar nesses dois conceitos, nulo e indefinido com um operador apropriado (??), em vez de usar negativos lógicos como! obj que são terreno fértil para erros.
Funções que retornam nulo implicitamente
Este problema é muito mais difícil de resolver devido à sua natureza implícita. Algumas funções processam dados presumindo que eles sempre serão fornecidos, mas em alguns casos, esse não é o caso. Vamos considerar um exemplo padrão:
function foo (num) {
return 23*num
}
Se num for nulo, o resultado desta função será 0, o que não era esperado. Nesses casos, não temos escolha a não ser testar o código. Existem dois tipos de teste que podem ser feitos. A primeira é usar uma instrução if simples:
function foo (num) {
if (!num) throw new Error('Error')
return 23*num
}
A segunda maneira é usar a mônada Either , que é abordada em detalhes no artigo que mencionei. Essa é uma ótima maneira de lidar com dados ambíguos, ou seja, dados que podem ou não ser nulos. Isso ocorre porque o JavaScript já tem uma função integrada que suporta dois fluxos de ações, Promessa:
function exists (value) {
return x != null ? Promise.resolve(value) : Promise.reject(`Invalid value: ${value}`)
}
async function foo (num) {
return exists(num).then(v => 23 * v)
}
É assim que você pode delegar a instrução catch from exists para a função que chamou foo:
function init (n) {
foo(n)
.then(console.log)
.catch(console.error)
}
init(12) // 276
init(null) // Invalid value: null
APIs externas e registros de banco de dados
Este é um caso muito comum, especialmente quando existem sistemas desenvolvidos a partir de bancos de dados que foram criados ou preenchidos anteriormente. Por exemplo, um novo produto que usa o mesmo banco de dados de seu predecessor de sucesso, integrando usuários de sistemas diferentes e assim por diante.
O grande problema com isso não é o fato de o banco de dados ser desconhecido - na verdade, esse é o motivo, já que não sabemos o que foi feito no nível do banco de dados, e não podemos confirmar se receberemos dados com valor nulo ou indefinido ou não ... Não podemos deixar de falar sobre a documentação de baixa qualidade quando o banco de dados não está devidamente documentado e enfrentamos o mesmo problema de antes.
Não há quase nada que possamos fazer aqui e, pessoalmente, prefiro verificar o estado dos dados para ter certeza de que posso trabalhar com eles. No entanto, você não pode validar todos os dados, pois muitos dos objetos retornados podem simplesmente ser muito grandes. Portanto, antes de realizar qualquer operação, é recomendável verificar os dados envolvidos no funcionamento da função, como um mapa ou um filtro, para certificar-se de que está indefinido ou não.
Gerando erros
É uma boa prática usar funções de asserção para bancos de dados e APIs externas. Essencialmente, essas funções retornam dados, se houver, e caso contrário, um erro é gerado. O caso de uso mais comum para esse tipo de função é quando temos uma API, por exemplo, para pesquisar um tipo de dados específico por identificador, o conhecido findById:
async function findById (id) {
if (!id) throw new InvalidIDError(id)
const result = await entityRepository.findById(id)
if (!result) throw new EntityNotFoundError(id)
return result
}
Substitua Entity pelo nome de sua entidade, como UserNotFoundError.
Isso é bom, pois podemos ter uma função dentro do mesmo controlador para localizar usuários por ID e outra função que utiliza esse usuário para localizar outros dados, por exemplo, os perfis deste usuário em outra coleção de bancos de dados. Ao chamar a função de pesquisa de perfil, usamos asserção para garantir que o usuário realmente exista em nosso banco de dados. Caso contrário, a função nem será executada e você pode pesquisar o erro diretamente na rota:
async function findUser (id) {
if (!id) throw new InvalidIDError(id)
const result = await userRepository.findById(id)
if (!result) throw new UserNotFoundError(id)
return result
}
async function findUserProfiles (userId) {
const user = await findUser(userId)
const profile = await profileRepository.findById(user.profileId)
if (!profile) throw new ProfileNotFoundError(user.profileId)
return profile
}
Observe que não faremos uma chamada de banco de dados se o usuário não existir, pois a primeira função garante que o usuário existe. Agora podemos fazer algo assim na rota:
app.get('/users/{id}/profiles', handler)
// --- //
async function handler (req, res) {
try {
const userId = req.params.id
const profile = await userService.getProfile(userId)
return res.status(200).json(profile)
} catch (e) {
if (e instanceof UserNotFoundError || e instanceof ProfileNotFoundError) return res.status(404).json(e.message)
if (e instanceof InvalidIDError) return res.status(400).json(e.message)
}
}
Podemos descobrir o tipo de erro retornado simplesmente verificando o nome da instância da classe de erro existente.
Conclusão
Existem várias maneiras de processar dados para garantir um fluxo contínuo e previsível de informações. Você conhece alguma outra dica ?! Deixe nos comentários
Gostou do material ?! Quer dar um conselho, expressar uma opinião ou apenas dizer olá? Veja como me encontrar nas redes sociais:
Este artigo foi postado originalmente em dev.to por Lucas Santos. Se você tiver dúvidas ou comentários sobre o tópico do artigo, poste-os sob o artigo original em dev.to