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

Imagem da requisição no Postman

  • 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á! 🧡