This guide demonstrates how to implement secure and production-ready file upload capabilities using Spring Boot. The solution covers configuration, controller logic, error handling, and basic validation—without relying on external storage services.
Project Setup
Initialize a Spring Boot project (v3.x recommended) with these core dependencies:
- Spring Web
- Spring Boot Configuration Processer (optional, for IDE support)
Configuraton
In application.yml, define the upload directory and add constraints:
app:
storage:
upload-path: ./uploads
spring:
servlet:
multipart:
max-file-size: 10MB
max-request-size: 10MB
Upload Handler Implementation
Create a dedicated service and REST controller that separates concerns and improves testability:
FileStorageService.java
package com.example.upload.service;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.FileSystemUtils;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.nio.file.*;
import java.time.Instant;
import java.util.UUID;
@Service
public class FileStorageService {
private final Path rootLocation;
public FileStorageService(@Value("${app.storage.upload-path}") String uploadPath) {
this.rootLocation = Paths.get(uploadPath);
}
public void init() {
try {
Files.createDirectories(rootLocation);
} catch (IOException e) {
throw new RuntimeException("Failed to initialize upload directory", e);
}
}
public StoredFileInfo store(MultipartFile file) throws IOException {
if (file.isEmpty()) {
throw new IllegalArgumentException("Empty file provided");
}
String originalName = file.getOriginalFilename();
String extension = getFileExtension(originalName);
String uniqueName = UUID.randomUUID() + "-" + Instant.now().getEpochSecond() + extension;
Path destination = rootLocation.resolve(uniqueName);
Files.copy(file.getInputStream(), destination, StandardCopyOption.REPLACE_EXISTING);
return new StoredFileInfo(
uniqueName,
originalName,
Files.size(destination),
Files.getLastModifiedTime(destination).toInstant()
);
}
private String getFileExtension(String filename) {
if (filename == null || filename.lastIndexOf(".") == -1) {
return "";
}
return filename.substring(filename.lastIndexOf("."));
}
}
StoredFileInfo.java (a simple record for response payload):
package com.example.upload.service;
import java.time.Instant;
public record StoredFileInfo(
String storedName,
String originalName,
long sizeBytes,
Instant uploadedAt
) {}
UploadController.java:
package com.example.upload.controller;
import com.example.upload.service.FileStorageService;
import com.example.upload.service.StoredFileInfo;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
@RestController
@RequestMapping("/api/v1/files")
public class UploadController {
private final FileStorageService storageService;
public UploadController(FileStorageService storageService) {
this.storageService = storageService;
this.storageService.init();
}
@PostMapping("/upload")
public ResponseEntity<StoredFileInfo> uploadFile(@RequestParam("data") MultipartFile file) {
try {
StoredFileInfo info = storageService.store(file);
return ResponseEntity.ok(info);
} catch (IllegalArgumentException e) {
return ResponseEntity.badRequest().build();
} catch (IOException e) {
return ResponseEntity.internalServerError().build();
}
}
}
Testing the Endpoint
Use curl to test the upload endpoint:
curl -X POST "http://localhost:8080/api/v1/files/upload" \
-H "Content-Type: multipart/form-data" \
-F "data=@/path/to/image.jpg"
A successful response returns JSON like:
{
"storedName": "a1b2c3d4-1712345678.jpg",
"originalName": "image.jpg",
"sizeBytes": 245760,
"uploadedAt": "2024-04-05T10:21:18.123Z"
}
Key Improvements Over Basic Implementations
- Uses
UUID+ timestamp for safe, collision-resistant filenames - Separates storage logic into a reusable service layer
- Includes built-in size limits via Spring’s multipart configuration
- Returns structured metadata instead of plain strings
- Handles edge cases like empty files and I/O failures explciitly