Resumo
Recentemente, estive desenvolvendo uma API com NestJS e precisava encontrar uma form de para fazer upload e publicar arquivos estáticos (assets) — como imagens, documentos e outros arquivos — de forma segura e prática.
Depois de fazer uma pesquisa bem rápidinho usando GPT, optei por usar o MinIO, que é um armazenamento de objetos compatível com o protocolo S3 da AWS, mas que roda localmente (ou em qualquer lugar) via Docker.
Neste post vou compartilhar como organizei o projeto, a configuração usando Docker Compose, e como integrei a API com o MinIO para conseguir enviar arquivos e deixá-los públicos para acesso via URL.
Docker
O projeto roda em containers Docker, orquestrados pelo Docker Compose, com os seguintes serviços principais:
- api: backend NestJS, exposto na porta
5400 - minio: servidor de armazenamento de objetos (S3 compatível), nas portas
9000(API) e9001(console web) - postgres: banco de dados PostgreSQL para persistência
- redis: cache e sessão
- minio-init: container especial para inicializar buckets e definir políticas no MinIO via
mc(cliente do MinIO)
services: api: build: context: . dockerfile: ./Dockerfile ports: - "5400:5400" env_file: - ./.env.production depends_on: postgres: condition: service_healthy minio: condition: service_started
minio: image: quay.io/minio/minio:latest command: server /data --console-address ":9001" ports: - "9000:9000" - "9001:9001" env_file: - ./.env.production volumes: - minio_data:/data healthcheck: test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/ready"] interval: 5s timeout: 3s retries: 10
minio-init: image: minio/mc depends_on: minio: condition: service_healthy entrypoint: > /bin/sh -c " mc alias set local http://minio:9000 $MINIO_ROOT_USER $MINIO_ROOT_PASSWORD && mc mb -p local/$S3_BUCKET || true && mc mb -p local/posts || true && mc anonymous set download local/$S3_BUCKET && mc anonymous set download local/posts " env_file: - ./.env.production
volumes: minio_data:- O MinIO roda com o console web acessível em
http://localhost:9001, onde você pode visualizar e gerenciar buckets. - O container minio-init roda após o MinIO estar saudável, e executa comandos via o cliente
mcpara:- Criar os buckets definidos na variável
$S3_BUCKETe o bucketposts. - Definir esses buckets como públicos para download anônimo (ou seja, qualquer pessoa pode visualizar e baixar os arquivos).
- Criar os buckets definidos na variável
O comando usado no docker-compose para o container minio-init:
mc anonymous set download local/$S3_BUCKETmc anonymous set download local/postsDefine os buckets como públicos para download anônimo, permitindo que qualquer pessoa possa acessar os arquivos diretamente via URL.
A URL pública para acessar um arquivo é:
http://localhost:9000/<bucket>/<nome-do-arquivo>Por exemplo:
http://localhost:9000/posts/foto.pngService
Exemplo do serviço que faz a integração usando o pacote @aws-sdk/client-s3
import { randomUUID } from 'node:crypto';import { DeleteObjectCommand, PutObjectCommand, S3Client} from '@aws-sdk/client-s3';import { ConfigService } from '@nestjs/config';
import { Logger } from '@nestjs/common';import { UploadDriver } from '~/app/upload/upload.interface';import { AppConfig } from '~/config';import { AppBucket } from '~/config/storage';
export class S3UploadProvider implements UploadDriver { private readonly s3: S3Client; private readonly bucket: string; private readonly publicUrl: string; private logger = new Logger(S3UploadProvider.name);
constructor(config: ConfigService<AppConfig>, bucket: AppBucket | undefined) { this.bucket = bucket || config.get('S3_BUCKET')!; this.publicUrl = config.get('S3_PUBLIC_URL')!;
this.s3 = new S3Client({ region: config.get('S3_REGION'), endpoint: config.get('S3_ENDPOINT'), credentials: { accessKeyId: config.get('S3_ACCESS_KEY')!, secretAccessKey: config.get('S3_SECRET_KEY')! }, forcePathStyle: true }); }
// Método que separa bucket do prefixo de pastas dentro do bucket, caso exista getKeyAndBucket() { this.logger.debug( `Getting key and bucket for bucket string: ${this.bucket}` ); const [Bucket, ...prefix] = this.bucket.split('/'); const folders = prefix.join('/'); this.logger.debug(`Parsed Bucket: ${Bucket}, Folders: ${folders}`);
return { Bucket, folders }; }
// Método para upload de arquivo async upload(file: Express.Multer.File) { this.logger.debug( `Uploading file: ${file.originalname} to bucket: ${this.bucket}` ); // Gera um ID único concatenando um UUID com o nome original do arquivo const id = randomUUID().concat(file.originalname); this.logger.debug(`Generated unique ID for file: ${id}`);
// Monta a chave (key) no bucket, considerando possíveis subpastas const [bucket, ...folders] = this.bucket.split('/'); const key = folders.length ? `${folders.join('/')}/${id}` : id; this.logger.debug(`Final S3 Key for upload: ${key}`);
// Envia o arquivo para o S3/MinIO await this.s3.send( new PutObjectCommand({ Bucket: bucket, Key: key, Body: file.buffer, ContentType: file.mimetype, CacheControl: 'public, max-age=31536000, immutable' // cache longo para assets estáticos }) );
// Gera a URL pública do arquivo const url = new URL(`${bucket}/${key}`, this.publicUrl).toString();
this.logger.debug(`File uploaded successfully. Accessible at: ${url}`);
return { key, url }; }
// Método para deletar arquivo pelo key async delete(key: string): Promise<void> { this.logger.debug( `Deleting file with key: ${key} from bucket: ${this.bucket}` );
await this.s3.send( new DeleteObjectCommand({ Bucket: this.getKeyAndBucket().Bucket, Key: key }) );
this.logger.debug(`File with key: ${key} deleted successfully.`); }}Source
Se quiser ver o código completo do backend, incluindo todo setup e integrações, tá no meu GitHub:
👉 https://github.com/nathan2slime/apl-atani-backend
Algumas informações podem estar desatualizadas