[DÚVIDA] - Lidando com imagens no back-end

Eae pessoal, tudo certo ? Estou em dúvida de como abordar um assunto, e gostaria de ouvir a opinião de vocês.

Contexto

Estou fazendo um projeto de estudo utilizando NodeJS, só para treinar algumas coisas que eu nunca fiz. Por exemplo, nos meus projetos anteriores, sempre utilizei o Fastify como framework, algum ORM para lidar com o banco de dados, SQLite, e Javascript. Nesse projeto eu estou utilizando Express, sem nenhum ORM, utilizando Postgresql em um container docker, e Typescript.

Enfim, neste projeto há uma funcionalidade que permite o usuário adicionar produtos, contendo nome, descrição, preço e uma imagem. No Controller desta rota, eu recebo a imagem com o tipo multipart/formdata, salvo ela localmente em uma pasta public/uploads utilizando o Multer (acredito que geralmente isso é armazenado em um storage de terceiro, porém fiz dessa forma pois esse não era o foco), crio uma url com base no nome da imagem + data de upload, e salvo essa url no banco, junto com os outros dados. Esse procedimento de capturar a imagem recebida no body da requisição, validar se ela realmente é uma imagem, validar tamanho, etc. ocorre em um Middleware nessa rota de Adicionar Produto.

Está funcionando normalmente, ele cria a url corretamente, e salva localmente. A rota de deletar um produto também funciona, ele deleta o registro no banco, e remove o arquivo localmente também.

Problema - Editando o produto.

Vou criar uma rota para permitir que o usuário edite um produto. No front-end, o usuário clicaria no botão de editar produto, e seria redirecionado para uma página de formulário, carregando todas as informações do produto em questão, porém habilitando os inputs, permitindo que ele altere tanto os campos de texto, quanto a imagem. No final, ele clicaria em Salvar, que dispararia uma requisição para uma rota /produts/edit/:id, enviando TODAS as informações daquele produto (independente se foram alterados ou não).

Nos campos do tipo texto é ok, visto que é só eu fazer um UPDATE FROM WHERE, porém a questão da imagem está me intrigando um pouco: Se o usuário alterar a imagem do produto, eu iria receber a nova imagem no body, validar igual eu fiz na rota de Adicionar Produto, excluir a imagem "antiga" localmente (eu criei uma função para isso na rota de Deletar, então eu conseguiria reaproveitar), e atualizar a image_url no banco. Agora vem minha dúvida (sim, eu sei que enrolei bastante para chegar nisso kkkkkk desculpa): E se o usuário não alterar a imagem ?

Em um primeiro momento, eu pensei em fazer a mesma coisa como se ele tivesse alterado: Eu teria que excluir a imagem "antiga" localmente, e receber ela novamente no body, a mesma imagem. Mas isso me cheira estranho. Pensei em alguma forma identificar que as imagens são iguais, mas não consegui pensar em uma. Eu poderia verificar pelo nome da imagem, mas se o usuário enviar uma imagem com conteúdo diferente, porém com o mesmo nome, a aplicação rejeitaria.

Eu sei que é uma aplicação de estudo, e que provavelmente neste contexto não haveria problema em deletar e adicionar a mesma imagem, porém como eu disse, isso cheira estranho, e mesmo não trabalhando na área, acredito que não seja feito desse jeito. Gostaria do ponto de vista de vocês sobre como lidar com isso (não precisa ter exemplo em código, só quero uma explicação sobre como abordar.)

Muito obrigado, e um ótimo final de semana.

Você conhece o método HTTP PATCH?

Estou com a impressão de que você está fazendo tudo somente através do método HTTP POST. A evidência pra isso é pela sua rota: /produts/edit/:id. Esse "edit" na rota foge do padrão REST, pois geralmente não utilizamos verbos nas rotas - utilizamos somente substantivos. Para indicar que você quer editar um produto já existente, e diferenciar da rota de criação de produto, geralmente utilizamos os verbos PATCH ou PUT. Veja o seguinte link: https://pt.stackoverflow.com/a/217901/128995

Resumindo, o PUT permite que alteremos a entidade inteira (todos os campos do produto). Enquanto isso, o PATCH permite que alteremos somente alguns campos da entidade (por exemplo, só a foto, ou só o nome, ou o nome e o preço, etc.).

Então me soa que você quer utilizar o método HTTP PATCH, para alterar somente alguns campos. O cliente (front-end) saberá quais campos mudaram e quais não mudaram, e farà a requisição passando somente os campos que foram alterados, com seus novos valores. Então se o usuário não mudou a foto, você não passa o campo da foto, e nada muda.

Na verdade eu nem criei a rota ainda, mas eu estava pensando em utilizar o método PUT. Na minha visão, este é o método ideal, visto que o usuário pode em uma única requisição alterar todos os campos mencionados. Ele não é obrigado a fazer isso, ele pode alterar apenas um. Mas se a aplicação permite que todos sejam alterados em uma requisição, então é um PUT. Posso estar errado também neste pensamento, não sei. O método PATCH poderia ser aplicado, se por exemplo, eu tivesse uma rota exclusiva só para alterar a imagem. Mas como eu tenho apenas uma rota, que permite a alteração de todos os campos, então é um PUT. > Resumindo, o PUT permite que alteremos a entidade inteira (todos os campos do produto). Enquanto isso, o PATCH permite que alteremos somente alguns campos da entidade (por exemplo, só a foto, ou só o nome, ou o nome e o preço, etc.). Exatamente o que você mencionou.
> O método PATCH poderia ser aplicado, se por exemplo, eu tivesse uma rota exclusiva só para alterar a imagem Na verdade, não! Se você tivesse uma rota exclusiva para alterar somente a imagem, não seria necessário utilizar o método PATCH! Se fizer uma rota separada, a imagem seria considerada uma entidade separada, e não um campo da entidade produto. Então poderia ser um PUT mesmo. Ter uma rota separada para a imagem pode ser uma boa solução também (talvez até a melhor), como já sugeriram, mas o PATCH não é necessário nesse caso, pois você estaria alterando a entidade inteira, e não um pedaço dela. Talvez o meu resumo tenha sido resumido demais... O PUT é utilizado quando estamos substituindo a entidade existente por completo (e se entidade não existir, ela é criada). É como se a antiga deixasse de existir, e outra ficasse no lugar dela, com o mesmo ID. E o PATCH é utilizado quando queremos atualizar valores daquela entidade existente. Pode ser um ou todos os valores. Ainda é a mesma entidade, parecida, terá o mesmo ID, mas será diferente. **Se modificamos todos os valores da entidade, o resultado do PUT e do PATCH é o mesmo**. mas se modificamos somente alguns valores, o resultado é diferente! A ideia é a seguinte: de qualquer forma o frontend tem que passar os valores novos dos campos editados para o backend através do request body, né? Segundo o padrão REST, se você utiliza o PUT, o seu frontend seria obrigado a passar todos os valores, de todos os campos da entidade através do request body - independente se todos eles mudaram ou não. Ou seja, nome, preço, quantidade, imagem, tudo. Se você não passa algum desses valores, o padrão REST indica que as colunas em branco sejam atualizadas para nulo no banco de dados! Ou seja, tudo que não é passado é jogado fora! No banco, você altera a linha inteira de uma vez. Com o PATCH, o frontend só envia os campos que foram alterados (e seus novos valores). No seu controller você itera sobre cada campo recebido no request body, e atualiza somente as colunas correspondentes no banco de dados. Se o cliente muda só o nome e o preço, o cliente faz uma requisição enviando somente o nome e o preço no JSON do request body, através de um método PATCH, e o backend deve atualizar somente esses valores no banco de dados - os valores não enviados continuam iguais já estavam no banco, ao invés de ficar nulo. Também é possível alterar todos os campos através do PATCH, desde que cada um deles tenha sido alterado pelo usuário. E lembre-se de que você não precisa escolher entre criar um PUT ou um PATCH. Você pode criar as duas rotas. Na verdade, é até encorajado, já que em determinados momentos pode fazer sentido chamar um ou outro. Um sistema CRUD é baseado em Create (POST), Read (GET), Update (PUT e PATCH), e Delete (DELETE). Não faz sentido criar um POST e não poder criar um DELETE também, né? Aliás, é incomum um usuário ter, por exemplo, um produto com nome maçã, uma foto de maçã, e preço de maçã, e depois ele querer atualizar esse produto para banana, preço de banana, e foto de banana. É mais comum que ele delete a maçã (DELETE) e crie uma banana do zero (POST) né? Mas se ele quiser fazer isso, ele pode fazer tranquilamente utilizando o PATCH também. De qualquer forma, no seu caso específico, mantenho a recomendação de utilizar o PATCH no lugar do PUT, e enviar na requisição somente os pares de campo-valor que mudaram, para seguir o padrão RESTful. Recomendo também estas duas leituras breves, para exemplificar e complementar: https://medium.com/xp-inc/node-js-atualiza%C3%A7%C3%B5es-parciais-com-o-verbo-patch-61b47542fbaa https://stackoverflow.com/questions/28459418/use-of-put-vs-patch-methods-in-rest-api-real-life-scenarios
Valeu. Realmente neste contexto acho que o PATCH se encaixa melhor. Eu ia fazer com o PUT, onde o usuário iria enviar todos os dados, independente de terem sido alterados ou não, pois como são apenas 5 campos, achei que seria melhor, do que ver verificar quais foram os campos recebidos no Controller. Mas ai me surgiu essa dúvida em relação a imagem, e em nenhum momento eu pensei em simplesmente não enviar a imagem. Ou seja, eu fiquei preso com essa questão de que o usuário tem que enviar tudo novamente, inclusive a imagem, e estava buscando idéias sobre como verificar se a imagem que recebi é a mesma que eu já tinha ou não. Muito obrigado.

Você não precisa identificar se as imagens são as mesmas, amigo. Na verdade, você pode fazer da seguinte forma:

Caso a imagem seja obrigatória para o produto, você primeiro checa se ele já possui uma imagem salva ou não e isso define se você irá requerer o preenchimento do input de arquivo.

Caso já tenha, você pode apresentar a imagem existente pro usuário e dar a opção de subir outra para substituir no lugar. Caso não receber nada você simplesmente ignora a parte de upload local e geração de URL e tudo mais, apenas ignora aquele campo e atualiza o restante.

Nossa, é tão fácil e eu nem pensei nisso. Na minha cabeça, quando eu exibisse a imagem antiga, eu estaria com ela "carregada" no input. Mas isso não é necessário. É só eu exibir a imagem, e deixar o input lá. Se o usuário carregar algo nesse input, quer dizer que ele quer alterar a foto, e então eu envio ela na requisição. Se tiver vazia, quer dizer que ele quer manter a foto antiga mesmo, então eu não altero nada. Muito obrigado.

acredito que geralmente isso é armazenado em um storage de terceiro, porém fiz dessa forma pois esse não era o foco

Não, isso é comum ser no proprio host, pq custa menos. Só usam de terceiros quando fica muito grande a quantidade de imagens!

Quanto a edição da imagem,

Vc não precisa mexer na imagem original se o usuário não mudar a imagem! Pq isso agora parece "certo" mas quando tiver muita gente editando, isso vai gerar um problema de banda enorme!

O que eu faria: na edição colocaria uma thumb da imagem original(pra pessoa saber que tem a imagem) e um campo de enviar imagem logo abaixo(ou um botão mudar imagem, quando clicado abre o campo de upload e...). Ou seja, as coisas mudariam só se o usuário enviasse uma nova imagem(apaga a antiga coloca a nova e tal). Caso contrario nada muda no banco de bados e no banco de imagens!

Valeu uriel. Muito massa saber essa questão de armazenar local vs terceiro. Sobre a edição, essa questão da thumb já existe no front-end, então quando o usuário escolhe uma foto do seu pc, ele já mostra a prévia da imagem. Se eu entendi bem, então eu teria uma rota separada só para editar uma imagem ? Da forma como está, há um ícone "Editar produto" em cada um dos produtos que ficam listados em uma página. Quando eu clico nesse ícone, ele abre um formulário, carregando todas as informações do produto, inclusive a foto na thumb. Ai o usuário edita apenas o que ele quer, e envia novamente os dados. Você fala para criar uma rota para receber uma nova imagem, caso o usuário tenha alterado ?
> Se eu entendi bem, então eu teria uma rota separada só para editar uma imagem? Não, não precisa de rota separada! > Ai o usuário edita apenas o que ele quer, e envia novamente os dados. Você fala para criar uma rota para receber uma nova imagem, caso o usuário tenha alterado ? Não! O que to dizendo é que vc só deve mudar no banco o que foi modificado! Os campos modificados. Se o user não editou a imagem não muda no banco e na pasta. Se ele editou muda em ambos. Não precisa de outra rota pra isso. Só precisa de uma lógica pra saber o que foi e o que não foi editado
Entendi. Acho que nos campos de texto é facil identificar isso, mas a imagem deve ser um pouco mais complicado. Mas vou tentar encontrar algo a respeito disso. Obrigado.
O melhor mesmo seria conseguir identificar se é a mesma imagem no client-side, pq se não, se eu tiver que verificar no back-end, quer dizer que uma requisição foi feita de qualquer forma. Eu estaria poupando a conexão com o banco, e o tempo de realizar a query, mas a mesma imagem seria enviada de qualquer forma né ?
> O melhor mesmo seria conseguir identificar se é a mesma imagem no client-side, pq se não, se eu tiver que verificar no back-end, isso mesmo
Beleza, muito obrigado, já me deu uma luz sobre o que fazer.

Uma maneira bem simples é salvar o hash da imagem e comparar com o hash da "nova" imagem, mas acredito que uma solução melhor é como o colega sugeriu simplesmente não enviar os dados da imagem todo request!!

Eu costumo resolver isso da seguinte maneira na edição Coloco um thumb, ou o hover (preview no mousehover de um icone, por exemplo) para mostrar a imagem atual, um checkbox "Apagar" (as vezes o usuário so quer remover a imagem sem fazer upload de uma nova) E o input de "file" para a nova imagem, ai se o usuário não mexer nesse campo o backend deve receber esse campo como nulo, ou em branco, ou vazio (nao tenho certeza pois nao uso node no backend) MAS com certeza você consegue identificar que não foi feito "up" da imagem