Today I want to share a simple approach for up and downloading files with JavaScript (ES6), React and a Spring Boot backend. This example can be used for any common content type like jpg, pdf, txt, HTML, png, etc., and is not limited to a specific content type.
The sample application uses React 17 with Node 15 and Spring Boot 2.5.0 with Java 11. We won't use any additional npm module for up and downloading files at the client-side and just rely on the Fetch API and plain JavaScript. The backend will store the files in an H2 in-memory database and will randomly return a file.
Implementing the React Frontend
The final application will look like the following:
For a quick setup of our React application, we use Facebook's create-react-app
CLI to generate a basic React project.
When running npx create-react-app frontend
, we get a new project with the following package.json
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | { "name": "frontend", "version": "0.1.0", "private": true, "dependencies": { "react": "^17.0.2", "react-dom": "^17.0.2", "react-scripts": "4.0.3" }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test --env=jsdom", "eject": "react-scripts eject" }, "browserslist": { "production": [ ">0.2%", "not dead", "not op_mini all" ], "development": [ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" ] } } |
All the code will reside in the default App.js
file. The component has the following state:
1 2 3 4 5 | state = { file: '', error: '', msg: '' } |
The file
attribute is used for storing the uploaded file, the error
attribute for displaying possible error messages and msg
is used for displaying information messages.
The upload related JSX looks like the following:
1 2 3 4 5 6 7 | <div className="App-intro"> <h3>Upload a file</h3> <h4 style={{color: 'red'}}>{this.state.error}</h4> <h4 style={{color: 'green'}}>{this.state.msg}</h4> <input onChange={this.onFileChange} type="file"></input> <button onClick={this.uploadFile}>Upload</button> </div> |
Any change for the input field will result in setting the uploaded file in the component's state:
1 2 3 4 5 | onFileChange = (event) => { this.setState({ file: event.target.files[0] }); } |
For uploading a file, we check if the user tries to upload an actual file and if the file size exceeds 2MB. Our backend will only allow files with a maximum size of 2MB.
If one of the two validations fail, we set the error
attribute and stop the upload process.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | uploadFile = (event) => { event.preventDefault(); this.setState({error: '', msg: ''}); if(!this.state.file) { this.setState({error: 'Please upload a file.'}) return; } if(this.state.file.size >= 2000000) { this.setState({error: 'File size exceeds limit of 2MB.'}) return; } let data = new FormData(); data.append('file', this.state.file); data.append('name', this.state.file.name); fetch('http://localhost:8080/api/files', { method: 'POST', body: data }).then(response => { this.setState({error: '', msg: 'Sucessfully uploaded file'}); }).catch(err => { this.setState({error: err}); }); } |
For a file with less than 2MB of size, we wrap the file in a FormData
object, so that the request content type will be multipart/form-data
. This is required as otherwise, our backend won't be able to deserialize the request correctly. For sending the request we use the Fetch API and display a message in case of a successful upload.
Downloading a File From the Spring Boot Backend
Next, downloading a random file requires a button and a function:
1 2 3 4 | <div className="App-intro"> <h3>Download a random file</h3> <button onClick={this.downloadRandomImage}>Download</button> </div> |
The downloadRandomImage()
function looks like the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 | downloadRandomImage = () => { fetch('http://localhost:8080/api/files') .then(response => { const filename = response.headers.get('Content-Disposition').split('filename=')[1]; response.blob().then(blob => { let url = window.URL.createObjectURL(blob); let a = document.createElement('a'); a.href = url; a.download = filename; a.click(); }); }); } |
The response from the backend contains the byte array representation of the random file. To read this byte array in full, we make use of the blob()
function of the response. This function returns a Promise that resolves with a Blob
. To extract the correct filename, we access the response header Content-Disposition
and store the filename in a local variable.
As the browsers currently don't support a standard way of downloading files from an AJAX request, we create an object URL for the incoming Blob
and force the browser to download the image with a hidden <a>
HTML element.
That's everything for the frontend. You can find the full App.js
code on GitHub.
Implementing the Spring Boot Backend
Next, the backend is a simple Spring Boot application that contains the dependencies for H2, JPA, and Web:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>de.rieckpil.learning</groupId> <artifactId>spring-boot-uploading-and-downloading-files-with-react</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.5.0</version> <relativePath/> <!-- lookup parent from repository --> </parent> <properties> <java.version>11</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <finalName>app</finalName> <!-- Maven plugins described in one of the following sections --> </build> </project> |
The JPA entity for storing the uploaded files looks like the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | @Entity public class FileEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String fileName; private String contentType; @Lob private byte[] data; // constructors, getters & setters } |
To upload and download files, we provide an API:/api/files
which is accessible via HTTP GET for downloading a random file and HTTP POST for uploading a file.
Moreover, we need to configure CORS to access the Content-Disposition
header in the frontend application:
1 2 3 4 5 6 7 | @RestController @RequestMapping("/api/files") @CrossOrigin(value = {"*"}, exposedHeaders = {"Content-Disposition"}) public class FileBoundary { // code ... } |
To limit the maximum size of the uploaded file, we added the following configuration to the application.properties
file.
1 2 3 | spring.h2.console.enabled=true spring.servlet.multipart.max-file-size=2MB spring.servlet.multipart.max-request-size=10MB |
Uploading a file is straightforward as we receive an object of the type MultipartFile
as @RequestParam("file")
and therefore have access to the filename, the size etc.:
1 2 3 4 5 6 7 8 9 10 11 12 | @PostMapping public ResponseEntity<Void> uploadNewFile(@NotNull @RequestParam("file") MultipartFile multipartFile) throws IOException { FileEntity fileEntity = new FileEntity(multipartFile.getOriginalFilename(), multipartFile.getContentType(), multipartFile.getBytes()); fileEntityRepository.save(fileEntity); URI location = ServletUriComponentsBuilder.fromCurrentRequest().build().toUri(); return ResponseEntity.created(location).build(); } |
Retrieving a Random File from the Database
For downloading a file, we add some logic to select a random file from the database. The endpoint will return a ResponseEntity
of type byte[]
.
The most important part here is setting the correct HttpHeaders
for the browser. As we store the content type of the file in the database, we can add the right Content-Type
header of the response. In addition, we set the Content-Disposition
header to inform the browser about the attached file and its name.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | @GetMapping public ResponseEntity<byte[]> getRandomFile() { long amountOfFiles = fileEntityRepository.count(); Long randomPrimaryKey; if (amountOfFiles == 0) { return ResponseEntity.ok(new byte[0]); } else if (amountOfFiles == 1) { randomPrimaryKey = 1L; } else { randomPrimaryKey = ThreadLocalRandom.current().nextLong(1, amountOfFiles + 1); } FileEntity fileEntity = fileEntityRepository.findById(randomPrimaryKey).get(); HttpHeaders header = new HttpHeaders(); header.setContentType(MediaType.valueOf(fileEntity.getContentType())); header.setContentLength(fileEntity.getData().length); header.set("Content-Disposition", "attachment; filename=" + fileEntity.getFileName()); return new ResponseEntity<>(fileEntity.getData(), header, HttpStatus.OK); } |
The code logic for retrieving a random file is just optional. We use this to show you that this code works for every common content type. Storing the files in a database might also not be the best solution for production.
Serving the React Application from the Spring Boot Backend
To access our frontend application, someone has to serve the static frontend resources (HTML, JavaScript, etc.). Usually, a web server (e.g. Nginx) hosts the static files which are created after you build the frontend application with npm run build
. Nevertheless, for this example, we choose a self-contained system approach.
Our Spring Boot application can also static content next to providing data via REST APIs. To make this work, we have to instruct the application where to find the static content for the React application. Within the build
section of the pom.xml
we can configure this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | <build> <finalName>app</finalName> <resources> <resource> <directory>${project.basedir}/frontend/build</directory> <filtering>false</filtering> <targetPath>public/</targetPath> </resource> <resource> <directory>${project.basedir}/src/main/resources</directory> <filtering>false</filtering> </resource> </resources> <!-- more plugins --> </build> |
This instructs our Spring Boot application to also include the frontend/build
folder for serving public content.
Furthermore to make development more convenient, we're using the frontend-maven-plugin to build the React application with Maven:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | <plugin> <groupId>com.github.eirslett</groupId> <artifactId>frontend-maven-plugin</artifactId> <version>1.12.0</version> <executions> <execution> <id>install node and npm</id> <goals> <goal>install-node-and-npm</goal> </goals> <phase>generate-resources</phase> </execution> <execution> <id>npm install</id> <goals> <goal>npm</goal> </goals> <phase>generate-resources</phase> <configuration> <arguments>install</arguments> </configuration> </execution> <execution> <id>npm build</id> <goals> <goal>npm</goal> </goals> <phase>generate-resources</phase> <configuration> <environmentVariables> <CI>true</CI> </environmentVariables> <arguments>run build</arguments> </configuration> </execution> </executions> <configuration> <workingDirectory>frontend</workingDirectory> <nodeVersion>v15.14.0</nodeVersion> </configuration> </plugin> |
Whenever you now run mvn package
, Maven will first build the frontend and then backend application in one run. This makes development much more convenient and as long your traffic is moderate, serving the frontend using the Spring Boot backend is feasible.
For more React deep-dive have a look at this excellent book and for Spring at the Essential Developer Resources.
You can find the code on GitHub. Detailed instructions to run this example can be found in the README.md
file in the repository.
Keep up-/downloading files with Spring Boot and React,
Phil.