File upload is a common requirement in web applications. Users may need to upload profile images, invoices, documents, CSV files, or other media through a REST API.
Spring Boot supports file uploads with HTTP multipart/form-data requests and the MultipartFile interface. A safe implementation validates the file, generates a server-side name, stores it in a controlled location, and returns a useful response to the client.
MultipartFile parameter.Set upload limits in application.properties:
spring.servlet.multipart.enabled=true
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=20MB
# Directory used by this example
app.upload.directory=uploads
Choose limits according to the application’s needs. Limiting request size helps protect the application from unexpectedly large requests.
The service creates the upload directory when needed, generates a random storage name, and prevents path traversal by resolving the file inside the configured directory.
package com.example.fileupload.service;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.UUID;
@Service
public class FileStorageService {
private final Path uploadDirectory;
public FileStorageService(@Value("${app.upload.directory}") String directory) {
this.uploadDirectory = Path.of(directory).toAbsolutePath().normalize();
}
public String store(MultipartFile file) throws IOException {
if (file.isEmpty()) {
throw new IllegalArgumentException("Cannot store an empty file");
}
Files.createDirectories(uploadDirectory);
String originalName = StringUtils.cleanPath(file.getOriginalFilename() == null
? "file"
: file.getOriginalFilename());
String extension = originalName.contains(".")
? originalName.substring(originalName.lastIndexOf('.'))
: "";
String storedName = UUID.randomUUID() + extension.toLowerCase();
Path target = uploadDirectory.resolve(storedName).normalize();
if (!target.getParent().equals(uploadDirectory)) {
throw new IllegalArgumentException("Invalid file path");
}
Files.copy(file.getInputStream(), target, StandardCopyOption.REPLACE_EXISTING);
return storedName;
}
public Path load(String fileName) {
Path file = uploadDirectory.resolve(fileName).normalize();
if (!file.getParent().equals(uploadDirectory)) {
throw new IllegalArgumentException("Invalid file path");
}
return file;
}
}
The controller accepts the field named file. The client must use the same field name when creating the multipart request.
package com.example.fileupload.controller;
import com.example.fileupload.service.FileStorageService;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.nio.file.Files;
@RestController
@RequestMapping("/files")
public class FileController {
private final FileStorageService storageService;
public FileController(FileStorageService storageService) {
this.storageService = storageService;
}
@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<String> upload(@RequestParam("file") MultipartFile file) throws IOException {
String storedName = storageService.store(file);
return ResponseEntity.status(HttpStatus.CREATED).body(storedName);
}
@GetMapping("/{fileName}")
public ResponseEntity<byte[]> download(@PathVariable String fileName) throws IOException {
var path = storageService.load(fileName);
if (!Files.exists(path)) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(Files.readAllBytes(path));
}
}
Start the Spring Boot application and send a multipart request with curl:
curl -X POST http://localhost:8080/files/upload \
-F "file=@C:/files/profile.png"
curl -O http://localhost:8080/files/{stored-file-name}
The upload response contains the generated storage name. Use that value with the download endpoint.
Do not trust the original file name or the client-provided content type. Add validation for:
For images and documents, inspect the file content rather than relying only on the extension. Never execute uploaded files on the server.
Local disk storage is suitable for development and some single-server applications. In a containerized or multi-instance deployment, local files may disappear when an instance is replaced or may not be available to another instance.
For production, consider object storage such as Amazon S3, Azure Blob Storage, or Google Cloud Storage. Store the object key and metadata in the database, and return a controlled download URL.
Return clear status codes for common failures. For example, use 400 Bad Request for invalid files, 413 Payload Too Large when the request exceeds the configured limit, and 404 Not Found when a requested file does not exist.
Spring Boot makes multipart file uploads simple with MultipartFile, but a production-ready implementation must also validate content, protect paths, enforce size limits, and choose storage appropriate for the deployment environment. Keep file metadata in the database and use secure external storage when applications run across multiple instances.
A document management system (DMS) allows authenticated users to upload, download, and manage documents such as PDFs, images, and text files. The application should provide controlled storage, reliable retrieval, authorization, validation, and clear responses for successful and failed operations.
Primary actor: User
Trigger: The user selects a document and submits it through the application.
Alternate flows: An invalid type returns 400 Bad Request, an oversized request returns 413 Payload Too Large, and a storage failure returns a controlled server error.
Primary actor: User
Trigger: The user requests an authorized document.
If the document does not exist, the API returns 404 Not Found. A read failure is logged internally and returned as a safe error response.
Primary actor: Admin
Trigger: An authorized administrator requests document deletion.
The following controller demonstrates upload, download, and delete endpoints. It generates a UUID-based storage name and validates that resolved paths remain inside the upload directory.
package com.javacodex.controller;
import com.javacodex.exception.FileStorageException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.UUID;
@Slf4j
@RestController
@RequestMapping("/api/files")
public class FileUploadController {
private final Path uploadDirectory;
public FileUploadController(@Value("${upload-dir}") String uploadDir) {
this.uploadDirectory = Path.of(uploadDir).toAbsolutePath().normalize();
}
@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<String> handleFileUpload(@RequestParam("file") MultipartFile file) {
if (file.isEmpty()) {
return ResponseEntity.badRequest().body("A file is required");
}
try {
Files.createDirectories(uploadDirectory);
String originalName = file.getOriginalFilename() == null
? "document"
: Path.of(file.getOriginalFilename()).getFileName().toString();
String extension = originalName.contains(".")
? originalName.substring(originalName.lastIndexOf('.')).toLowerCase()
: "";
String storedName = UUID.randomUUID() + extension;
Path target = uploadDirectory.resolve(storedName).normalize();
if (!target.getParent().equals(uploadDirectory)) {
throw new FileStorageException("Invalid storage path");
}
Files.copy(file.getInputStream(), target, StandardCopyOption.REPLACE_EXISTING);
log.info("Document stored with id {}", storedName);
return ResponseEntity.status(HttpStatus.CREATED)
.body("Document uploaded successfully: " + storedName);
} catch (IOException exception) {
throw new FileStorageException("Unable to store the document", exception);
}
}
@GetMapping("/download/{fileName:.+}")
public ResponseEntity<Resource> downloadFile(@PathVariable String fileName) {
try {
Path filePath = uploadDirectory.resolve(fileName).normalize();
if (!filePath.getParent().equals(uploadDirectory)) {
throw new FileStorageException("Invalid document path");
}
Resource resource = new UrlResource(filePath.toUri());
if (!resource.exists() || !resource.isReadable()) {
return ResponseEntity.notFound().build();
}
String contentType = Files.probeContentType(filePath);
MediaType mediaType = contentType == null
? MediaType.APPLICATION_OCTET_STREAM
: MediaType.parseMediaType(contentType);
return ResponseEntity.ok()
.contentType(mediaType)
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + resource.getFilename() + "\"")
.body(resource);
} catch (IOException exception) {
throw new FileStorageException("Unable to read the document", exception);
}
}
@DeleteMapping("/delete/{fileName:.+}")
public ResponseEntity<String> deleteFile(@PathVariable String fileName) {
try {
Path filePath = uploadDirectory.resolve(fileName).normalize();
if (!filePath.getParent().equals(uploadDirectory)) {
throw new FileStorageException("Invalid document path");
}
if (!Files.exists(filePath)) {
return ResponseEntity.notFound().build();
}
Files.delete(filePath);
log.info("Document deleted with id {}", fileName);
return ResponseEntity.ok("Document deleted successfully");
} catch (IOException exception) {
throw new FileStorageException("Unable to delete the document", exception);
}
}
}
package com.javacodex.exception;
public class FileStorageException extends RuntimeException {
public FileStorageException(String message) {
super(message);
}
public FileStorageException(String message, Throwable cause) {
super(message, cause);
}
}
A global handler keeps API responses consistent and prevents implementation details such as absolute filesystem paths from being returned to clients.
package com.javacodex.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.multipart.MaxUploadSizeExceededException;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(FileStorageException.class)
public ResponseEntity<String> handleFileStorageException(FileStorageException exception) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("The document operation could not be completed");
}
@ExceptionHandler(MaxUploadSizeExceededException.class)
public ResponseEntity<String> handleMaxUploadSize() {
return ResponseEntity.status(HttpStatus.PAYLOAD_TOO_LARGE)
.body("The uploaded document is too large");
}
@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleGenericException() {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("An unexpected error occurred");
}
}
upload-dir=/var/lib/javacodex/documents
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=10MB
Use an environment variable or a deployment secret for the production storage path. Ensure the application process has only the minimum directory permissions it needs.
A Spring Boot document management system should combine multipart upload support with authentication, authorization, validation, safe storage names, path protection, controlled downloads, administrative deletion, and consistent exception handling. This design provides a reliable foundation for managing PDFs, images, text files, and other documents while reducing common file-upload security risks.
Build secure file upload APIs with validation, safe storage, and controlled downloads.