Upload de imagens no S3 da AWS com Node.js

✅ Pré-requisitos

Sempre queremos entregar uma melhor experiência para nossa audiência.

Para continuar com a leitura, certifique-se de cumprir os seguintes requisitos:

  • Conhecimento intermediário em JavaScript (Promises e Classes);
  • Conhecimento Básico em Node.js (path, fs) | npm ou yarn para adicionar libs e executar o projeto;
  • Insomnia ou Postman instalados — para testar as rotas;
  • MongoDB instalado, ou ter uma conta no MongoDB Atlas
  • MongoDB Compass ou Robo 3T instalado;
  • Opcional: Conta na AWS para criar um S3, vai ser obrigatório se quiser testar em seu próprio bucket, caso contrário testando localmente já dá para continuar a leitura.

🔰 Introdução

Em 10 passos vamos implementar uma API em Node.js que irá fazer upload de imagens em uma pasta temporária no servidor local e também no serviço S3 (Simple Storage Service) da AWS (Amazon Web Services). Iremos utilizar o MongoDB para armazenar os Posts que contém informações referente a imagem que será salva. Vamos disponibilizar as rotas para que o front end consuma a API.

Node.js na prática - Evento online | Rocketseat
Descubra como usar todo o potencial de Node.js criando um projeto incrível em uma aula 100% prática.

S3 da AWS é um serviço altamente escalável e barato, que nos motiva utiliza-lo para arquivos estáticos. Custa menos dinheiro colocar arquivos no S3 do que no servidor em uma pasta temporária que consome o disco —  Heroku, Digital Ocean, Umbler, entre outros.

Todavia, para um app de teste, claro que compensa manter nesses serviços.

Hoje já vamos aprender como usar S3, pode ser necessário para apps profissionais.

👨‍💻 Bora codar

Vou adicionar os códigos e explicar passo a passo:

Passo 1 - Criando o arquivo package.json

Crie uma pasta no seu workspace chamada backend-upload, abra no seu Editor de códigos — VsCode, e crie o arquivo package.json com a configuração abaixo:

{
  "name": "backend-upload",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "dev": "nodemon src/index.js",
    "start": "node src/index.js"
  },
  "dependencies": {
    "aws-sdk": "^2.390.0",
    "dotenv": "^6.2.0",
    "express": "^4.16.4",
    "mongoose": "^5.4.5",
    "morgan": "^1.9.1",
    "multer": "^1.4.1",
    "multer-s3": "^2.9.0"
  },
  "devDependencies": {
    "cors": "^2.8.5",
    "nodemon": "^1.18.9"
  }
}
package.json

Dependências em produção:

  • aws-sdk = kit de desenvolvimento da AWS, api que utilizamos para lidar com seus serviços;
  • dotenv = lib que carrega as variáveis de ambiente para aplicação — arquivo .env;
  • express = servidor WEB que recebe requisições HTTP;
  • mongoose = Object Data Modeling (ODM) faz o mapeamento do Objeto (Schema) JavaScript em uma Coleção no MongoDB;
  • morgan = middleware que faz o log de requisições HTTP de uma maneira mais amigável para o desenvolvedor;
  • multer = lib que lida com upload de arquivos;
  • multer-s3 = lib que lida com upload de arquivos no S3/AWS.

Dependências em ambiente de desenvolvimento:

  • nodemon = para fazer o reload da aplicação assim que é feita uma alteração no código, exceto arquivos .env.
  • cors = habilita o front end consumir nosso back end (API) que estamos construindo.

scripts:

  • dev = vai ser utilizando apenas em desenvolvimento para o nodemon monitorar as atualizações e fazer reload do app;
  • start = vai ser utilizado em produção, para rodar a aplicação.

Passo 2 - Instalando as dependências

Execute um dos comandos comando abaixo:

npm install
// ou
yarn install

Após a execução do comando, a pasta node_modules que carrega todas as dependências definidas no package.json deve ficar na raiz no seu projeto.

Passo 3 - Criando a variável de ambiente

Crie o arquivo .env e .env-sample na raiz do projeto:

Ambos terão os mesmos conteúdo, porém, você vai preencher conforme as suas credenciais da Amazon que você obterá no Passo 8, por enquanto deixe assim:

.env e .env-sample:

# Se hospedar sua aplicação em algum lugar
# coloque o host aqui:
APP_URL=http://localhost:3000

# s3: para produção
# local: em desenvolvimento
STORAGE_TYPE=local

# Pode ser da sua máquina local ou Mongo Atlas
MONGO_URL=mongodb://localhost:27017/upload

# Pegue esses dados no site S3 AWS
BUCKET_NAME=
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1

O Arquivo .env nunca deve ser enviado para o controle de versão (git), por isso existe o arquivo .env-sample que será enviado somente com as propriedades definidas, sem os valores preenchidos (a menos que os dados não sejam sensíveis).

Passo 4 - Criando o Servidor

Crie uma pasta src na raiz do projeto e dentro dela o arquivo index.js :

src/index.js:

Criando o servidor com Express no Node.js

Node.js na prática - Evento online | Rocketseat
Descubra como usar todo o potencial de Node.js criando um projeto incrível em uma aula 100% prática.

1 -  Carregando para aplicação as variáveis de ambiente;

3 a 7 - Importando os arquivos necessários para criar o servidor;

9 - Criando a variável app que irá conter uma instância do servidor express;

14 a 19 - Criando a conexão com mongoose;

21 -Adicionando o middleware cors, para habilitar à API ser acessada de outra origem como um front end hospedado em outra origem;

22 - Permitindo o servidor express lidar com JSON no body da requisição.

23 - Permitindo o servidor lidar com requisições no padrão URL Encode para facilitar o envio de arquivos;

24 - Utilizando o Morgan no formato dev para fazer o logging das requisições HTTP, de maneira que o(a) dev consiga ler no terminal e entender o que está sendo requisitado — rota, parâmetros e corpo da requisição;

Exemplo:

my/user/workspace/backend-upload
POST /posts 200 1364.073 ms - 315
POST /posts 200 4152.602 ms - 272
GET /posts 200 20.996 ms - 1537
DELETE /posts/5f5bb5d24341822ebff17f2c 200 799.458 ms - -

25 a 28 - Expondo a rota files para servir arquivos estáticos, path é responsável para acessar o caminho na pasta do servidor, __dirname  é o diretório onde o path está sendo executado;

30 - Importa o arquivo de rotas que vamos criar logo em seguida e utiliza como um middleware que recebe as requisições;

32 - Inicia o servidor e disponibiliza a aplicação na porta 3000.

Passo 5 -  Criando as Rotas da aplicação

Crie o arquivo routes.js dentro da pasta src:

src/routes.js

1 - Importa o roteador do express — Router. E atribui na constante router.

2 - Importa o multer que lida com upload de arquivos;

3 - Importa a configuração do multer que iremos criar em seguida;

5 - Importa o model Post que é a representação de um Post do Objeto JavaScript para o mongoDB. Vamos criar o model Post em seguida;

7 a 10 -  Criamos uma rota /post que recebe as requisições GET que irá buscar todos os posts incluídos na base de dados do mongodb na coleção Post e retorna via JSON um array de posts.

13 a 24 -  Criamos uma rota /post que recebe as requisições POST, ela irá receber o arquivo, usando o multer com as configurações. Irá receber um arquivo por vez (single) da requisição no parâmetro file. Será pego os dados da imagem quem vem no atributo file: nome, tamanho do arquivo, nome do arquivo (key) e a URL, com esses dados em mãos iremos criar um Post no banco de dados. Por fim será retornado o JSON com os dados do Post.

26 a 32 - Criamos uma rota /post/:id que que recebe requisições DELETE com um parâmetro id informado na rota. Buscamos o Post pelo ID, removemos usando o await post.remove() e retornamos um status 200.

34 - exportamos esse módulo de rotas que foi importado no arquivo index.js.

Passo 6 - Configurando o Multer para salvar imagem no disco e no S3

Crie a pasta config dentro da pasta src. Nela irá conter o arquivo multer.js com as configurações para salvar imagens na pasta temporária: tmp/uploads e também no bucket do S3.

src/config/multer.js

1 a 5 - Importamos os módulos necessários para lidar com os arquivos enviados na rota /posts via método POST. Tanto os arquivos que serão salvos no servidor local quanto no servidor no S3 da AWS.

7 - Definimos a constante que armazena o valor máximo (2 MB) permitido para uploads de arquivo, abstraindo a conta matemática que converte 2 Megabytes em um valor inteiro, resolvemos o problema de números mágicos, seguindo uma boa prática do Clean Code 😀

9 a 39 - Criamos um objeto storageTypes com duas propriedades: local e s3.

Na propriedade local, utilizamos o multer para para salvar no disco (servidor local) usando a função diskStorage que recebe a propriedade destination que recebe uma função com os três parâmetros (req, file, callback) chamamos o cb passando null para o erro, e o caminho de onde vai ser salvo o arquivo, nesse caso em uma pasta tmp/uploads na raiz do projeto.

A propriedade filename é uma função que recebe os mesmos parâmetros da função anterior, implementamos uma lógica para gerar o nome do arquivo usando um hash para representar o nome do arquivo mais o nome original da imagem. No final é chamada a função cb com dois parâmetros, o erro é null e o segundo é o nome do arquivo: file.key.

Já na propriedade s3 é feita a configuração para salvar os dados no serviço S3. Usando a função multerS3 passamos as configurações:

  • s3 = recebe uma instância do aws.S3
  • bucket = nome do bucket que criamos no site da S3;
  • contentType = serve para abrir a imagem no navegador ao invés de forçar o download;
  • acl = são as permissões, public-read significa que nossa API pode ter acesso ao bucket;
  • key = nome da imagem.

41 a 61 - exportamos o módulo multerConfig passando as propriedades:

  • dest = caminho para a pasta uploads  onde são salvos arquivos temporariamente;
  • storage = onde serão salvos os arquivos, pode ser local ou s3, depende do que foi configurado na variável de ambiente STORAGE_TYPE em ambiente de dev: local e ambiente de produção s3;
  • limits recebe o objeto fileSize com o valor inteiro do tamanho máximo permitido para uploads de arquivo;
  • fileFilter função que filtra os tipos de arquivos que serão aceitos, se o arquivo enviado não estiver incluído no array de arquivos permitidos é lançado um erro para o usuário caso contrário é permitido e continua a operação.

Passo 7 - Criando o Model Post

Dentro de src crie a pasta models com arquivo Post.js

PostSchema será a entidade que representará um Post tanto no Objeto JavaScript quando na coleção do MongoDB. O Mongoose irá fazer essa intermediação, mapeando a relação Objeto ↔  Documento e fornecendo interfaces para um CRUD completo.

src/models/Post.js

1 a 5 - Importamos as bibliotecas necessárias para criar o Schema Post usando mongoose, e a outras bibliotecas que irão lidar com o arquivo para gerar um link completo e deletar as imagens.

7 - Criamos uma constante s3 que recebe uma instância da biblioteca S3 da AWS;

9 a 18 - Definimos o Schema do Post, com os atributos necessários com seus respectivos tipos de dados. Mongoose (ODM) vai fazer com que esse objeto seja compatível com a MongoDB.

20 a 24 - Criamos um lógica que antes (pre) que for salvo algum documento no MongoDB essa função será chamada. Ela basicamente utiliza o this referente ao PostSchema para adicionar na url o endereço do servidor representado pela variável de ambiente APP_URL mais /files/ e o nome do arquivo que está na variável this.key:

Representação:

http://localhost:3000/files/c8a934791778f64650c57dd3c94d8d47-minhafoto.png

26 a 45 - Criamos um lógica antes (pre) que for chamada alguma função de deleção de documentos no MongoDB, a função seja executada.

Se estiver no ambiente STORAGE_TYPE = "s3" a lógica do if é executada, deletando do bucket definido no BUCKET_NAME o arquivo (key) this.key.

Se não estiver no ambiente da S3 então é removido o arquivo do servidor local, usando o file system (fs) do Node.js que vai fazer unlink para remover o arquivo. É utilizado o promisify do Node.js para deixar o código em formato de promises tornando mais elegante e não sendo necessário lidar com callbacks. Para dentro da promise do unlink vai o caminho resolvido via path.resolve do arquivo no servidor.

Passo 8 - Criando um Bucket no S3 da AWS

Deixei os vídeos no momento exato onde começa a falar sobre assunto:

  • Crie um Bucket seguindo os mesmos passos do vídeo.
  • Depois crie um usuário no Identity and Access Management (IAM) da AWS.
  • Dê permissão ao acesso público do bucket (ACL):

Pronto, agora já podemos continuar.

Passo 9 - Iniciando o MongoDB e conferindo as variáveis de ambiente

Antes de iniciar o App, você precisa estar com o MongoDB rodando na máquina, utilize o Docker ou MongoAltas.

Se você não tiver o MongoDB no seu Docker, crie um container usando esse comando:

docker run --name mongo -p 27017:27017 -d -t mongo

Confira se o mongo já está executando:

docker ps

Para saber se o mongo está funcionando, acesse a URL: http://localhost:27017/

Resultado esperado:

It looks like you are trying to access MongoDB over HTTP on the native driver port.

Utilizando o MongoDB Compass ou Robo 3T,  crie um banco de dados chamado upload e uma coleção Post.

Por fim verifique se as variáveis de ambiente estão preenchidas:

.env:

# Se hospedar sua aplicação em algum lugar
# coloque o host aqui:
APP_URL=http://localhost:3000

# s3 para produção
# local em desenvolvimento
STORAGE_TYPE=s3

# Pode ser da sua máquina local ou Mongo Atlas
MONGO_URL=mongodb://localhost:27017/upload

# Pegue esses dados no site S3 AWS
BUCKET_NAME=tutorialuploadfile
AWS_ACCESS_KEY_ID=123JH12K3H1KJ3K1
AWS_SECRET_ACCESS_KEY=123JK12H3JK1+23232321AASD+ggn4qbkgyCI
AWS_DEFAULT_REGION=us-east-1

Se for testar no S3 mantenha o STORAGE_TYPE=s3, senão, deixe STORAGE_TYPE=local

Recomendo testar primeiro localmente.

Passo 10 - Iniciando o servidor e fazendo requisições com Insomnia

Para iniciar a aplicação basta executar o comando:

yarn dev
// ou
npm dev

Abra o Insomnia e crie três rotas:

POST http://localhost:3000/posts
GET http://localhost:3000/posts
DELETE http://localhost:3000/posts/5f5bb5d24341822ebff17f2c

Tente criar, buscar e deletar o Post.

Teste fazer o upload tanto na máquina local quando no S3.

Veja as imagens:

Criando um Post na rota posts com o método POST
Listando os Posts na rota posts com o método GET
Deletando um post pelo seu ID na rota posts com o método DELETE

Imagem que fiz upload no bucket do S3:

Listando uma imagem que fiz upload no bucket do s3

Pronto! App finalizado 👏 👊 💪

Qualquer dúvida pergunte aqui nos comentários ou na comunidade do Discord!

👍 Conclusão

Com poucos arquivos e poucas linhas de código, conseguimos criar um servidor web, expor três rotas para listar, deletar e criar posts. Fazer upload de imagens tanto em disco local quanto no S3 da AWS.

Se ficou alguma dúvida, o Diego Fernandes fez um vídeo explicando como fazer upload de imagens no back end Node.js, confere lá:

E aí, o que achou do post?

Espero que tenha curtido! 💜

O aprendizado é contínuo e sempre haverá um próximo nível! 🚀