Upload de Imagens com Integração Front/Back-End utilizando Stack MERN (Parte 1/2)

Viviane Dias - Sep 23 '19 - - Dev Community

Enquanto desenvolvia um projeto há um tempo atrás, precisei fazer um upload de imagens integrar um formulário de cadastro. Achei pouquíssímos artigos que falassem sobre o assunto e também funcionassem do modo como eu precisava. Por fim, consegui realizar o que inicialmente estava à procura e resolvi escrever esse artigo para compartilhar alguns truques que aprendi no processo. As tecnologias utilizadas nesse tutorial são: NodeJS e MongoDB.

Visão Geral

O objetivo desse artigo é criar um app que consiga criar usuários e depois mostrar seu perfil em uma página. É algo simples, porém com um diferencial: um campo de upload de imagens no front, com um servidor que salva essa imagem no banco de dados e depois consegue buscá-la e retorná-la novamente.

O que vai ser usado aqui não é exclusivo para esse caso (criação de perfil), mas algo mais generalista e que pode ser usado em diferentes situações. Optei por não mostrar apenas as funções que fazem o upload mas sim todo o processo, pois senti que a parte de integração é uma peça fundamental para esse fluxo, e que não aparecia em nenhum artigo.

O método de upload é simples e possivelmente não é a melhor opção para apps que possuem uma grande demanda de upload de imagens, mas se você está fazendo apenas um webapp simples ou projeto pessoal, acredito que esse artigo seja para você! Os passos serão os seguintes:

  • Instalar dependências
  • Scripts
  • Estruturação do projeto
  • Setup MongoDB
  • Editando server.js
  • Modelos
    • Imagens
    • Users
  • Rotas
    • Básico
    • Criar usuário
    • Buscar usuário
    • Upload da imagem
  • Conclusão

Setup do back-end

Primeiro vá até a pasta em que você deseja guardar o projeto, no meu caso:


 shell
cd ~/Documents/programming/photo-upload-tutorial/photo-upload


Enter fullscreen mode Exit fullscreen mode

Próximo passo é inicializar o projeto com o comando npm init. Esse comando irá criar um .json com várias informações sobre o app, mas, principalmente, irá guardar as bibliotecas necessárias para a execução dele posteriormente. Após executar o comando algumas perguntas serão feitas pelo terminal, preencha como você preferir, o meu ficou assim:

Screenshot de uma tela de terminal

Instalar dependências

Depois de disso, instale as dependências que utilizaremos no projeto:


 shell
npm i body-parser cors express helmet mongoose multer --save


Enter fullscreen mode Exit fullscreen mode

 shell
npm i nodemon --save-dev


Enter fullscreen mode Exit fullscreen mode

Explicando um pouco sobre os comandos utilizados:

  • i: Installar
  • --save: Salvar as bibliotecas no arquivo package.json para caso outra pessoa queira também executar esse projeto, todas as bibliotecas usadas já estarão lá.
  • --save-dev: Muito parecido com o anterior, mas nesse caso essa biblioteca só será instalada no modo de desenvolvimento.

Scripts

Agora para os scripts! No momento apenas o script "test" existe. Vamos adicionar mais dois e seu objeto scripts no package.json deve ficar assim:



  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node server.js",
    "server": "nodemon server.js"
  }


Enter fullscreen mode Exit fullscreen mode

Estruturação do projeto

A seguir, crie todas as pastas e arquivos necessários para estruturar o app. Faça de acordo com o esquema a seguir:

photo-upload/
├── client/
├── config/
│ └── db.js
│ └── keys.js
├── models/
│ └── Images.js
│ └── Users.js
├── public/
│ └── uploads/
├── routes/
│ └── api/
│ └── users.js
├── server.js
└── package.json

Setup MongoDB

Nessa parte vamos configurar nosso banco de dados utilizando MongoDB.

Criando o banco de dados

Não pretendo entrar em detalhes sobre a instalação do Mongo, mas uma dica são os tutoriais dos docs que são bem detalhados. Depois de instalado, no terminal execute os seguintes comandos:

Para accesar o terminal do MongoDB


mongo


Enter fullscreen mode Exit fullscreen mode
Criar/acessar o novo banco de dados


use nome-do-banco


Enter fullscreen mode Exit fullscreen mode
Inserindo dados no banco para ele aparecer na listagem


db.nome-do-banco.insert({ "user": "qualquer nome" });


Enter fullscreen mode Exit fullscreen mode
Vendo se o banco aparece na lista de bancos de dados disponíveis


show dbs


Enter fullscreen mode Exit fullscreen mode
Criando um novo usuário

 shell
db.createUser({
  user: "nome do usuário que vai acessar o seu banco de dados",
  pwd: "senha do usuário p/ acessar o banco de dados",
  roles:[{
    role: "readWrite",
    db: "nome do banco de dados que esse usuário terá acesso"
  }]
})


Enter fullscreen mode Exit fullscreen mode

Conectando servidor e banco de dados

Após a criação do banco, precisamos conectá-lo com o servidor. Para isso, vá até o arquivo db.js e insira:



const mongoose = require('mongoose')
const keys = require('./keys')

const MONGO_USERNAME = '[nome do usuário que você criou anteriormente]'
const MONGO_PASSWORD = keys.dbPassword
const MONGO_HOSTNAME = 'localhost'
const MONGO_PORT = '27017'
const MONGO_DB = '[nome do banco de dados criado anteriormente]'

const url = `mongodb://${MONGO_USERNAME}:${MONGO_PASSWORD}@${MONGO_HOSTNAME}:${MONGO_PORT}/${MONGO_DB}`

// Conectar com MongoDB
mongoose
  .connect(url, { useNewUrlParser: true })
  .then(() => console.log('MongoDB Connected'))
  .catch(err => console.log(err))



Enter fullscreen mode Exit fullscreen mode

Note que em MONGO_PASSWORD foi setado uma variável, isso porque não é uma boa prática de segurança publicar senhas de bancos de dados ou API's em repositórios. Em vista disso, setei a senha do banco em um outro arquivo chamado keys.js que não é rastreado pelo git (caso adicionado ao .gitignore) e não sobe para o repositório remoto, ficando apenas no local.



  module.exports = {
    dbPassword: "senha do usuário para acessar o banco",
  }


Enter fullscreen mode Exit fullscreen mode

Editando server.js

Como já criamos a estrutura básica para o nosso app, vá até o server.js e adicione o código base que irá rodar a aplicação, chamar as rotas e o banco de dados, além de setar mais alguns detalhes.



  const express = require('express')
  const bodyParser = require('body-parser')
  const cors = require('cors')
  const helmet = require('helmet')
  const db = require('./config/db')

  const users = require('./routes/api/users')

  // Executando express
  const app = express()

  // Middleware do body parser
  app.use(bodyParser.urlencoded({ extended: false }))
  app.use(bodyParser.json())

  // Adicionando Helmet para melhorar a segurança da API
  app.use(helmet())

  // Habilitando CORS para todos os requests
  app.use(cors())

  // Usar Routes
  app.use('/api/users', users)

  // Acessar arquivos de imagem salvos
  app.use(express.static('public'))

  // Definir porta que o app irá rodar
  const port = process.env.PORT || 5000
  app.listen(port, () => console.log(`Server running on port ${port}`))



Enter fullscreen mode Exit fullscreen mode

Modelos

Precisamos salvar os dados preenchidos no formulário do front-end em algum lugar, para isso setamos Schemas no servidor que vão se conectar e salvar esses dados no banco de dados, para que assim possamos buscá-los posteriormente. Nesse projeto vamos criar dois, um para os usuários e outro para as imagens, para isso, altere os dois arquivos com os conteúdos correspondentes abaixo:

Users



const mongoose = require('mongoose')
const Schema = mongoose.Schema

// Criar Schema
const UserSchema = new Schema({
  name: {
    type: String,
    required: true
  },
  username: {
    type: String,
    required: true,
    unique: true
  },
  imgId: {
    type: Schema.Types.ObjectId,
    required: true
  },
  description: {
    type: String,
    required: true
  },
  location: {
    type: String,
    required: true,
  },
  createdAt: {
    type: Date,
    default: Date.now
  }
})

module.exports = User = mongoose.model('users', UserSchema)


Enter fullscreen mode Exit fullscreen mode

Images



const mongoose = require('mongoose')
const Schema = mongoose.Schema

// Criar Schema relacionado ao Users, através do userId
const ImageSchema = new Schema({
  fieldname: {
    type: String,
    required: true
  },
  originalname: {
    type: String,
    required: true
  },
  encoding: {
    type: String,
    required: true
  },
  mimetype: {
    type: String,
    required: true
  },
  destination: {
    type: String,
    required: true
  },
  filename: {
    type: String,
    required: true
  },
  path: {
    type: String,
    required: true
  },
  size: {
    type: String,
    required: true
  },
  createdAt: {
    type: Date,
    default: Date.now
  }
})

module.exports = Images = mongoose.model('images', ImageSchema)


Enter fullscreen mode Exit fullscreen mode

Rotas

Com os modelos definidos, partiremos para um dos momentos mais cruciais para que a aplicação funcione: A criação das rotas. Nesse projeto vamos criar quatro rotas, e cada uma delas será executada após ser chamada pelo client com um método HTTP específico. Elas estarão dentro do arquivo routes/api/users.js e serão estas:

  • Cria o usuário (POST)
  • Busca o usuário (GET)
  • Salva imagem (POST)
  • Busca imagem (GET)

Básico

Importe todos os arquvios e bibliotecas necessárias no arquvio routes/api/users.js




  const express = require('express')
  const router = express.Router()
  const multer = require('multer')
  const path = require('path')

  // Carregar modelo User
  const User = require('../../models/Users')
  // Carregar modelo Images
  const Images = require('../../models/Images')

  [restante do codigo]

  module.exports = router



Enter fullscreen mode Exit fullscreen mode

Cria o usuário



// @route   POST api/users/register
// @desc    Register user
// @access  Public
router.post('/register', (req, res) => {
  let errors = {}
  User.findOne({ username: req.body.username })
    .then(user => {
      // Caso já exista um usuário com esse username, ele retorna um erro
      if (user) {
        errors.username = 'Esse username já foi usado'
        return res.status(400).json(errors)
      } else {
        const newUser = new User({
          name: req.body.name,
          username: req.body.username,
          imgId: req.body.imgId,
          description: req.body.description,
          location: req.body.location
        })
        newUser.save()
          .then(user => res.json(user))
          .catch(err => {
            // Caso dê um erro ao buscar usuário, a api retorna um erro
            console.log(err);
            res.status(404).json({ user: 'Erro ao salvar usuário' })
          })
      }
    })
    .catch(err => {
      // Caso dê um erro ao buscar usuário, a api retorna um erro
      console.log(err);
      res.status(404).json({ user: 'Erro ao cadastrar usuário' })
    })
})


Enter fullscreen mode Exit fullscreen mode

Busca o usuário



// @route   GET api/users/:username
// @desc    Buscar usuário pelo username
// @access  Public
router.get('/:username', (req, res) => {
  const errors = {}
  User.findOne({ username: req.params.username })
    .then(user => {
      // Caso não haja nenhum usuário com esse username, a api retorna um erro
      if (!user) {
        errors.nousers = 'Esse usuário não existe'
        res.status(404).json(errors)
      }
      // Retorna o usuário
      res.json(user)
    })
    .catch(err => {
      // Caso dê um erro ao buscar usuário, a api retorna um erro
      console.log(err);
      res.status(404).json({ user: 'Erro ao buscar usuário' })
    })
})


Enter fullscreen mode Exit fullscreen mode

Upload da imagem

Para fazer o upload usaremos o multer, um package que facilita esse processo ao fornecer funções prontas que nos ajudam a setar onde essas fotos serão armazenadas, filtros de tipos de extensão aceitas, se queremos apenas um upload por vez ou vários, etc.



const upload = multer({
  storage: storage,
  limits: {
    fileSize: 5000000
  },
  fileFilter: function (req, file, cb) {
    checkFileType(file, cb)
  }
}).single('img')

// Check file type
const checkFileType = (file, cb) => {
  // Allow ext
  const fileTypes = /jpeg|jpg|png|gif/

  // Check ext
  const extname = fileTypes.test(path.extname(file.originalname).toLowerCase())
  // Check mime
  const mimetype = fileTypes.test(file.mimetype)
  if (mimetype && extname) {
    return cb(null, true)
  } else {
    cb('Erro: Insira apenas imagens')
  };
}


Enter fullscreen mode Exit fullscreen mode
Então descrevemos como essa chamada POST irá funcionar


// @route   POST api/users/upload
// @desc    Upload img usuário
// @access  Public
router.post('/upload', (req, res) => {
  upload(req, res, (err) => {
    const errors = {}

    // Caso haja erro no upload, cair aqui
    if (err) {
      errors.upload = err
      return res.status(404).json(errors)
    }

    // Caso o usuário não insira n  enhuma imagem e tente fazer upload, cair aqui
    if (!req.file) {
      errors.upload = 'Insira uma foto de perfil'
      return res.status(400).json(errors)
    }

    // Salvar img
    new Images(req.file)
      .save()
      .then(img => res.json({
        msg: 'Upload da imagem foi bem sucedido!',
        file: `uploads/${img.filename}`,
        id: img._id
      }))
      .catch(() => {
        errors.upload = 'Ocorreu um erro ao fazer o upload da imagem'
        res.status(404).json(errors)
      })
  })
})


Enter fullscreen mode Exit fullscreen mode
Depois, como buscar essa imagem no banco e retonar ela como um .json


// @route   GET api/users/image
// @desc    Buscar img usuário
// @access  Public
router.get('/image/:imgId', (req, res) => {
  const errors = {}
  Images.findById(req.params.imgId)
    .then(img => {
      res.send(img)
    })
    .catch(() => {
      errors.upload = 'Ocorreu um erro ao carregar a imagem'
      res.status(404).json(errors)
    })
})


Enter fullscreen mode Exit fullscreen mode

Conclusão

Pronto! Sua API está pronta e já pode ser testada :) Para rodar o servidor, execute npm run server dentro da pasta do projeto. Para facilitar o teste, vou colocar aqui as chamadas completas, seus métodos HTTP e body (quando for um POST).

Salva imagem

(POST) localhost:5000/api/users/upload

Caso você esteja testando em um programa como o Postman, aqui o key pode ser qualquer um e o value precisa ser uma imagem (file) com um dos tipos que foram setados na função checkFileType(). Depois que o upload for bem sucedido, guarde o id da imagem pois ele vai ser útil para o body do cadastro de usuário, se você quiser testá-lo. Além de um retorno bem sucedido da API, para saber se a imagem foi de fato salva você pode checar se ela está na pasta public/uploads/.

Busca imagem

(GET) localhost:5000/api/users/image/:imgId

Cria o usuário

(POST) localhost:5000/api/users/register


 javascript
{
  "name": "Vivi",
  "imgId": "5d87ace32732d74ba134bca5",
  "description": "Meu nome é Viviane, tenho 21 anos e amo tomar café depois do almoço ;)",
  "location": "São Paulo",
  "username": "vivianedias"
}


Enter fullscreen mode Exit fullscreen mode

Busca o usuário

(GET) localhost:5000/api/users/:username

Antes de concluir gostaria de chamar atenção para uma parte em específico desse código. No arquivo server.js adicionamos uma linha que é crucial para o funcionameto do app:



app.use(express.static('public'))


Enter fullscreen mode Exit fullscreen mode

O que essa linha faz é tornar a pasta /public uma rota estática para que posteriormente possamos consumir as imagens guardadas ali dentro, no front!

Bom, com isso concluímos a primeira parte desse artigo, o front-end sai logo menos :) Espero que vocês tenham gostado, sugestões e dúvidas são bem-vindas, e todo o código desse artigo vai estar aqui.

. . .
Terabox Video Player