Building Robust File Upload Handling in Spring Boot Applications

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

Tags: spring-boot file-upload multipart java rest-api

Posted on Mon, 07 Sep 2026 16:08:22 +0000 by mikes1471