# Spring Boot REST API tutorial, part 3: Upload and download files

> For the complete documentation index, see [llms.txt](https://www.jetbrains.com/help/idea/2026.3/llms.txt).

* [1: Create and run a Spring Boot project](spring-boot-tutorial-create-project.html)

* [2: Add REST endpoints](spring-boot-tutorial-rest-endpoints.html)

* [3: Upload and download files]()

* [4: Store data in H2 and package a JAR](spring-boot-tutorial-h2-database-jar.html)

In [Spring Boot REST API tutorial, part 2: Add GET and DELETE endpoints](spring-boot-tutorial-rest-endpoints.html), the API worked with photo identifiers and file names. It could describe a photo, but it did not hold an image. In this part, add a page for uploading files, store their bytes in the service, and return them through a download endpoint.

Procedure: Create an HTML page for file uploads

To upload an image, the browser needs a way to select a file and send it to the API. This page provides a file picker and an upload button, then shows the server response in an alert.

* In `src/main/resources/static`, create `upload.html` with the following content:

```HTML
<!DOCTYPE html>
<html lang="en">
<head><meta name="robots" content="noindex"/>
    <meta charset="UTF-8">
    <title>Photo Upload</title>
</head>
<body>

<h2>Photo Upload</h2>

<input id="fileupload" type="file" name="fileupload"/>
<button id="upload-button" onclick="uploadFile()">Upload</button>

<script>
    async function uploadFile() {
        let formData = new FormData();
        formData.append("data", fileupload.files[0]);
        await fetch("http://localhost:8080/photos", {
            method: "POST",
            body: formData
        }).then(result => result.text())
            .then(text => alert(text));
    }
</script>
</body>
</html>
```

Spring Boot serves files in this directory directly, so the page is available at `/upload.html` without a controller.

`FormData` makes the browser send a multipart request, which carries the file bytes together with the file name and content type. Each part has a name: here it is `data`. The upload endpoint uses that same name to find the file in the request. A mismatch there is a common cause of an upload that appears to do nothing.

Procedure: Add file data to the model

The existing `Photo` class has a file name but nowhere to keep the image. Add the bytes and the content type, such as `image/jpeg`, so the download endpoint can later tell the browser what kind of file it is returning.

1. Add fields for the content type and file bytes to `Photo`:

```JAVA
private String contentType;

@JsonIgnore
private byte[] data;
```

The photo listing only needs metadata. Including the bytes would add a long base64 string to each photo in the JSON response. `@JsonIgnore` tells the Jackson JSON converter to skip this field, leaving the identifier, file name, and content type. The bytes remain stored for the download endpoint to return separately.

2. Generate the accessors for both new fields: press `Alt+Insert` (Windows), `⌘ N` (macOS), `⌃ N` (IntelliJ IDEA Classic (macOS)), `⌘ N` (macOS System Shortcuts), `Alt+Insert` (XWin), `Alt+Insert` (GNOME), `Alt+Insert` (KDE), `Alt+Insert` (Emacs), `Alt+Insert` (Sublime Text), `⌘ N` (Sublime Text (macOS)), `Alt+Insert` (Visual Studio), `⌘ ⌃ N` (Visual Studio (macOS)), `Alt+Insert` (NetBeans), `Alt+Insert` (Eclipse), `⌘ N` (Eclipse (macOS)), select Getter and Setter, and select `contentType` and `data`.

3. Remove the constructor from `PhotosService` that adds `hello.jpg`:

```JAVA
public PhotosService() {
    db.put("1", new Photo("1", "hello.jpg"));
}
```

Real uploads replace that example, which has no image data. Also, remove the two-argument constructor from `Photo`:

```JAVA
public Photo(String id, String fileName) {
    this.id = id;
    this.fileName = fileName;
}
```

Keep the no-argument constructor.

Procedure: Handle multipart file uploads

1. In `PhotosService`, add a method that builds and stores a photo from the file data:

```JAVA
public Photo save(String fileName, String contentType, byte[] data) {
    Photo photo = new Photo();
    photo.setId(UUID.randomUUID().toString());
    photo.setFileName(fileName);
    photo.setContentType(contentType);
    photo.setData(data);
    db.put(photo.getId(), photo);
    return photo;
}
```

The server assigns a UUID to identify the photo and returns the stored object so the client receives that identifier for later requests. Photos remain in memory until the application stops; part 4 replaces the map with a database.

2. In `PhotosController`, add the upload endpoint:

```JAVA
@PostMapping("/photos")
public Photo create(@RequestPart("data") MultipartFile file) throws IOException {
    return photosService.save(file.getOriginalFilename(), file.getContentType(), file.getBytes());
}
```

`@RequestPart("data")` selects the file sent by the page. `MultipartFile` provides its original name, content type, and bytes.

The controller extracts these values so the service does not depend on the HTTP-specific `MultipartFile` type. The same service method could accept a file name, content type, and bytes from another source without needing an HTTP request. Reading the bytes can throw `IOException`, which the method declares.

Procedure: Increase the upload size limit

The default file size limit is 1 MB. If you upload a larger file, the request fails, and the [Run](run-tool-window.html) tool window reports `MaxUploadSizeExceededException`. Raise the limit before uploading larger photos. This setting belongs in `application.properties`, where you configure application behavior without changing the Java code.

* Open `src/main/resources/application.properties` and start typing `spring.servlet.multipart`. IntelliJ IDEA completes the configuration keys and shows their descriptions. Set both limits:

```PROPERTIES
spring.servlet.multipart.max-file-size=100MB
spring.servlet.multipart.max-request-size=100MB
```

`max-file-size` limits each file; `max-request-size` limits the whole request, including multipart overhead.

Procedure: Add a file download endpoint

The API can now store an image, but its JSON responses deliberately leave out the bytes. Add a separate controller for downloads. This endpoint returns the image bytes and sets headers that tell the browser what kind of file it received and what to call it.

1. In the `web` package, create a class named `DownloadController` and inject the service:

```JAVA
@RestController
public class DownloadController {

    private final PhotosService photosService;

    public DownloadController(PhotosService photosService) {
        this.photosService = photosService;
    }

}
```

Both controllers receive the same service instance, so they use the same stored photos.

2. In `DownloadController`, add a method that returns the photo bytes as a file download:

```JAVA
@GetMapping("/download/{id}")
public ResponseEntity<byte[]> download(@PathVariable String id) {
    Photo photo = photosService.get(id);
    if (photo == null) {
        throw new ResponseStatusException(HttpStatus.NOT_FOUND);
    }

    HttpHeaders headers = new HttpHeaders();
    headers.setContentType(MediaType.valueOf(photo.getContentType()));
    headers.setContentDisposition(
            ContentDisposition.builder("attachment")
                    .filename(photo.getFileName())
                    .build());

    return new ResponseEntity<>(photo.getData(), headers, HttpStatus.OK);
}
```

`ResponseEntity` combines the bytes, headers, and status. `Content-Type` identifies the file format, and `Content-Disposition` asks the browser to download the file under its original name.

Procedure: Test the file upload and download

1. Restart the application, open `http://localhost:8080/upload.html`, choose an image smaller than 100 MB, and click Upload.

The alert shows an identifier, a file name, and a content type. Copy the identifier from the alert.

2. Open `http://localhost:8080/photos`. The JSON lists the identifier, file name, and content type without the image bytes.

3. Open `http://localhost:8080/download/<id>`, replacing `<id>` with the copied identifier. The browser downloads the image under its original name.

![Browser download history showing ij.png, saved under its original name after opening the /download endpoint with the photo identifier.](https://resources.jetbrains.com/help/img/idea/2026.3/sb_downloaded_image.png)

4. Request an identifier that does not exist. The download endpoint answers with a 404, the same as the JSON endpoints, because both ask the service and both treat `null` the same way.

[Next part](spring-boot-tutorial-h2-database-jar.html)

