303 palavras
2 minutos
MinIO com NestJS

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) e 9001 (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 mc para:
    • Criar os buckets definidos na variável $S3_BUCKET e o bucket posts.
    • Definir esses buckets como públicos para download anônimo (ou seja, qualquer pessoa pode visualizar e baixar os arquivos).

O comando usado no docker-compose para o container minio-init:

mc anonymous set download local/$S3_BUCKET
mc anonymous set download local/posts

Define 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.png

Service#

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

Compartilhar

Se este artigo te ajudou, compartilhe com outras pessoas!

Algumas informações podem estar desatualizadas