Nesse post vou te mostrar como criar uma API simples de galeria de imagens usando NestJS no backend, MongoDB para armazenar os dados e Cloudinary para guardar as imagens. Tudo bem direto ao ponto, pra você poder usar como base em outros projetos.
Mas antes de continuar, você deve estar se perguntando:
🤔 “Mas por que usar o Cloudinary? Não dá pra salvar a imagem direto no banco?“
Dá, até dá. Mas na prática… não é uma boa ideia.
Te explico: Salvar imagens no banco (em base64 ou usando GridFS, por exemplo) deixa o banco mais pesado, dificulta backups e não entrega essas imagens de forma rápida e otimizada pro usuário final.
E é aí que entra o Cloudinary (e outros serviços parecidos):
- Ele armazena as imagens de forma eficiente
- Distribui por CDN (ou seja, elas carregam mais rápido em qualquer lugar)
- Permite redimensionar, converter e até aplicar filtros (tudo via URL)
Então, no fim das contas, o banco vai guardar o link da imagem, e quem cuida de mostrar ela de forma rápida é o Cloudinary.
O que vamos construir?
Vamos criar uma API simples para uma galeria de imagens com upload de imagem que será enviada para o Cloudinary e a URL armazenada no MongoDB.
Tecnologias utilizadas:
- NestJS
- MongoDB
- Mongoose
- Multer
- Cloudinary
O que você terá ao final deste post:
Uma API funcional com rotas CRUD completo + upload de imagem integrado
Requisitos:
Antes de tudo, você precisa ter:
- o Node.js instalado na sua máquina. Se ainda não tiver, você pode instalá-lo por aqui: nodejs.org
- Um cluster no MongoDB: MongoDB Atlas
- Uma conta no Cloudinary
Setup inicial do projeto
1. Criando Projeto com NestJS
Caso ainda não tenha a CLI do Nest instalado, é só rodar:
npm i -g @nestjs/cli
E agora sim vamos criar o projeto (que chamarei carinhosamente de “galeria-api”) com o comando:
nest new galeria-api
Escolha o gerenciador de pacote de sua preferência. Eu vou de npm
mesmo. 😅
Quando o processo de instalação terminar, entre na pasta do projeto:
cd galeria-api
2. Instalando as dependências
Hora de instalar as bibliotecas necessárias pra lidar com o banco, o upload e a integração com o Cloudinary.
Vamos começar instalando o Mongoose, o Multer e a integração do Multer com o NestJS:
npm install @nestjs/mongoose mongoose multer @nestjs/platform-express
Agora o Cloudinary, variáveis de ambiente e streamifier:
npm install cloudinary @nestjs/config streamifier
E por último, as tipagens para o Multer (pra deixar o Typescript feliz 😁):
npm install -D @types/multer
Prontinho! Agora já temos o terreno pronto pra começar a construir nossa API.
No próximo passo, vamos conectar nossa aplicação com o MongoDB
Configurar o MongoDB Atlas com Mongoose
Então bora abrir o nosso editor de códigos favorito porque chegou o momento de conectar nossa aplicação ao banco de dados.
1. Criando o arquivo .env
Antes de mais nada, vamos criar um arquivo .env
na raiz do projeto para guardar nossa connection string do MongoDB.
Se você estiver usando o MongoDB Atlas, a conexão vai ser mais ou menos assim:
MONGO_URI=mongodb+srv://<usuário>:<senha>@<cluster>.mongodb.net/<banco>?retryWrites=true&w=majority
Obs: substitua
,
e
pelos dados do seu banco. E em
substitua por galeria-db
⚠️IMPORTANTE: Verifique se o .env
está listado no .gitignore
, afinal, você não vai querer que dados sensíveis vazem por aí, certo? 👀
2. Configurando o módulo do banco
Abra o arquivo app.module.ts
e configure a conexão com o banco:
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [
ConfigModule.forRoot(), // habilita uso do .env
MongooseModule.forRoot(process.env.MONGO_URI || ''), //inicializa a conexão com o MongoDB
],
})
export class AppModule {}
Com isso, sempre que o app for iniciado, ele vai ler a variável MONGO_URI
do .env
e conectar com o banco.
3. Testando a conexão
Vamos testar se tudo o que fizemos até aqui está funcionando. Para isso rode o comando:
npm run start:dev
Se a string estiver correta e o banco estiver ativo, a aplicação deve iniciar sem erros.
No próximo passo, vamos criar a estrutura do nosso primeiro módulo.
Criando o módulo images
Agora que já conectamos com o banco, é hora de começar a construir nosso módulo principal: o módulo de imagens.
Ele será responsável por:
- Receber as imagens via upload
- Enviar para o Cloudinary
- Salvar a URL e outras infos no MongoDB
- Listar e futuramente permitir deletar as imagens
1. Gerando o módulo e o schema
Vamos usar o CLI do NestJS pra gerar o modulo. Se o servidor estiver rodando, dê um CRTL + c
no terminal para interromper a execução e rode o comando:
nest g module images
Repare que o nest criou uma pasta chamada images e dentro dele um arquivo chamado images.module.ts
e já chamou esse módulo no app.module.ts
2. Criando o schema da imagem
O NestJS com Mongoose não usa migrations como em bancos relacionais. Aqui, a gente define a estrutura dos documentos com schemas.
Vamos criar o arquivo src/images/schemas/image.schema.ts
e adicionar o seguinte:
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';
export type ImageDocument = Image & Document;
@Schema({ timestamps: true })
export class Image {
@Prop({ required: true })
url: string;
@Prop()
filename: string;
}
export const ImageSchema = SchemaFactory.createForClass(Image);
Aqui estamos salvando:
- A URL da imagem (vinda do Cloudinary)
- O nome do arquivo (que pode ser útil futuramente)
3. Registrando o schema no módulo
Agora precisamos dizer ao Nest que esse schema existe. No arquivo images.module.ts
, atualize os imports assim:
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { Image, ImageSchema } from './schemas/image.schema';
@Module({
imports: [
MongooseModule.forFeature([{ name: Image.name, schema: ImageSchema }]),
],
})
export class ImagesModule {}
Agora temos o esqueleto do nosso módulo pronto: model configurado, MongoDB preparado pra receber as imagens, e o Nest já sabe como lidar com tudo isso.
No próximo passo, vamos integrar o upload com Multer e fazer com que as imagens sejam enviadas diretamente para o Cloudinary.
Upload de imagens com Multer e Cloudinary
Agora que temos o módulo images
e o schema no jeito, vamos configurar o upload. A ideia aqui é que, quando alguém enviar uma imagem, ela vá direto pro Cloudinary, e a gente salve no MongoDB apenas o link dela (e talvez o nome original, por organização).
1. Configurando o Cloudinary
Após o login na sua conta do Cloudinary, localize e copie essas informações que vamos usar no .env
:
CLOUDINARY_CLOUD_NAME
CLOUDINARY_API_KEY
CLOUDINARY_API_SECRET
No seu .env
, adicione:
CLOUDINARY_CLOUD_NAME=seu-cloud-name
CLOUDINARY_API_KEY=sua-api-key
CLOUDINARY_API_SECRET=sua-api-secret
Agora, crie um novo arquivo chamado cloudinary.provider.ts
dentro da pasta images
// src/images/cloudinary.provider.ts
import { v2 as cloudinary } from 'cloudinary';
export const CloudinaryProvider = {
provide: 'CLOUDINARY',
useFactory: () => {
return cloudinary.config({
cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
api_key: process.env.CLOUDINARY_API_KEY,
api_secret: process.env.CLOUDINARY_API_SECRET,
});
},
};
O código acima cria um provider customizado no Nest para configurar o Cloudinary, que é o serviço onde vamos fazer o upload das nossas imagens, lembra?
2. Criando o serviço de upload
Agora crie um arquivo upload.service.ts
na pasta images
. Ele será responsável por fazer o upload real pro Cloudinary:
// src/images/upload.service.ts
import { Injectable, InternalServerErrorException } from '@nestjs/common';
import {
v2 as cloudinary,
UploadApiResponse,
UploadApiErrorResponse,
} from 'cloudinary';
import * as streamifier from 'streamifier';
@Injectable()
export class UploadService {
async uploadImage(file: Express.Multer.File): Promise<{
secure_url: string;
original_filename: string;
}> {
if (!file?.buffer) {
throw new InternalServerErrorException('Invalid file or missing buffer.');
}
return new Promise((resolve, reject) => {
const stream = cloudinary.uploader.upload_stream(
{ folder: 'galeria' }, //cria uma pasta no Cloudinary
(
error: UploadApiErrorResponse | undefined,
result: UploadApiResponse | undefined,
) => {
if (error) {
console.error('Error on Cloudinary:', error);
return reject(
new InternalServerErrorException('Error uploading image.'),
);
}
if (!result?.secure_url) {
return reject(
new InternalServerErrorException(
'Upload failed. No URL returned.',
),
);
}
return resolve({
secure_url: result.secure_url,
original_filename: result.original_filename,
});
},
);
streamifier.createReadStream(file.buffer).pipe(stream);
});
}
}
3. Atualizando o módulo para incluir os providers
Volte no images.module.ts
e atualize para incluir o provider e o serviço de upload:
import { UploadService } from './upload.service';
import { CloudinaryProvider } from './cloudinary.provider';
@Module({
imports: [
MongooseModule.forFeature([{ name: Image.name, schema: ImageSchema }]),
],
providers: [UploadService, CloudinaryProvider],
})
export class ImagesModule {}
4. Serviço de persistência
Vamos usar novamente a CLI do Nest, mas desta vez para criar o nosso arquivo images.service.ts
sem os arquivos .spec.ts
de teste (não vamos focar nos testes neste post).
nest g service images --no-spec
E por fim, no images.service.ts
, vamos salvar a imagem no banco:
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { Image, ImageDocument } from './schemas/image.schema';
@Injectable()
export class ImagesService {
constructor(
@InjectModel(Image.name) private imageModel: Model<ImageDocument>,
) {}
async create(data: { url: string; filename: string }) {
const createdImage = new this.imageModel(data);
return createdImage.save();
}
}
Obs: nesse exemplo, não usamos DTOs para simplificar o post. Mas em aplicações maiores, é uma boa prática criar DTOs (Data Transfer Objects
) para validar os dados antes de salvar no banco.
5. Configurando o Controller
Bom, hora de criar a rota que recebe o upload!
Para isso, bora usar o CLI do Nest novamente para criar o arquivo images.controller.ts
nest g controller images --no-spec
No images.controller.ts
, importe os utilitários e crie o endpoint:
import {
Body,
Controller,
Post,
UploadedFile,
UseInterceptors,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { ImagesService } from './images.service';
import { UploadService } from './upload.service';
@Controller('images')
export class ImagesController {
constructor(
private readonly imagesService: ImagesService,
private readonly uploadService: UploadService,
) {}
@Post()
@UseInterceptors(FileInterceptor('file'))
async uploadImage(
@UploadedFile() file: Express.Multer.File,
@Body() body: any,
) {
const result = await this.uploadService.uploadImage(file);
return this.imagesService.create({
url: result.secure_url,
filename: body.filename || result.original_filename,
});
}
}
6. Testando o upload
Caso queira testar o que fizemos até aqui, siga esses passos:
- rode o comando no seu terminal para subir o servidor:
npm run start:dev
- abra o seu Postman ou Insomnia
- Coloque o método da requisição para POST
- no campo de URL coloque o endereço:
http://localhost:3000/images
- Em Body, selecione a opção
form-data
- preencha com
- filename(tipo Text) e dê um nome pra sua imagem. No meu caso, coloquei como “teste”
- file(tipo File) faça o upload da imagem
- clique em enviar
-
se tudo ocorrer bem, você terá como resposta um objeto como esse:
{ "url": "https://res.cloudinary.com/dsczhlniq/image/upload/v1743976095/galeria/iebiosewbsspvrqtd3yz.jpg", "filename": "teste", "_id": "67f2f6a0961676b700bfa971", "createdAt": "2025-04-06T21:48:16.145Z", "updatedAt": "2025-04-06T21:48:16.145Z", "__v": 0 }
Se você abrir o MongoDB Atlas, verá que as informações foram salvas, no banco. E no Cloudinary, você encontrará a imagem que acabou de fazer o upload. 🎉
Outras Rotas (CRUD básico)
Além do upload, vamos adicionar outras rotas pra completar o básico da nossa API de galeria. A ideia aqui é simples: listar tudo que foi enviado, buscar por ID (se quiser), atualizar o nome da imagem e também permitir deletar dessa imagem.
1. Listar todas as imagens
No images.service.ts
, adicione:
async findAll(): Promise<Image[]> {
return this.imageModel.find().sort({ createdAt: -1 }).exec();
}
Aqui estamos retornando todas as imagens cadastradas, ordenadas da mais recente para a mais antiga.
E no images.controller.ts
, adicione o seguinte método:
@Get()
async findAll() {
return this.imagesService.findAll();
}
2. Buscar imagem por ID
Se quiser permitir buscar uma imagem específica pelo ID.
No images.service.ts
:
async findOne(id: string): Promise<Image | null> {
return this.imageModel.findById(id).exec();
}
No images.controller.ts
:
@Get(':id')
async findOne(@Param('id') id: string) {
return this.imagesService.findOne(id);
}
3. Atualizando o nome da imagem
Vamos adicionar o método updateFilename()
no images.service.ts
:
async updateFilename(
id: string,
filename: string,
): Promise<{ message: string }> {
const image = await this.imageModel.findById(id);
if (!image) {
throw new NotFoundException('Imagem não encontrada');
}
image.filename = filename;
await image.save();
return {
message: `Nome da imagem atualizado para ${filename} com sucesso!`,
};
}
Em seguida, vamos adicionar a rota para atualizar o nome do arquivo em images.controller.ts
:
@Patch(':id')
async updateFilename(
@Param('id') id: string,
@Body('filename') filename: string,
) {
return this.imagesService.updateFilename(id, filename);
}
4. Deletar uma imagem
Primeiro, crie o método deleteImage()
no upload.service.ts
:
async deleteImage(publicId: string): Promise<void> {
await cloudinary.uploader.destroy(`galeria/${publicId}`);
}
Importante: o public_id
no Cloudinary precisa incluir a pasta se você usou folder: 'galeria'
no upload.
E no images.service.ts
, vamos deletar do banco e do Cloudinary:
async remove(id: string): Promise<any> {
const image = await this.imageModel.findById(id);
if (!image) {
throw new NotFoundException('Imagem não encontrada');
}
// Remove do Cloudinary (usando o nome original como public_id)
await this.uploadService.deleteImage(image.filename);
// Remove do banco
await this.imageModel.findByIdAndDelete(id);
return {
message: `Imagem ${image.filename} removida com sucesso!`,
};
}
Não esqueça de adicionar o uploadservice dentro do contructor:
@Injectable()
export class ImagesService {
constructor(
@InjectModel(Image.name) private imageModel: Model<ImageDocument>,
private readonly uploadService: UploadService,
) {}
...
}
Agora vamos criar a rota que remove a imagem do MongoDB e também do Cloudinary:
No images.controller.ts
:
@Delete(':id')
async remove(@Param('id') id: string) {
return this.imagesService.remove(id);
}
Conclusão
Com poucos arquivos e um pouco de configuração, você já tem uma API funcional de galeria de imagens, pronta pra ser usada em qualquer projeto: um portfólio, um catálogo de produtos, ou o que mais você imaginar.
O combo NestJS + MongoDB + Cloudinary se mostra bem poderoso, e ao mesmo tempo simples de implementar.
Claro que ainda dá pra evoluir bastante: autenticação, upload múltiplo, paginação, tags, categorias, e por aí vai. Mas por hoje, a base está firme.
Código completo no GitHub:
https://github.com/lucianakyoko/galeria-api
E no próximo post…
A gente vai criar o front-end da galeria usando Next.js!
Vamos aprender como:
- Fazer upload de imagens direto pro backend
- Mostrar as imagens da galeria
- Deletar com um clique
- E deixar tudo com um layout lindinho com TailwindCSS ✨
Te espero lá! 🧡