Spring Boot cookbook · Intermediate
Upload files to S3 from Spring Boot with presigned URLs
Let browsers upload large files directly to S3 while your API controls who can upload what.
Approach
Use the AWS SDK v2 S3Presigner to generate a short-lived presigned PUT URL for a server-generated key and content type. The client uploads directly to S3, then calls your API to confirm; the API verifies the object with headObject before saving metadata.
Solution
@RestController
@RequestMapping("/api/v1/uploads")
class UploadController {
private static final Set<String> ALLOWED = Set.of("image/png", "image/jpeg", "application/pdf");
private final S3Presigner presigner;
private final S3Client s3;
private final String bucket;
UploadController(S3Presigner presigner, S3Client s3, @Value("${app.uploads.bucket}") String bucket) {
this.presigner = presigner; this.s3 = s3; this.bucket = bucket;
}
record UploadRequest(@NotBlank String contentType, @Positive @Max(52_428_800) long size) {}
record UploadResponse(String key, String url) {}
@PostMapping
UploadResponse create(@Valid @RequestBody UploadRequest req, Authentication auth) {
if (!ALLOWED.contains(req.contentType())) throw new ResponseStatusException(HttpStatus.UNSUPPORTED_MEDIA_TYPE);
String key = "users/%s/%s".formatted(auth.getName(), UUID.randomUUID());
PutObjectRequest put = PutObjectRequest.builder().bucket(bucket).key(key)
.contentType(req.contentType()).contentLength(req.size()).build();
PresignedPutObjectRequest presigned = presigner.presignPutObject(p -> p
.signatureDuration(Duration.ofMinutes(5)).putObjectRequest(put));
return new UploadResponse(key, presigned.url().toString());
}
@PostMapping("/complete")
ResponseEntity<Void> complete(@RequestParam String key, Authentication auth) {
if (!key.startsWith("users/" + auth.getName() + "/")) throw new ResponseStatusException(HttpStatus.FORBIDDEN);
HeadObjectResponse head = s3.headObject(h -> h.bucket(bucket).key(key)); // throws if missing
files.register(auth.getName(), key, head.contentLength(), head.contentType());
return ResponseEntity.noContent().build();
}
}
Pitfalls
- Accepting client-chosen object keys.
- Long-lived presigned URLs.
- Streaming large uploads through the app (
MultipartFile) when direct upload is possible.
Variations
- Spring Cloud AWS
S3Templatesimplifies presigning. - S3 event notifications to trigger scanning/thumbnails.
Further reading: AWS SDK for Java: Presigned URLs
Reference: https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/examples-s3-presign.html
More Spring Boot recipes
- Add correlation IDs to Spring Boot logs
- Add retries and a circuit breaker with Resilience4j
- Authenticate machine clients with API keys in Spring Security
- Cache service results in Redis with @Cacheable
- Consume Kafka messages with retries and a dead-letter topic
- Handle concurrent updates with optimistic locking
- Implement soft delete with Hibernate
- Make POST endpoints idempotent in Spring Boot