
Ooopáaa Borapraticar! 👋 Hoje vamos colocar a mão na massa e criar um serviço de upload de arquivos completo, profissional e que você pode usar em qualquer projeto real. Vamos usar Spring Boot com MinIO (que é tipo um S3 da Amazon, mas rodando na sua máquina).
Por que esse projeto é massa? 🤔
Cara, quantas vezes você já precisou implementar upload de arquivos e ficou quebrando a cabeça com onde armazenar, como validar, como organizar? Pois é, esse projeto resolve tudo isso de forma elegante:
- ✅ Upload seguro com validação de tipo e tamanho
- ✅ Armazenamento em MinIO (compatível com S3)
- ✅ API REST completa (upload, listagem, exclusão)
- ✅ Tratamento de erros profissional
- ✅ Docker para facilitar sua vida
- ✅ Documentação completa
O que vamos construir? 🛠️
Um serviço completo com 3 endpoints principais:
- POST /api/v1/files/upload – Para fazer upload dos arquivos
- GET /api/v1/files/list – Para listar todos os arquivos
- DELETE /api/v1/files/delete/{fileName} – Para deletar arquivos
E o melhor: tudo com validação, logs, tratamento de erro e uma arquitetura limpa!
Pré-requisitos (relaxa, é pouca coisa) 📋
Antes de começar, você vai precisar de:
- Java 25+ (se não tem, baixa aí)
- Maven (para gerenciar as dependências)
- Docker (opcional, mas recomendo muito)
- Sua IDE favorita (IntelliJ, VS Code, Eclipse…)
Passo 1: Criando o Projeto Base 🏗️
Vamos começar criando um projeto Spring Boot. Você pode usar o Spring Initializr ou criar na mão mesmo.
Dependências que vamos precisar:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.5.7</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>me.paulschwarz</groupId>
<artifactId>spring-dotenv</artifactId>
<version>4.0.0</version>
</dependency>
</dependencies>
Passo 2: Configurando o MinIO 🐳
Aqui é onde a mágica acontece! Vamos usar Docker para subir o MinIO rapidinho:
# docker-compose.yml
version: '3.8'
services:
minio:
image: minio/minio:latest
container_name: minio-server
ports:
- "9000:9000"
- "9001:9001"
environment:
MINIO_ROOT_USER: minioadmin
MINIO_ROOT_PASSWORD: minioadmin123
command: server /data --console-address ":9001"
volumes:
- minio_data:/data
volumes:
minio_data:
Para subir o MinIO:
docker-compose up -d
Pronto! Agora você tem um servidor de arquivos rodando em http://localhost:9000 e o console em http://localhost:9001.
Passo 3: Configurando as Variáveis de Ambiente 🔧
Crie um arquivo .env na raiz do projeto:
# Configurações da Aplicação APP_NAME=file-upload-service MAX_FILE_SIZE=10MB MAX_REQUEST_SIZE=10MB # Configurações do MinIO MINIO_ENDPOINT=http://localhost:9000 MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin123 MINIO_BUCKET_NAME=images MINIO_REGION=us-east-1
Passo 4: Criando a Configuração do MinIO ⚙️
Agora vamos criar a classe que vai configurar o cliente MinIO:
@Log4j2
@Configuration
public class MinioConfig {
@Value("${minio.endpoint}")
private String endpoint;
@Value("${minio.access-key}")
private String accessKey;
@Value("${minio.secret-key}")
private String secretKey;
@Bean
public MinioClient minioClient() {
log.info("Configuring MinIO client with endpoint: {}", endpoint);
return MinioClient.builder()
.endpoint(endpoint)
.credentials(accessKey, secretKey)
.build();
}
}
Passo 5: Criando os DTOs (Data Transfer Objects) 📦
Vamos criar as classes que vão representar nossas respostas:
// FileResponse.java
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class FileResponse {
private boolean success;
private String message;
private String fileName;
private String fileUrl;
private Long fileSize;
private String contentType;
private String error;
}
// FileListResponse.java
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class FileListResponse {
private boolean success;
private String message;
private List<String> files;
private Integer count;
private String error;
}
// FileDeleteResponse.java
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class FileDeleteResponse {
private boolean success;
private String message;
private String fileName;
private String error;
}
Passo 6: Implementando o Serviço de Upload 🎯
Aqui é onde a coisa fica interessante! Vamos criar o serviço que vai fazer toda a mágica:
@Log4j2
@Service
public class FileUploadServiceImpl implements FileUploadService {
private final MinioClient minioClient;
@Value("${minio.bucket-name}")
private String bucketName;
@Value("${minio.endpoint}")
private String minioEndpoint;
public FileUploadServiceImpl(MinioClient minioClient) {
this.minioClient = minioClient;
}
@Override
public FileResponse uploadImage(MultipartFile file) {
try {
// Validações
validateFile(file);
// Criar bucket se não existir
createBucketIfNotExists();
// Gerar nome único para o arquivo
String fileName = generateUniqueFileName(file.getOriginalFilename());
// Upload do arquivo
minioClient.putObject(
PutObjectArgs.builder()
.bucket(bucketName)
.object(fileName)
.stream(file.getInputStream(), file.getSize(), -1)
.contentType(file.getContentType())
.build()
);
log.info("File {} uploaded successfully to bucket {}", fileName, bucketName);
// Retornar URL do arquivo
String fileUrl = String.format("%s/%s/%s", minioEndpoint, bucketName, fileName);
return FileResponse.builder()
.success(true)
.message("File uploaded successfully")
.fileName(fileName)
.fileUrl(fileUrl)
.fileSize(file.getSize())
.contentType(file.getContentType())
.build();
} catch (Exception e) {
log.error("Error uploading file", e);
return FileResponse.builder()
.success(false)
.error("Error uploading file: " + e.getMessage())
.build();
}
}
private void validateFile(MultipartFile file) {
if (file.isEmpty()) {
throw new IllegalArgumentException("File cannot be empty");
}
if (file.getSize() > 10 * 1024 * 1024) { // 10MB
throw new IllegalArgumentException("File too large. Maximum size: 10MB");
}
String contentType = file.getContentType();
if (!isValidImageType(contentType)) {
throw new IllegalArgumentException("Invalid file type. Accepted types: JPEG, PNG, GIF, WebP");
}
}
private boolean isValidImageType(String contentType) {
return contentType != null && (
contentType.equals("image/jpeg") ||
contentType.equals("image/png") ||
contentType.equals("image/gif") ||
contentType.equals("image/webp")
);
}
private String generateUniqueFileName(String originalFilename) {
String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss"));
String randomString = UUID.randomUUID().toString().substring(0, 8);
String extension = getFileExtension(originalFilename);
return String.format("%s_%s%s", timestamp, randomString, extension);
}
// ... outros métodos
}
Passo 7: Criando o Controller 🎮
Agora vamos expor nossa API:
@Log4j2
@RestController
@RequestMapping("/api/v1/files")
public class FileUploadController {
private final FileUploadService fileUploadService;
public FileUploadController(FileUploadService fileUploadService) {
this.fileUploadService = fileUploadService;
}
@PostMapping("/upload")
public ResponseEntity<FileResponse> uploadFile(@RequestParam("file") MultipartFile file) {
log.info("Upload request received for file: {}", file.getOriginalFilename());
FileResponse response = fileUploadService.uploadImage(file);
if (response.isSuccess()) {
log.info("Upload completed successfully: {}", response.getFileName());
return ResponseEntity.ok(response);
} else {
return ResponseEntity.badRequest().body(response);
}
}
@GetMapping("/list")
public ResponseEntity<FileListResponse> listFiles() {
FileListResponse response = fileUploadService.listFiles();
return ResponseEntity.ok(response);
}
@DeleteMapping("/delete/{fileName}")
public ResponseEntity<FileDeleteResponse> deleteFile(@PathVariable String fileName) {
FileDeleteResponse response = fileUploadService.deleteFile(fileName);
if (response.isSuccess()) {
return ResponseEntity.ok(response);
} else {
return ResponseEntity.badRequest().body(response);
}
}
}
Passo 8: Tratamento de Erros Profissional 🛡️
Vamos criar um handler global para tratar exceções:
@Log4j2
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<FileResponse> handleIllegalArgument(IllegalArgumentException e) {
log.error("Validation error: {}", e.getMessage());
FileResponse response = FileResponse.builder()
.success(false)
.error(e.getMessage())
.build();
return ResponseEntity.badRequest().body(response);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<FileResponse> handleGenericException(Exception e) {
log.error("Unexpected error occurred", e);
FileResponse response = FileResponse.builder()
.success(false)
.error("An unexpected error occurred")
.build();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
}
}
Passo 9: Testando a Aplicação 🧪
Agora vamos testar nossa aplicação:
- Subir o MinIO:
docker-compose up -d - Rodar a aplicação:
mvn spring-boot:run - Testar o upload:
curl -X POST -F "file=@sua-imagem.jpg" http://localhost:8080/api/v1/files/upload - Listar arquivos:
curl http://localhost:8080/api/v1/files/list - Deletar arquivo:
curl -X DELETE http://localhost:8080/api/v1/files/delete/nome-do-arquivo.jpg
Bonus: Coleção do Postman 📮
O projeto já vem com uma coleção do Postman pronta para você importar e testar todos os endpoints sem dor de cabeça!
Dicas Extras para Deixar Ainda Mais Profissional 💡
- Adicione autenticação – JWT, OAuth2, etc.
- Implemente cache – Redis para melhorar performance
- Adicione métricas – Micrometer + Prometheus
- Configure CI/CD – GitHub Actions, Jenkins, etc.
- Adicione testes – JUnit, TestContainers
Conclusão 🎉
E aí, curtiu? Com esse projeto você tem uma base sólida para qualquer sistema que precise de upload de arquivos. O legal é que você pode facilmente adaptar para outros tipos de arquivo, adicionar mais validações, integrar com CDN, etc.
O código completo está disponível no GitHub: https://github.com/devsdofuturobr/files
Clona lá, testa, quebra, melhora e me conta como foi! 😄
Gostou do conteúdo? Se inscreve no canal Borapraticar para mais tutoriais como esse! Lá eu posto sempre conteúdo prático de desenvolvimento, dicas de carreira e muito código na veia! 🚀
#BoraPraticar #SpringBoot #MinIO #Java #Upload #API #Docker #DevLife