DEV Community

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

Posted on

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

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:

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

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:

npm i body-parser cors express helmet mongoose multer --save
npm i nodemon --save-dev

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"
  }

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
Criar/acessar o novo banco de dados
use nome-do-banco
Inserindo dados no banco para ele aparecer na listagem
db.nome-do-banco.insert({ "user": "qualquer nome" });
Vendo se o banco aparece na lista de bancos de dados disponíveis
show dbs
Criando um novo usuário
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"
  }]
})

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))

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",
  }

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}`))

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)

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)

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

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' })
    })
})

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' })
    })
})

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')
  };
}
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)
      })
  })
})
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)
    })
})

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

{
  "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"
}

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'))

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.

Top comments (0)