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
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:
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
shell
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
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"
}]
})
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
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"
}
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.