<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Arshi Sabah</title>
    <description>The latest articles on DEV Community by Arshi Sabah (@arshisabah).</description>
    <link>https://dev.to/arshisabah</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2888967%2Ffde843f3-48f5-4f61-8d59-7585d95030d3.jpg</url>
      <title>DEV Community: Arshi Sabah</title>
      <link>https://dev.to/arshisabah</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/arshisabah"/>
    <language>en</language>
    <item>
      <title>I Built a QR-Code File Transfer App with Spring Boot and Vanilla JavaScript</title>
      <dc:creator>Arshi Sabah</dc:creator>
      <pubDate>Sun, 04 Oct 2026 03:43:52 +0000</pubDate>
      <link>https://dev.to/arshisabah/i-built-a-qr-code-file-transfer-app-with-spring-boot-and-vanilla-javascript-kem</link>
      <guid>https://dev.to/arshisabah/i-built-a-qr-code-file-transfer-app-with-spring-boot-and-vanilla-javascript-kem</guid>
      <description>&lt;p&gt;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?&lt;/p&gt;

&lt;p&gt;That was the idea behind &lt;strong&gt;QR File Transfer&lt;/strong&gt;, 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.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/arshisabah" rel="noopener noreferrer"&gt;
        arshisabah
      &lt;/a&gt; / &lt;a href="https://github.com/arshisabah/QR-File-Transfer-System" rel="noopener noreferrer"&gt;
        QR-File-Transfer-System
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;QR File Transfer&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;Send a file from one device to another &lt;strong&gt;using only QR codes&lt;/strong&gt;: 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.&lt;/p&gt;
&lt;p&gt;Built with &lt;strong&gt;Spring Boot 3 (Java 17)&lt;/strong&gt;, &lt;strong&gt;MySQL&lt;/strong&gt;, and &lt;strong&gt;vanilla HTML/CSS/JavaScript&lt;/strong&gt;. It is deliberately simple and beginner-friendly, so it works well as a learning project.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;How it works&lt;/h2&gt;
&lt;/div&gt;
&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;
&lt;pre class="notranslate"&gt;&lt;code&gt; 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.&lt;/code&gt;&lt;/pre&gt;…&lt;/div&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/arshisabah/QR-File-Transfer-System" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

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

&lt;p&gt;The backend never sees the file. It only stores metadata: name, size, type, chunk count and status.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Backend:&lt;/strong&gt; Java 17, Spring Boot 3.3, Spring Data JPA, MySQL&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frontend:&lt;/strong&gt; HTML, CSS and vanilla JavaScript, no framework&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;QR:&lt;/strong&gt; &lt;code&gt;qrcode&lt;/code&gt; to generate codes, &lt;code&gt;jsQR&lt;/code&gt; to read them from the camera&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Three REST endpoints are enough:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Endpoint&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;POST&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/transfers&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Create a transfer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GET&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/transfers/{id}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Get transfer details&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PUT&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/transfers/{id}/status&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Update status&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each QR code carries a small JSON payload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;transferId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;chunkIndex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;shownIndex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;totalChunks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;arrayBufferToBase64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Lessons from the bugs
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. My chunks didn't fit in a QR code
&lt;/h3&gt;

&lt;p&gt;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".&lt;/p&gt;

&lt;p&gt;Dropping to &lt;strong&gt;400 bytes&lt;/strong&gt; 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.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Showing each QR code once is fragile
&lt;/h3&gt;

&lt;p&gt;In the first version, each chunk appeared once and the sender stopped. If the camera missed one frame, the transfer was dead.&lt;/p&gt;

&lt;p&gt;The fix: &lt;strong&gt;loop the chunks forever&lt;/strong&gt; until the receiver reports completion. The receiver ignores repeats, so extra passes cost nothing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The same QR code is on screen for ~2 seconds, so it's scanned many times.&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;receivedChunks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chunkIndex&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;receivedChunks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chunkIndex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Phones block the camera on plain HTTP
&lt;/h3&gt;

&lt;p&gt;Browsers only allow camera access on &lt;code&gt;localhost&lt;/code&gt; or HTTPS. Opening the app from a phone over &lt;code&gt;http://192.168.x.x&lt;/code&gt; gave me &lt;em&gt;"Camera API is not available"&lt;/em&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;
&lt;span class="gu"&gt;### 4. Validate everything the camera gives you&lt;/span&gt;

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.

&lt;span class="gu"&gt;## Try it yourself&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
bash&lt;br&gt;
git clone &lt;a href="https://github.com/arshisabah/QR-File-Transfer-System.git" rel="noopener noreferrer"&gt;https://github.com/arshisabah/QR-File-Transfer-System.git&lt;/a&gt;&lt;br&gt;
cd QR-File-Transfer-System/backend&lt;br&gt;
mvn spring-boot:run&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;


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

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>java</category>
      <category>springboot</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
