Dev.to WebDev 🛠 Dev 👁 0 📖 4 min read

I Built a QR-Code File Transfer App with Spring Boot and Vanilla JavaScript

What if you could send a file from one device to another with no cable, no Bluetooth and no cloud upload, using only a screen and a camera? That was the idea behind QR File Transfer, a small project I built with Spring

I Built a QR-Code File Transfer App with Spring Boot and Vanilla JavaScript

What if you could send a file from one device to another with no cable, no Bluetooth and no cloud upload, using only a screen and a camera?

That was the idea behind QR File Transfer, a small project I built with Spring Boot, MySQL and plain JavaScript. The sender's screen plays a stream of QR codes, and the receiver's camera scans them and rebuilds the file.

QR File Transfer

Send a file from one device to another using only QR codes: no cable, no Bluetooth, no cloud upload. The sender screen shows a stream of QR codes, and the receiver's camera scans them and rebuilds the original file.

Built with Spring Boot 3 (Java 17), MySQL, and vanilla HTML/CSS/JavaScript. It is deliberately simple and beginner-friendly, so it works well as a learning project.

How it works

 SENDER (browser)                                        RECEIVER (browser)
 ───────────────                                        ──────────────────
 1. Pick a file
 2. POST /api/transfers  ──►  Spring Boot + MySQL  ◄──  3. Enter transfer ID
    (name, size, type,        (stores metadata only)       GET /api/transfers/{id}
     chunk count)
 4. Split file into 400-byte chunks
 5. Show chunk 0, 1, 2 ... as QR codes  ─── camera ───►  6. Scan, ignore repeats
    (loops until receiver is done)                          collect every chunk
                                                         7. Check file size, rebuild
 9. Sees status COMPLETED, stops  ◄───────────────────  8.
…

How it works

  1. The sender picks a file. The browser asks the backend to create a transfer and gets back an ID like TRF-K7M2QX.
  2. The file is split into 400-byte chunks. Each chunk is base64-encoded, wrapped in JSON, and rendered as a QR code.
  3. The receiver types the transfer ID, starts the camera, and scans the screen.
  4. Once every chunk is in, the receiver checks the rebuilt file size and offers a download.
  5. The receiver marks the transfer COMPLETED, and the sender screen notices and stops.

The backend never sees the file. It only stores metadata: name, size, type, chunk count and status.

The architecture

  • Backend: Java 17, Spring Boot 3.3, Spring Data JPA, MySQL
  • Frontend: HTML, CSS and vanilla JavaScript, no framework
  • QR: qrcode to generate codes, jsQR to read them from the camera

Three REST endpoints are enough:

Method Endpoint Purpose
POST /api/transfers Create a transfer
GET /api/transfers/{id} Get transfer details
PUT /api/transfers/{id}/status Update status

Each QR code carries a small JSON payload:

const payload = JSON.stringify({
  transferId,
  chunkIndex: shownIndex,
  totalChunks: chunks.length,
  data: arrayBufferToBase64(chunk)
});

Lessons from the bugs

1. My chunks didn't fit in a QR code

My first version used 2048-byte chunks. After base64 encoding and the JSON wrapper, that's about 2.8 KB per chunk, which is more than a QR code can hold at a reliable error-correction level. The library simply threw "data too big".

Dropping to 400 bytes produced much smaller codes (QR version 19 in my tests) that phone cameras read quickly. Smaller chunks mean more codes, but a readable code beats a dense one every time.

2. Showing each QR code once is fragile

In the first version, each chunk appeared once and the sender stopped. If the camera missed one frame, the transfer was dead.

The fix: loop the chunks forever until the receiver reports completion. The receiver ignores repeats, so extra passes cost nothing:

// The same QR code is on screen for ~2 seconds, so it's scanned many times.
if (receivedChunks.has(payload.chunkIndex)) return;
receivedChunks.set(payload.chunkIndex, payload.data);

3. Phones block the camera on plain HTTP

Browsers only allow camera access on localhost or HTTPS. Opening the app from a phone over http://192.168.x.x gave me "Camera API is not available".


### 4. Validate everything the camera gives you

A QR code is untrusted input. The receiver checks the transfer ID, rejects out-of-range chunk indexes, and compares the rebuilt size with the size the sender announced before showing the download button.

## Try it yourself


bash
git clone https://github.com/arshisabah/QR-File-Transfer-System.git
cd QR-File-Transfer-System/backend
mvn spring-boot:run




Then open `http://localhost:8080/index.html` to send, and `receiver.html` on a second device to receive. You'll need Java 17+, Maven and MySQL. The README covers the rest.

## Limitations

- It's slow: about 400 bytes every 1.8 seconds, so it suits files of a few KB to maybe a few hundred KB
- Files are capped at 1 MB
- There's no encryption or authentication, so anyone who can see the codes and knows the ID can receive the file

## What's next

- Encrypting chunks with a shared passphrase
- A "missing chunks" view on the receiver
- Expiring old transfers
- Faster transfer with more data per frame

If you have ideas or spot a bug, open an issue or a PR. And if you found this useful, a ⭐ on the repo helps a lot.

**Repo:** https://github.com/arshisabah/QR-File-Transfer-System

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.