<?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: Simon Briggs</title>
    <description>The latest articles on DEV Community by Simon Briggs (@simon_briggs_6607e52a5245).</description>
    <link>https://dev.to/simon_briggs_6607e52a5245</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%2F3816669%2F5882e476-8d57-4350-9270-8b6bdf8dd658.png</url>
      <title>DEV Community: Simon Briggs</title>
      <link>https://dev.to/simon_briggs_6607e52a5245</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/simon_briggs_6607e52a5245"/>
    <language>en</language>
    <item>
      <title>How to Handle PDF Rendering in React Without Killing Performance</title>
      <dc:creator>Simon Briggs</dc:creator>
      <pubDate>Wed, 30 Sep 2026 09:38:32 +0000</pubDate>
      <link>https://dev.to/simon_briggs_6607e52a5245/how-to-handle-pdf-rendering-in-react-without-killing-performance-4n3b</link>
      <guid>https://dev.to/simon_briggs_6607e52a5245/how-to-handle-pdf-rendering-in-react-without-killing-performance-4n3b</guid>
      <description>&lt;p&gt;You add a PDF viewer to your React app, test it with a two-page invoice, and everything feels instant. Then a customer uploads a 300-page scanned contract. The tab freezes, the fan spins up, and your support inbox fills with "the viewer is broken" tickets.&lt;/p&gt;

&lt;p&gt;PDF rendering is one of those features that looks trivial in a demo and turns expensive in production. The good news is that most of the pain comes from a handful of avoidable mistakes. This guide walks through them in the order you are likely to hit them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why PDFs Are Heavy to Render
&lt;/h2&gt;

&lt;p&gt;A PDF is not a document you can simply paint. It is a set of drawing instructions: fonts, vector paths, embedded images, and layout operators. Libraries like PDF.js, which powers &lt;code&gt;react-pdf&lt;/code&gt;, must parse those instructions and rasterize each page onto a &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt;. A dense page with large embedded images can take real time and memory to draw.&lt;/p&gt;

&lt;p&gt;Multiply that by hundreds of pages, plus a high-DPI display, and you can see how a naive "render everything" approach falls over. So the strategy is simple: do less work, do it later, and do it off the main thread.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Set Up the Worker Correctly
&lt;/h2&gt;

&lt;p&gt;PDF.js parses documents in a Web Worker so the UI thread stays responsive. If the worker is misconfigured, the library may fall back to a slower path or throw errors. With &lt;code&gt;react-pdf&lt;/code&gt; on a modern bundler, the setup looks like this:&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;pdfjs&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-pdf&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;pdfjs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GlobalWorkerOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;workerSrc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
 &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pdfjs-dist/build/pdf.worker.min.mjs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;
&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run this once at module level, not inside a component. Also make sure the worker version matches the installed &lt;code&gt;pdfjs-dist&lt;/code&gt; version, since a mismatch is a common source of confusing runtime errors.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Keep Your Props Referentially Stable
&lt;/h2&gt;

&lt;p&gt;This is the most common React-specific mistake. If you pass a new &lt;code&gt;options&lt;/code&gt; object or a new &lt;code&gt;file&lt;/code&gt; object on every render, the &lt;code&gt;Document&lt;/code&gt; component treats it as a different input and reloads the entire PDF.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Bad: new object every render&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Document&lt;/span&gt; &lt;span class="na"&gt;file&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;cMapUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/cmaps/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;

&lt;span class="c1"&gt;// Good: defined once, outside the component&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;cMapUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/cmaps/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;standardFontDataUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/standard_fonts/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Viewer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="p"&gt;})&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;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Document&lt;/span&gt; &lt;span class="na"&gt;file&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you pass a &lt;code&gt;File&lt;/code&gt; or &lt;code&gt;Blob&lt;/code&gt;, keep it in state or a ref. If the value must be computed, wrap it in &lt;code&gt;useMemo&lt;/code&gt;. Your console will often warn you about this, so do not ignore that warning.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Render Only What the User Can See
&lt;/h2&gt;

&lt;p&gt;Rendering all pages up front is the single biggest performance killer. A user looking at page 4 does not need page 240 painted on a canvas. Use a virtualization library such as &lt;code&gt;react-window&lt;/code&gt; so only visible pages exist in the DOM:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;FixedSizeList&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-window&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Document&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Page&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-pdf&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Viewer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;numPages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setNumPages&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Document&lt;/span&gt; &lt;span class="na"&gt;file&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onLoadSuccess&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;numPages&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setNumPages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;numPages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
     &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;FixedSizeList&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;800&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;620&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;itemCount&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;numPages&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;itemSize&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;820&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
       &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;style&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
         &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
           &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Page&lt;/span&gt; &lt;span class="na"&gt;pageNumber&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
         &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
       &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
     &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;FixedSizeList&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
   &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Document&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&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;p&gt;This assumes uniform page sizes. If your documents mix portrait and landscape pages, read each page's dimensions and use a variable-size list instead. Also check the API of the version of &lt;code&gt;react-window&lt;/code&gt; you install, as newer major versions changed the component signatures.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Control Resolution Deliberately
&lt;/h2&gt;

&lt;p&gt;Canvas cost scales with pixel count. On a 3x display, a page rendered at full device pixel ratio can carry roughly nine times the pixels of a 1x render. Most users will not notice the difference between 2x and 3x for reading a document, but your memory usage will.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Page&lt;/span&gt;
 &lt;span class="na"&gt;pageNumber&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
 &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
 &lt;span class="na"&gt;devicePixelRatio&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;devicePixelRatio&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mobile browsers also impose canvas size limits, and exceeding them can produce blank pages or crashes. Capping the ratio is a cheap safeguard. For thumbnail sidebars, go even lower and render at a small width, because a 120px preview does not need a full-resolution canvas.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Turn Off Layers You Do Not Need
&lt;/h2&gt;

&lt;p&gt;Each &lt;code&gt;Page&lt;/code&gt; can render a text layer (for selection and search) and an annotation layer (for links and form fields). Both add DOM nodes and processing time. If your use case is a read-only preview or a thumbnail strip, switch them off:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Page&lt;/span&gt; &lt;span class="na"&gt;pageNumber&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;renderTextLayer&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;renderAnnotationLayer&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep the text layer on only where users truly need to select or search text. A good pattern is to disable it for thumbnails and enable it for the main reading pane.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Load Large Files Smarter
&lt;/h2&gt;

&lt;p&gt;By default, the browser may download the whole file before you see anything useful. PDF.js supports range requests and streaming, which let it fetch only the bytes it needs for the pages being viewed. Your server must return &lt;code&gt;Accept-Ranges&lt;/code&gt; headers and support partial content responses for this to work. You can then tune loading through the &lt;code&gt;options&lt;/code&gt; object:&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;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="na"&gt;disableAutoFetch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// do not prefetch the entire file&lt;/span&gt;
 &lt;span class="na"&gt;rangeChunkSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;65536&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// size of each requested chunk&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test this against your real hosting setup. Some CDNs and proxies strip range support, and then the optimization silently does nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Clean Up After Yourself
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;react-pdf&lt;/code&gt; destroys documents when components unmount, but if you work with &lt;code&gt;pdfjs-dist&lt;/code&gt; directly, cleanup is your job. Call &lt;code&gt;pdf.destroy()&lt;/code&gt; when you are done with a document and &lt;code&gt;page.cleanup()&lt;/code&gt; after rendering pages you will not revisit soon. Also avoid holding references to old canvases in state. Memory leaks in viewers are slow and quiet, and they usually show up only after a user has flipped through several large files in one session.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Sometimes the Problem Is the File, Not the Code
&lt;/h2&gt;

&lt;p&gt;Here is a lesson that took many developers too long to learn: you can optimize the viewer endlessly and still struggle with a 90 MB scan where every page is a full-resolution image. No amount of virtualization makes a bloated file light.&lt;/p&gt;

&lt;p&gt;This is where it helps to treat the document as part of the pipeline. Before wiring up your viewer, try preparing the file itself. If a giant document is only needed in part, split it into smaller sections. If files are oversized, compress them. Doing this in a browser-based toolkit like &lt;a href="https://pdfconveter.com" rel="noopener noreferrer"&gt;PDF Conveter&lt;/a&gt; is a handy way to do it, since it is free and runs in the browser, so there is nothing to install just to get a heavier test file down to a realistic size.&lt;/p&gt;

&lt;p&gt;It is also useful for the reverse case. Need to stress-test your viewer? Build a deliberately heavy fixture by merging several documents, then compare the render behavior before and after compression. Having a small set of light, medium, and heavy PDFs turns performance work from guesswork into a repeatable benchmark. If you ship a product where users upload PDFs, consider guiding them to reduce file size before upload, which saves bandwidth for everyone.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Quick Performance Checklist
&lt;/h2&gt;

&lt;p&gt;Before shipping your viewer, run through this list:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The worker is configured once, with a matching version.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;options&lt;/code&gt; and &lt;code&gt;file&lt;/code&gt; props are stable across renders.&lt;/li&gt;
&lt;li&gt;Pages are virtualized, not all mounted at once.&lt;/li&gt;
&lt;li&gt;Device pixel ratio is capped, and thumbnails use small widths.&lt;/li&gt;
&lt;li&gt;Text and annotation layers are off wherever they are not needed.&lt;/li&gt;
&lt;li&gt;Range requests are verified against your real server or CDN.&lt;/li&gt;
&lt;li&gt;Documents and pages are cleaned up on unmount.&lt;/li&gt;
&lt;li&gt;You have tested with light, medium, and very heavy PDFs, on a mid-range phone as well as your dev machine.
Measure before and after each change with the browser's Performance panel and memory profiler. Assumptions about what is slow are wrong surprisingly often.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Good PDF performance in React is rarely about one clever trick. It is about respecting how expensive rasterization is, then trimming the work at every layer: fewer pages mounted, fewer pixels drawn, fewer features enabled, and lighter files going in. Apply these habits early, and that 300-page contract becomes just another Tuesday instead of a production incident.&lt;/p&gt;

&lt;p&gt;If you are building something with PDFs, start with a lean viewer, build a small set of test files, and only add features once the basics run smoothly.&lt;/p&gt;

</description>
      <category>api</category>
      <category>react</category>
      <category>backend</category>
    </item>
    <item>
      <title>I Tried 3 Python PDF Parsers. The Results Were Surprisingly Different</title>
      <dc:creator>Simon Briggs</dc:creator>
      <pubDate>Tue, 29 Sep 2026 05:17:01 +0000</pubDate>
      <link>https://dev.to/simon_briggs_6607e52a5245/i-tried-3-python-pdf-parsers-the-results-were-surprisingly-different-2pg</link>
      <guid>https://dev.to/simon_briggs_6607e52a5245/i-tried-3-python-pdf-parsers-the-results-were-surprisingly-different-2pg</guid>
      <description>&lt;p&gt;I assumed &lt;code&gt;extract_text()&lt;/code&gt; was just &lt;code&gt;extract_text()&lt;/code&gt;. Pick any library; get the same string back.&lt;/p&gt;

&lt;p&gt;Then I fed one PDF to three parsers and got three different strings. One put my page footer on line two. One wove two columns into a single stream of nonsense. One (all of them, actually) returned nothing at all for a scanned copy.&lt;/p&gt;

&lt;p&gt;Here's what I ran and what I'd pick for which job.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;I built a one-page test PDF with reportlab. It's small, but it packs in the things that usually break parsers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a header ("ACME Corp | Q3 Report | CONFIDENTIAL")&lt;/li&gt;
&lt;li&gt;a two-column body&lt;/li&gt;
&lt;li&gt;a ruled table (5 rows, 4 columns)&lt;/li&gt;
&lt;li&gt;a "Page 1" footer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I also rendered that page to an image and wrapped it in a new PDF, so I'd have a "scanned" version with no text layer.&lt;/p&gt;

&lt;p&gt;Versions: pypdf 5.9.0, pdfplumber 0.11.9, PyMuPDF 1.28.2. Each ran 50 times on the file and was averaged.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pypdf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pdfplumber&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pymupdf&lt;/span&gt;

&lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;test.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pypdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PdfReader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;extract_text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pdfplumber&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;extract_text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pymupdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;get_text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three lines. Three outputs.&lt;/p&gt;

&lt;h2&gt;
  
  
  pypdf: fast enough, order is a gamble
&lt;/h2&gt;

&lt;p&gt;pypdf averaged &lt;strong&gt;2.9 ms&lt;/strong&gt; per run. It's pure Python with no dependencies, which is why it's still my default for merging, splitting, and reading metadata.&lt;/p&gt;

&lt;p&gt;The text came out in the order the PDF writes it to the page, not the order your eyes read it. So the header was followed immediately by "Page 1" (the footer), then the entire left column, then the entire right column.&lt;/p&gt;

&lt;p&gt;That's actually fine for the columns. Each one stayed intact. The footer placement is the annoying part if you're feeding this to a search index or an LLM.&lt;/p&gt;

&lt;p&gt;The table came back with every cell on its own line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Item
Qty
Unit Price
Total
Widget A
10
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can rebuild rows by grouping in fours, but only if you already know the table has four columns.&lt;/p&gt;

&lt;h2&gt;
  
  
  pdfplumber: the layout-aware one (and it shows)
&lt;/h2&gt;

&lt;p&gt;pdfplumber averaged &lt;strong&gt;32.8 ms&lt;/strong&gt;. That's roughly ten times slower than the other two on this page.&lt;/p&gt;

&lt;p&gt;What you get for the wait is text arranged by position. The footer landed at the bottom where it belongs, and the table rows stayed on single lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Widget A 10 $4.50 $45.00
Widget B 3 $12.00 $36.00
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The catch showed up in the body. It merged the two columns line by line, so one line read "Left column text about revenue growth in the Right column text about customer churn and". If you don't check for columns, you'll ship that into production without noticing.&lt;/p&gt;

&lt;p&gt;The fix is to crop first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pdfplumber&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;left&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;pagecrop&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;right&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;crop&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;left&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extract_text&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;right&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extract_text&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Its real strength is &lt;code&gt;extract_tables()&lt;/code&gt;. On my ruled table, it returned a clean list of lists, header row included, empty cells as empty strings. No fiddling.&lt;/p&gt;

&lt;h2&gt;
  
  
  PyMuPDF: the speed pick with a licensing footnote
&lt;/h2&gt;

&lt;p&gt;PyMuPDF was the fastest at &lt;strong&gt;2.0 ms&lt;/strong&gt;. Its default &lt;code&gt;get_text()&lt;/code&gt; output matched pypdf's ordering, footer-after-header quirk included.&lt;/p&gt;

&lt;p&gt;Two things stood out.&lt;/p&gt;

&lt;p&gt;First, &lt;code&gt;find_tables()&lt;/code&gt; returned the same clean table as pdfplumber, at a fraction of the runtime. If you're processing thousands of PDFs, that gap matters.&lt;/p&gt;

&lt;p&gt;Second, &lt;code&gt;get_text(sort=True)&lt;/code&gt; re-sorts blocks by position. On my two-column page, it padded the text with spaces to place the columns side by side, which reads more like the visual page but isn't what you want for clean prose. Sorting helps, but it isn't magic.&lt;/p&gt;

&lt;p&gt;One non-technical note. PyMuPDF is AGPL licensed, with a commercial license available from Artifex. For a personal script, ignore this. If you're shipping it inside a closed-source product, read the license before you build on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The scan test: a three-way tie at zero
&lt;/h2&gt;

&lt;p&gt;All three returned an empty string for the image-only version. Not garbled text. Nothing.&lt;/p&gt;

&lt;p&gt;That's the behavior to build around. These libraries read the text layer that's already inside the file, and a scan has none. They don't do OCR.&lt;/p&gt;

&lt;p&gt;A cheap guard in your pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extract_text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# probably a scan, send to OCR (pytesseract, ocrmypdf, etc.)
&lt;/span&gt;    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without something like this, an empty result looks like "the PDF has no content" instead of "the PDF has no text layer." Those need very different handling.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd actually pick
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Job&lt;/th&gt;
&lt;th&gt;My pick&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Merge, split, metadata&lt;/td&gt;
&lt;td&gt;pypdf&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tables, or mixed layouts&lt;/td&gt;
&lt;td&gt;pdfplumber (slow but forgiving)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Big batches, speed matters&lt;/td&gt;
&lt;td&gt;PyMuPDF&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scans&lt;/td&gt;
&lt;td&gt;none of them; use OCR first&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If I could only install one, I'd go with pdfplumber for anything table-heavy and PyMuPDF for volume.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caveats, so you don't over-trust my results
&lt;/h2&gt;

&lt;p&gt;My test file was generated by reportlab, which writes content in a fairly logical order. A PDF exported from InDesign, a browser print dialog, or an old accounting system can order its text stream very differently. That would change how pypdf and PyMuPDF behave.&lt;/p&gt;

&lt;p&gt;I also only tested a &lt;strong&gt;ruled&lt;/strong&gt; table. Borderless tables (the kind with just whitespace between columns) are much harder for every library here, and you'll likely need to tune settings.&lt;/p&gt;

&lt;p&gt;And timings depend on your machine. Trust the ratios more than the exact milliseconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  A habit that saves me debugging time
&lt;/h2&gt;

&lt;p&gt;Before I write parser code for a new kind of PDF, I look at it as a human first. Is that "table" a real table or a stack of text boxes? Is the text selectable or a scan? Does it have hidden columns?&lt;/p&gt;

&lt;p&gt;Sometimes the quickest way to find out is to push the file through a converter and see what structure survives. I'll run a suspicious PDF through &lt;a href="https://www.pdfconveter.com/pdf-to-excel" rel="noopener noreferrer"&gt;PDF Conveter&lt;/a&gt; to PDF to Excel. If the output comes back as a real grid, I know the layout is table-like. If it comes back as floating text boxes, I know to expect trouble.&lt;/p&gt;

&lt;p&gt;It's also handy when a non-dev teammate just needs the numbers in a spreadsheet, and I'd rather not write a script for a one-off.&lt;/p&gt;

&lt;p&gt;If you want to check how a file behaves before you commit to a parser, PDF Conveter is free to try, no script required.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it on your own files
&lt;/h2&gt;

&lt;p&gt;Run the three-line snippet above on the ugliest PDF you have. Multi-column reports and old invoices are good candidates. The differences will show up fast.&lt;/p&gt;

&lt;p&gt;If you find a case where one of these libraries behaves differently from what I saw, drop it in the comments. I'm curious which PDFs break which parser.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>How to Design an API That Handles Uploads, Conversions, and Cleanup Gracefully</title>
      <dc:creator>Simon Briggs</dc:creator>
      <pubDate>Mon, 28 Sep 2026 11:16:11 +0000</pubDate>
      <link>https://dev.to/simon_briggs_6607e52a5245/how-to-design-an-api-that-handles-uploads-conversions-and-cleanup-gracefully-58ge</link>
      <guid>https://dev.to/simon_briggs_6607e52a5245/how-to-design-an-api-that-handles-uploads-conversions-and-cleanup-gracefully-58ge</guid>
      <description>&lt;p&gt;The first version of every upload endpoint I've seen looks the same. It accepts a file, converts it, and returns the result in one request. It works great on your laptop with a 200 KB test file.&lt;/p&gt;

&lt;p&gt;Then a user uploads a 90 MB scanned document on hotel Wi-Fi, the request hangs for 40 seconds, the load balancer kills it, and your server keeps converting a file nobody is waiting for. Multiply that by a few hundred users and you have an incident.&lt;/p&gt;

&lt;p&gt;Here's how I'd design it instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stop treating it as one request
&lt;/h2&gt;

&lt;p&gt;Upload, conversion, and download have completely different failure profiles. Uploads fail because of networks. Conversions fail because of bad input or heavy CPU. Downloads fail because of expired links. Cramming them into a single synchronous call means one slow step ruins the other two.&lt;/p&gt;

&lt;p&gt;Split them into a job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST   /v1/uploads            -&amp;gt; returns upload URL + upload_id
POST   /v1/jobs               -&amp;gt; { upload_id, target: "docx" } returns 202 + job_id
GET    /v1/jobs/{job_id}      -&amp;gt; status, progress, result URL
DELETE /v1/jobs/{job_id}      -&amp;gt; cancel and clean up now
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key move is returning &lt;strong&gt;202 Accepted&lt;/strong&gt; from job creation. You're telling the client "I got it, it's not done, check back." That one status code removes most timeout headaches, because no HTTP connection has to stay open while the work happens.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the bytes off your API server
&lt;/h2&gt;

&lt;p&gt;If files stream through your app server, your app server becomes a file proxy. That's a bad job for something that should be handling JSON.&lt;/p&gt;

&lt;p&gt;Use presigned URLs instead. The client asks your API for an upload slot, and your API returns a short-lived URL pointing straight at object storage (S3, GCS, R2, whatever you use). The client uploads there directly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"upload_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"upl_8f2c1a"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://storage.example.com/...signature..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expires_in"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"max_bytes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;52428800&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why this works: your API only handles tiny metadata requests, storage handles the heavy transfer, and you can scale each independently. Set the expiry short (5 to 15 minutes is typical) and enforce the size limit in the signed policy, not just in your docs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validate the file, not the filename
&lt;/h2&gt;

&lt;p&gt;An extension is a suggestion. A user can rename &lt;code&gt;payload.exe&lt;/code&gt; to &lt;code&gt;report.pdf&lt;/code&gt; in two seconds.&lt;/p&gt;

&lt;p&gt;After the upload lands, check the actual content before you pass it to a converter. For PDFs, that means looking at the magic bytes (files start with &lt;code&gt;%PDF-&lt;/code&gt;). For other formats, use a library that inspects the signature rather than trusting the &lt;code&gt;Content-Type&lt;/code&gt; header the client sent.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;looks_like_pdf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%PDF-&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then return the right error. Two that get mixed up constantly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;413 Payload Too Large&lt;/strong&gt; when the file exceeds your limit&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;415 Unsupported Media Type&lt;/strong&gt; when the format isn't one you accept&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And add a stable, machine-readable error code in the body (&lt;code&gt;file_too_large&lt;/code&gt;, &lt;code&gt;unsupported_format&lt;/code&gt;, &lt;code&gt;encrypted_file&lt;/code&gt;). Clients shouldn't have to string-match your error messages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design the job states before you write the worker
&lt;/h2&gt;

&lt;p&gt;A job status endpoint is a tiny state machine. Decide the states up front and don't invent new ones later.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;queued -&amp;gt; processing -&amp;gt; succeeded
                     -&amp;gt; failed
                     -&amp;gt; canceled
        (any) -&amp;gt; expired
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two rules that save you pain. First, make states one-directional. A job never goes from &lt;code&gt;failed&lt;/code&gt; back to &lt;code&gt;processing&lt;/code&gt; (a retry creates a new attempt, or a new job). Second, include a reason on failure, not just the word "failed."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"job_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"job_41d9"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"failed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"password_protected"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The PDF is encrypted. Provide a password or upload an unlocked copy."&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That single field turns a support ticket into a self-service fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  Polling, webhooks, or both
&lt;/h2&gt;

&lt;p&gt;Polling is simple and works everywhere. Webhooks are efficient but need retries and signature verification on your side. I'd ship polling first.&lt;/p&gt;

&lt;p&gt;If you do poll, be kind to yourself. Return a &lt;code&gt;Retry-After&lt;/code&gt; header or a suggested interval in the response, and tell clients to back off (1s, 2s, 4s, capped at something like 10s). Otherwise you'll discover what 5,000 clients polling every 200 ms feels like.&lt;/p&gt;

&lt;p&gt;If you add webhooks later, sign the payload with an HMAC and include a timestamp so receivers can reject replays.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make job creation idempotent
&lt;/h2&gt;

&lt;p&gt;Mobile networks drop responses, not just requests. A client sends &lt;code&gt;POST /jobs&lt;/code&gt;, the server creates the job, the response never arrives, and the client retries. Now you've converted the same file twice.&lt;/p&gt;

&lt;p&gt;Accept an &lt;code&gt;Idempotency-Key&lt;/code&gt; header. If you see the same key again within a window (24 hours is common), return the original job instead of creating a new one. It's a small feature that prevents duplicate work and duplicate bills.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cleanup is a feature, not a chore
&lt;/h2&gt;

&lt;p&gt;This is the part most teams skip until the storage bill or a privacy question forces the issue.&lt;/p&gt;

&lt;p&gt;Uploaded files are often sensitive. Contracts, invoices, IDs, medical forms. Keeping them forever is a liability, and keeping them "just in case" isn't a plan. Build expiry into the design from day one:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Lifecycle rules on the bucket.&lt;/strong&gt; Set uploads to auto-delete after a fixed window (say 24 hours). This is your safety net, and it works even if your own cleanup code is broken.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explicit delete after success.&lt;/strong&gt; Once the client has downloaded the result, or the result link expires, remove both input and output.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A sweeper for orphans.&lt;/strong&gt; Uploads that never got a job attached (user closed the tab) still exist. A scheduled task that removes unattached uploads older than an hour catches them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Temp files on workers.&lt;/strong&gt; If your converter writes to local disk, clean up in a &lt;code&gt;finally&lt;/code&gt; block, not just on the happy path. A crashed conversion that leaves 300 MB in &lt;code&gt;/tmp&lt;/code&gt; will fill a container faster than you'd think.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Tell users the retention window in the API docs and in the response (&lt;code&gt;"expires_at": "2026-09-29T10:00:00Z"&lt;/code&gt;). A deletion policy nobody can see doesn't build much trust.&lt;/p&gt;

&lt;h2&gt;
  
  
  Protect the converter itself
&lt;/h2&gt;

&lt;p&gt;Converters chew through untrusted input, so treat them like it.&lt;/p&gt;

&lt;p&gt;Run conversions in a sandboxed worker with a hard timeout (60 to 120 seconds is a reasonable ceiling for documents). Cap memory. Cap page counts if you can. A malformed PDF with thousands of nested objects can eat CPU in ways a normal file never will.&lt;/p&gt;

&lt;p&gt;Also set a per-user or per-IP concurrency limit. One person queuing 400 conversions shouldn't push everyone else's job to the back.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test with ugly files
&lt;/h2&gt;

&lt;p&gt;Your test suite probably contains three clean PDFs. Real users bring:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a scanned document with no text layer&lt;/li&gt;
&lt;li&gt;a file with a password&lt;/li&gt;
&lt;li&gt;a 0-byte upload&lt;/li&gt;
&lt;li&gt;a PDF renamed to &lt;code&gt;.docx&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;a 300-page manual with embedded fonts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keep a small folder of these and run it in CI. Every bad file you collect from production goes into that folder. It's the cheapest regression suite you'll ever build.&lt;/p&gt;

&lt;p&gt;When I want a quick baseline for what a "correct" conversion should look like, I run the same file through a browser tool first and compare against my pipeline's output. &lt;a href="https://pdfconveter.com/" rel="noopener noreferrer"&gt;PDF Conveter&lt;/a&gt; is handy for that, since it's free and needs no setup. If your API's output for a table-heavy PDF looks worse than a reference conversion, you've found something worth debugging.&lt;/p&gt;

&lt;h2&gt;
  
  
  A quick checklist before you ship
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Uploads go directly to storage, with size limits in the signature&lt;/li&gt;
&lt;li&gt;Job creation returns 202 and an ID&lt;/li&gt;
&lt;li&gt;File content is validated, not just the extension&lt;/li&gt;
&lt;li&gt;Errors have stable codes and human-readable messages&lt;/li&gt;
&lt;li&gt;Job creation accepts an idempotency key&lt;/li&gt;
&lt;li&gt;Every stored file has an expiry, and you can point to where it's enforced&lt;/li&gt;
&lt;li&gt;The converter has timeouts, memory caps, and per-user limits&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is exotic. It's mostly about admitting up front that files are messy, networks are flaky, and users will do things you didn't plan for. Design for that, and the "graceful" part mostly takes care of itself.&lt;/p&gt;

&lt;p&gt;If you're building something similar, I'd like to hear what your ugliest test file was. Mine was a 40 MB scan of a fax of a printout, and it taught me more than any spec did&lt;/p&gt;

</description>
      <category>api</category>
      <category>backend</category>
      <category>webdev</category>
      <category>architecture</category>
    </item>
    <item>
      <title>How to Handle User Data Deletion Properly in a File-Processing App</title>
      <dc:creator>Simon Briggs</dc:creator>
      <pubDate>Sat, 26 Sep 2026 04:46:51 +0000</pubDate>
      <link>https://dev.to/simon_briggs_6607e52a5245/how-to-handle-user-data-deletion-properly-in-a-file-processing-app-28l7</link>
      <guid>https://dev.to/simon_briggs_6607e52a5245/how-to-handle-user-data-deletion-properly-in-a-file-processing-app-28l7</guid>
      <description>&lt;p&gt;A user clicks "Delete Account." Your database says the row is gone. Three weeks later, a support ticket arrives: "Why can I still find my old resume.pdf in a Google search result?"&lt;/p&gt;

&lt;p&gt;This happens more often than most engineering teams admit. File-processing apps have a deletion problem that regular CRUD apps don't: the data isn't just a table row. It's a file sitting in object storage, a thumbnail in a CDN cache, a backup snapshot from last Tuesday, and maybe a copy in a processing queue that hasn't run yet. Deleting the record is the easy 10%. Deleting the actual data is the hard 90%.&lt;/p&gt;

&lt;p&gt;If you're building anything that touches uploaded files, whether it's a PDF converter, an image editor, a document scanner, or a media pipeline, here's what proper deletion actually requires.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "DELETE FROM users WHERE id = ?" Isn't Deletion
&lt;/h2&gt;

&lt;p&gt;Most developers learn database deletion before they learn systems thinking. So the instinct is to treat account deletion as a database problem. But in a file-processing app, the database is just an index. It points to where things live. Deleting the pointer doesn't delete the destination.&lt;/p&gt;

&lt;p&gt;Consider a typical upload-process-download flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;User uploads a file to object storage (S3, GCS, or similar)&lt;/li&gt;
&lt;li&gt;Your app processes it and stores a result, maybe in a different bucket&lt;/li&gt;
&lt;li&gt;A thumbnail or preview gets generated and cached at the edge&lt;/li&gt;
&lt;li&gt;Metadata about the operation lands in your database and possibly your logs&lt;/li&gt;
&lt;li&gt;A backup job later that night copies everything, including the file, into a snapshot
Now the user asks you to delete their data. If you only touch step 4, you've deleted the map, not the territory. The file is still sitting in storage, still reachable if someone has the direct URL, and still present in that night's backup for as long as your retention policy holds it.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Four Places Data Actually Lives
&lt;/h2&gt;

&lt;p&gt;Before writing any deletion logic, map out where a single file's data physically exists. In most systems, it's some combination of:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Primary storage.&lt;/strong&gt; The actual file object, wherever you store it after upload.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Derived artifacts.&lt;/strong&gt; Thumbnails, converted formats, compressed versions, OCR text extracts. If your app converts a file from one format to another, you likely have both the original and the output sitting somewhere.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cache and CDN layers.&lt;/strong&gt; If files are served through a CDN, deleting the origin file doesn't automatically purge edge caches. Someone with a cached URL might retrieve it for hours or days after "deletion."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Backups and logs.&lt;/strong&gt; This is the one teams forget. Your nightly backup rotation might retain deleted files for 30, 60, or 90 days depending on your retention window. Application logs sometimes contain file paths, filenames, or even payload snippets from debugging.&lt;/p&gt;

&lt;p&gt;A deletion request that only clears the first item on this list isn't deletion. It's an illusion of deletion, and it becomes a real liability under regulations like GDPR's right to erasure or CCPA's deletion rights, both of which expect data to actually be gone within a defined timeframe, not just hidden from the UI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Designing a Deletion Flow That Actually Works
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Treat deletion as a job, not a click handler.&lt;/strong&gt; Don't try to synchronously delete everything the moment a user clicks a button. Object storage calls can fail, CDN purges can be rate-limited, and backup systems often can't be touched on demand. Queue the deletion as a background job with retries, and give the user immediate confirmation that the request was received rather than promising instant completion.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build a deletion manifest per file, not per user.&lt;/strong&gt; When a file is uploaded, track every artifact it spawns: the original, the converted output, the thumbnail, the cache key. Store this as a small structured record tied to the file's ID. When deletion runs, you're not guessing what might exist, you're working off a checklist.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Separate soft delete from hard delete.&lt;/strong&gt; A soft delete (a flag that hides the file from the user's dashboard) is fine as a first step and even useful for accidental deletion recovery. But it must have a hard deadline. If your soft delete window is 7 days, the hard delete job must actually run on day 7, permanently removing the underlying object, not just flipping another flag.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Purge caches explicitly.&lt;/strong&gt; If you use a CDN, issue an invalidation request as part of the deletion job. Don't assume TTL expiry is good enough, especially for sensitive documents like IDs, contracts, or financial statements.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Set backup retention with deletion in mind.&lt;/strong&gt; You generally can't reach into last week's backup and surgically remove one file. The realistic approach is to keep backup retention short and clearly documented, and to make sure your privacy policy accurately reflects that deleted files may persist in backups for up to that retention period. Honesty here matters more than a technically perfect solution.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Log the deletion, not the deleted content.&lt;/strong&gt; Keep an audit trail that says "file X was deleted on this date, triggered by this user action," but make sure your logs never captured the file's contents or sensitive metadata in the first place. If they already did, redact retroactively.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where This Gets Genuinely Difficult
&lt;/h2&gt;

&lt;p&gt;Multi-tenant systems and processing pipelines add complexity. If a file passes through a third-party API for conversion or OCR, you need to confirm what that provider does with the data after processing, and whether they offer their own deletion endpoint. A deletion flow that stops at your own infrastructure but ignores a downstream vendor is incomplete.&lt;/p&gt;

&lt;p&gt;This is actually one of the reasons &lt;a href="https://www.pdfconveter.com/" rel="noopener noreferrer"&gt;PDF Conveter&lt;/a&gt;has some conversion features around browser-based processing rather than uploading files to a server at all. For most of its conversion and editing tools, the file is processed locally in the user's browser and never persisted server-side, and files that do pass through are auto-deleted rather than retained. It sidesteps the entire "did we really delete it everywhere" problem for a large share of common file operations, because there's no long-term server copy to hunt down in the first place. It's a useful pattern to study if you're deciding between server-side and client-side processing for your own app: sometimes the simplest way to guarantee deletion is to not create a permanent copy at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Practical Checklist
&lt;/h2&gt;

&lt;p&gt;Before shipping a "Delete My Data" feature, confirm:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every file has a documented list of derived artifacts and where they live&lt;/li&gt;
&lt;li&gt;Deletion is a queued job with retry logic, not a synchronous call&lt;/li&gt;
&lt;li&gt;CDN cache purging is part of the deletion flow, not an afterthought&lt;/li&gt;
&lt;li&gt;Backup retention windows are documented and disclosed to users&lt;/li&gt;
&lt;li&gt;Downstream third-party processors have their own deletion path accounted for&lt;/li&gt;
&lt;li&gt;Logs don't retain file content, only metadata about the deletion event itself
Data deletion isn't a feature you bolt on at the end. It's a property of how you architect file handling from the start. The apps that get this right treat every file upload as a liability with a lifecycle, not just an asset to serve. Building that mindset early saves a lot of uncomfortable support tickets later.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>data</category>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>How to Document an API So Other Developers Actually Use It Correctly</title>
      <dc:creator>Simon Briggs</dc:creator>
      <pubDate>Fri, 25 Sep 2026 04:49:47 +0000</pubDate>
      <link>https://dev.to/simon_briggs_6607e52a5245/how-to-document-an-api-so-other-developers-actually-use-it-correctly-2mgo</link>
      <guid>https://dev.to/simon_briggs_6607e52a5245/how-to-document-an-api-so-other-developers-actually-use-it-correctly-2mgo</guid>
      <description>&lt;p&gt;Here's an uncomfortable truth: most API documentation is written for the person who built the API, not the person trying to use it. That mismatch is why developers end up in Stack Overflow threads, Discord channels, and support tickets asking questions the docs were supposed to answer.&lt;/p&gt;

&lt;p&gt;Good documentation isn't a formality you tack on after shipping. It's part of the product. A well-documented API gets adopted faster, generates fewer support requests, and earns trust from developers who've been burned before by docs that lied, omitted, or assumed too much. Let's break down what separates documentation that actually gets used correctly from documentation that just exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start With the Developer's First Five Minutes
&lt;/h2&gt;

&lt;p&gt;Every developer evaluating your API goes through the same mental checklist in the first few minutes: What does this do? How do I authenticate? Can I make one successful call right now?&lt;/p&gt;

&lt;p&gt;If your documentation can't answer those three questions on the landing page, you've already lost momentum. This is why a strong "Quickstart" section matters more than a complete reference. Give developers a working example with real (or clearly fake but realistic) values they can copy, paste, and run immediately. Skip the theory. Show the curl command. Show the response. Let them feel a win before you explain anything else.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.example.com/v1/orders &lt;span class="se"&gt;\&lt;/span&gt;
 &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_API_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
 &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
 &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"product_id": "SKU123", "quantity": 2}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A quickstart like this does more for adoption than three paragraphs explaining your architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Document the Contract, Not Just the Endpoint
&lt;/h2&gt;

&lt;p&gt;Endpoints are easy to document. Contracts are where most APIs fall short. A contract includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Required versus optional parameters, and what happens when optional ones are omitted&lt;/li&gt;
&lt;li&gt;Data types, including whether a number is an integer or a float, and whether a string has length limits&lt;/li&gt;
&lt;li&gt;What a successful response looks like, in full, not truncated&lt;/li&gt;
&lt;li&gt;Every possible error response, with actual status codes and error bodies, not just "returns an error if something goes wrong"
That last point matters more than developers give it credit for. Error handling is where integrations break in production. If your docs say "returns a 400 on invalid input" but don't specify the error format, developers write brittle error handling that fails the first time your API returns something unexpected.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Show Real Examples, Not Placeholder Noise
&lt;/h2&gt;

&lt;p&gt;Nothing signals a rushed documentation effort faster than seeing &lt;code&gt;foo&lt;/code&gt;, &lt;code&gt;bar&lt;/code&gt;, and &lt;code&gt;example_value_1&lt;/code&gt; scattered through your sample requests. Realistic examples do two things: they demonstrate expected formats (is a date &lt;code&gt;2026-09-25&lt;/code&gt; or &lt;code&gt;09/25/2026&lt;/code&gt;?), and they build confidence that the API works the way it claims.&lt;/p&gt;

&lt;p&gt;If your API deals with orders, use a believable order. If it deals with user profiles, use a believable name and email. Developers pattern-match faster from realistic data than from abstract placeholders.&lt;/p&gt;

&lt;h2&gt;
  
  
  Explain the Why, Not Just the What
&lt;/h2&gt;

&lt;p&gt;Reference docs answer "what parameters does this endpoint accept." Great docs also answer "why would I use this endpoint over that one." If you have two similar endpoints, like a bulk update and a single update, explain the tradeoffs. Rate limits, latency differences, and idempotency behavior all belong here.&lt;/p&gt;

&lt;p&gt;This is also where changelogs and versioning notes matter. Developers integrating today need to know if a field was recently renamed, deprecated, or if there's a breaking change coming. A dated changelog, even a short one, saves your support team from repeating the same explanation over and over.&lt;/p&gt;

&lt;h2&gt;
  
  
  Structure Determines Whether Docs Get Read
&lt;/h2&gt;

&lt;p&gt;Even excellent content fails if it's structured poorly. A few structural habits worth adopting:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consistent endpoint layout.&lt;/strong&gt; Every endpoint page should follow the same order: description, authentication requirements, parameters, request example, response example, error codes. Developers scanning quickly should always know where to look for a specific detail.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Searchable headings.&lt;/strong&gt; Use descriptive headings that match how developers actually search, like "Rate Limiting" instead of "Usage Policies." Think in terms of what someone would type into a search bar at 2 a.m. while debugging.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code samples in multiple languages when relevant.&lt;/strong&gt; Not every team can support five SDKs, but even switching between curl, JavaScript, and Python for your top three or four endpoints removes friction for a wide range of users.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't Forget the Non-Interactive Version
&lt;/h2&gt;

&lt;p&gt;Interactive documentation tools like Swagger or Postman collections are useful, but plenty of developers still want a static, offline reference they can annotate, share with a team, or read without a browser tab open. This matters more than teams expect, especially for enterprise clients whose security policies restrict access to external interactive tools during review, or for developers who prefer working from a printed or annotated copy during integration planning.&lt;/p&gt;

&lt;p&gt;This is actually a workflow I rely on constantly when packaging documentation for client handoff. After exporting API reference pages, I convert them into clean, shareable PDFs using &lt;a href="https://www.pdfconveter.com/" rel="noopener noreferrer"&gt;PDF Conveter&lt;/a&gt;, a free browser-based toolkit that handles different types of conversion, merging multiple doc sections into a single file, and compressing the result so it's easy to send over email. It's a small step, but having a portable, professional-looking PDF version of your API docs is genuinely useful when a client's dev team wants something they can review offline or forward internally without dealing with login-gated portals.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test Your Docs Like You Test Your Code
&lt;/h2&gt;

&lt;p&gt;Here's a practice most teams skip: have someone who didn't build the API try to integrate it using only the documentation, with no help from the engineering team. Watch where they get stuck. Every point of confusion is a documentation bug, and it should be tracked and fixed the same way you'd track a code bug.&lt;/p&gt;

&lt;p&gt;This single habit catches more real-world usability issues than any style guide or template ever will. Documentation isn't done when it's published. It's done when someone unfamiliar with your system can use it correctly on the first try.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Metric That Matters
&lt;/h2&gt;

&lt;p&gt;Good documentation isn't measured by word count or page views. It's measured by how few support tickets get filed asking questions the docs were supposed to answer, and by how quickly a new developer goes from reading your docs to shipping working code.&lt;/p&gt;

&lt;p&gt;If you want a quick gut check on your current docs, ask this: could a competent developer, with zero context on your team or product, make a correct API call within ten minutes using only what's written? If the honest answer is no, that's your next sprint.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>A Developer's Guide to Testing File Upload and Conversion Features</title>
      <dc:creator>Simon Briggs</dc:creator>
      <pubDate>Thu, 24 Sep 2026 03:48:42 +0000</pubDate>
      <link>https://dev.to/simon_briggs_6607e52a5245/a-developers-guide-to-testing-file-upload-and-conversion-features-1fe1</link>
      <guid>https://dev.to/simon_briggs_6607e52a5245/a-developers-guide-to-testing-file-upload-and-conversion-features-1fe1</guid>
      <description>&lt;p&gt;In the middle of the night, I got a Slack ping from our QA lead: "The production upload endpoint is silently dropping 40% of PDFs over 8MB." No error logs. No stack trace. Just... gone. Users clicked upload, saw a spinner, and got nothing back. It took us three hours to find the culprit: a reverse proxy timeout that had nothing to do with our actual upload code, and everything to do with the fact that nobody had ever tested what happens when a file takes longer than 30 seconds to process.&lt;/p&gt;

&lt;p&gt;If you've built any feature that touches file uploads or format conversion, you already know this pain lives in a weird blind spot. Unit tests pass. The demo works. Then real users show up with a 200-page scanned PDF, a corrupted DOCX from 2009, or a file named &lt;code&gt;résumé (final)(2).pdf&lt;/code&gt;, and everything you thought you'd covered falls apart.&lt;/p&gt;

&lt;p&gt;This is a guide to actually testing that stuff, not just the happy path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why file handling breaks differently than other features
&lt;/h2&gt;

&lt;p&gt;Most application logic deals with data you control: JSON payloads, form fields, database rows. File upload and conversion features deal with data that comes from &lt;em&gt;outside your system entirely&lt;/em&gt;. The file could be malformed, mislabeled, absurdly large, or crafted specifically to break your parser.&lt;/p&gt;

&lt;p&gt;That means your test strategy needs to shift from "does the function return the right value" to "does the system survive contact with reality." Those are different disciplines, and treating them the same is how bugs like my 2 AM incident happen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with a matrix, not a checklist
&lt;/h2&gt;

&lt;p&gt;The instinct is to write a list of test cases. Don't. Build a matrix instead, because upload/conversion bugs live at the intersection of variables, not in any single one.&lt;/p&gt;

&lt;p&gt;Your axes should include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;File size&lt;/strong&gt;: empty (0 bytes), tiny (1KB), typical (2-5MB), large (50MB+), and whatever your stated max is, plus one byte over it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File type validity&lt;/strong&gt;: correct extension with correct content, correct extension with wrong content (rename a &lt;code&gt;.zip&lt;/code&gt; to &lt;code&gt;.pdf&lt;/code&gt; and see what happens), no extension, wrong MIME type header&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Encoding and structure&lt;/strong&gt;: password-protected PDFs, PDFs with embedded fonts, DOCX files saved by Word vs. Google Docs vs. LibreOffice (they are not identical under the hood)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network conditions&lt;/strong&gt;: slow upload, connection drop mid-transfer, client cancels the request&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Concurrency&lt;/strong&gt;: same user uploading 10 files at once, 500 different users uploading simultaneously
A five-by-five matrix sounds like overkill until you realize that "large file + slow network + client cancel" is exactly the combination that leaves orphaned temp files rotting on your server disk. I've seen that exact bug take down a staging environment because nobody cleaned up partial uploads.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Test the file, not just the upload
&lt;/h2&gt;

&lt;p&gt;Here's where people cut corners. It's easy to test that a file &lt;em&gt;arrives&lt;/em&gt;. It's harder, and more important, to test that it arrives &lt;em&gt;correctly&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;If you're building a conversion pipeline (PDF to Word, image to PDF, whatever), write assertions against the actual output content, not just "the process didn't throw." A quick example using a Node test with a PDF parsing library:&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;fs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pdfParse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pdf-parse&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;converted PDF preserves original text content&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;inputText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Invoice #4471 — Total Due: $1,240.00&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;outputBuffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./test-output.pdf&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
 &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;pdfParse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;outputBuffer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

 &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toContain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Invoice #4471&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
 &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toContain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1,240.00&lt;/span&gt;&lt;span class="dl"&gt;'&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;p&gt;This catches the class of bugs that "did it return 200 OK" never will, like tables that get flattened into unreadable text blobs, or special characters (em dashes, currency symbols, accented letters) that silently turn into garbage during encoding conversion. I've watched a conversion tool turn "café" into "cafÃ©" because someone assumed UTF-8 everywhere and forgot the source file was Latin-1.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fuzz your file inputs
&lt;/h2&gt;

&lt;p&gt;If your app accepts user-uploaded files and does anything with them server-side (parsing, rendering, converting), you should be fuzz testing those inputs the same way you'd fuzz test an API endpoint.&lt;/p&gt;

&lt;p&gt;Tools like &lt;code&gt;radamsa&lt;/code&gt; or even a simple script that flips random bytes in a valid file can surface parser crashes you'd never think to test manually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# quick and dirty byte-flip fuzzer for local testing&lt;/span&gt;
python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"
import random
with open('sample.pdf', 'rb') as f:
   data = bytearray(f.read())
for _ in range(50):
   i = random.randint(0, len(data) - 1)
   data[i] = random.randint(0, 255)
with open('fuzzed.pdf', 'wb') as f:
   f.write(data)
"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run your conversion pipeline against a batch of these mutated files and watch what happens. A well-built system should fail gracefully with a clear error. A fragile one crashes the worker process, or worse, hangs indefinitely on a malformed byte stream and quietly eats your server's memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automate the boring parts, but keep a manual pass
&lt;/h2&gt;

&lt;p&gt;CI pipelines are great at running the same 200 file combinations every commit. They're bad at catching weird stuff, like a font-rendering issue that only shows up visually, or a table that technically "converts" but looks wrong to a human eye.&lt;/p&gt;

&lt;p&gt;I keep a small folder of "problem files" collected over time: real files that broke something in production, sanitized of any sensitive data. Before shipping any change to upload or conversion logic, I run that folder through manually and actually look at the output. It takes ten minutes and has caught things automated tests missed every single time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't forget the format edge cases
&lt;/h2&gt;

&lt;p&gt;If your product does any actual file conversion, whether that's PDF to Word, PDF to Excel, or images to PDF, this is where testing gets genuinely tricky. Tables that shift columns, hyperlinks that break mid-conversion, fonts that substitute themselves with something ugly- these are the bugs users actually notice and complain about, even when your test suite is green.&lt;/p&gt;

&lt;p&gt;When I'm sanity-checking a conversion feature quickly, without spinning up a whole test harness, I'll sometimes just run the file through &lt;a href="https://pdfconveter.com/" rel="noopener noreferrer"&gt;PDF Conveter&lt;/a&gt; as a second opinion. It's a free browser-based tool, and comparing its output against mine is a fast way to spot whether a formatting issue is a "PDF is just weird" problem or something actually broken in my own pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real lesson
&lt;/h2&gt;

&lt;p&gt;File upload and conversion features are deceptively simple to build and genuinely hard to make robust. The gap between "works in my testing" and "works for every user who throws a real-world file at it" is where most of the painful bugs live.&lt;/p&gt;

&lt;p&gt;Build your matrix, write output-content assertions instead of status-code assertions, fuzz your inputs, and keep a folder of real broken files as your manual regression suite. Your 2 AM self will thank you.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Designing a File Conversion Pipeline: Lessons from Building One</title>
      <dc:creator>Simon Briggs</dc:creator>
      <pubDate>Wed, 23 Sep 2026 04:18:36 +0000</pubDate>
      <link>https://dev.to/simon_briggs_6607e52a5245/designing-a-file-conversion-pipeline-lessons-from-building-one-1man</link>
      <guid>https://dev.to/simon_briggs_6607e52a5245/designing-a-file-conversion-pipeline-lessons-from-building-one-1man</guid>
      <description>&lt;p&gt;In the middle of the night, a batch job that had run cleanly for three weeks decided to eat 40 PDFs and spit out nothing. No error in the logs, no stack trace, just... silence. That's the night I stopped treating file conversion as a "quick utility" and started treating it as actual system design.&lt;/p&gt;

&lt;p&gt;If you've ever built something that converts files, whether it's images, documents, or data formats, you know the pitch sounds simple: take file A, turn it into file B. The reality is closer to building a small, opinionated compiler that has to deal with garbage input, half-broken encodings, and users who will absolutely upload a 200MB file named &lt;code&gt;final_final_v3(1).PDF&lt;/code&gt; and expect it to just work.&lt;/p&gt;

&lt;p&gt;Here's what I actually learned building a conversion pipeline, past the "just use a library" stage.&lt;/p&gt;

&lt;h2&gt;
  
  
  The naive version breaks fast
&lt;/h2&gt;

&lt;p&gt;The first version of almost every conversion tool looks like this:&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;convertFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inputPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;outputFormat&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inputPath&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;converter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;convert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;outputFormat&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;result&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;p&gt;This works in a demo. It works for your first ten test files. Then someone uploads a corrupted PDF, or a 300-page scanned document, or a file that's technically a &lt;code&gt;.docx&lt;/code&gt; but was renamed from &lt;code&gt;.zip&lt;/code&gt; by a confused user, and your single function has to somehow handle all of that gracefully. It won't, because it wasn't designed to.&lt;/p&gt;

&lt;p&gt;The real lesson: conversion isn't one operation. It's a pipeline of distinct stages, and each stage fails in its own specific way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Breaking it into stages actually matters
&lt;/h2&gt;

&lt;p&gt;Once I stopped writing one big function and started thinking in stages, debugging got dramatically easier. A reasonable pipeline looks something like:&lt;br&gt;
Validate → Parse → Transform → Render → Verify → Deliver&lt;/p&gt;

&lt;p&gt;Each stage has a narrow job and a narrow failure mode.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Validation&lt;/strong&gt; catches the obvious stuff before you waste compute: wrong MIME type, file too large, password-protected when you don't support that, zero-byte upload. This sounds trivial until you realize a huge share of "conversion failed" support tickets are actually validation failures that slipped through.&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;function&lt;/span&gt; &lt;span class="nf"&gt;validateInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&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;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ConversionError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;EMPTY_FILE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&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;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;MAX_SIZE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ConversionError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;FILE_TOO_LARGE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;isSupportedMimeType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mimeType&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ConversionError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;UNSUPPORTED_FORMAT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&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;p&gt;&lt;strong&gt;Parsing&lt;/strong&gt; is where most of the real complexity lives, especially with PDFs. A PDF isn't really "one format"; it's a container that can hold vector graphics, embedded fonts, scanned raster images, form fields, and metadata, all mixed. Parsing a PDF for text extraction is a completely different problem than parsing it to preserve table layout, which is a different problem again from parsing it for image extraction. Treating "parse the PDF" as a single step is where many tools quietly cut corners, which is exactly why formatting breaks or tables collapse into a wall of text after conversion.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Transform&lt;/strong&gt; is the actual conversion logic, mapping the parsed structure to the target format's model. This is where you decide what to do when the source format supports something the target doesn't. Word docs support tracked changes; plain text doesn't. You have to make an explicit decision (flatten, strip, warn) rather than let the library decide silently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verify&lt;/strong&gt; is the stage almost everyone skips, and it's the one that would've saved me from that 2 a.m. incident. A conversion that completes without throwing an error isn't the same as a conversion that succeeded. Did the output file actually open? Does it have the expected page count, roughly the expected size, and non-zero content? A cheap sanity check here catches a huge class of silent failures.&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;verifyOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;outputPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expectedMinSize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stats&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;outputPath&lt;/span&gt;&lt;span class="p"&gt;);&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;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;expectedMinSize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ConversionError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;OUTPUT_TOO_SMALL&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="p"&gt;});&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;
  
  
  Queue it, don't block it
&lt;/h2&gt;

&lt;p&gt;The other thing that changes everything: conversion is almost never instant, and it shouldn't block a request-response cycle. Once you're handling anything beyond tiny files, you want a job queue, not a synchronous endpoint.&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="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/convert&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;jobId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;convert&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;fileId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fileId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;targetFormat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;targetFormat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;jobId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;queued&lt;/span&gt;&lt;span class="dl"&gt;'&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;p&gt;This buys you retries, backpressure when load spikes, and the ability to actually see where a job is stuck instead of a request just timing out. It also means one huge file doesn't take down conversions for everyone else hitting your service at the same time, which is a real failure mode if you're running this on a single worker process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Idempotency saves you from yourself
&lt;/h2&gt;

&lt;p&gt;If a worker crashes mid-conversion and your queue retries the job, does running it twice cause a problem? If you're writing partial output to a shared path, the answer is often "yes, and it's ugly." Designing each job to be safely retryable, writing to a temp path first and only moving to the final location on success, is a small habit that prevents a lot of 3 a.m. pages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Format edge cases are the real work
&lt;/h2&gt;

&lt;p&gt;Here's the part nobody tells you upfront: the "conversion logic" itself is maybe 30% of the work. The other 70% is edge cases. Fonts that don't embed properly and fall back to something ugly. Hyperlinks that survive a PDF-to-Word conversion structurally but lose their href. Tables that render fine visually in the source but have no real row/column structure underneath, so any converter has to guess. Scanned PDFs that need OCR before there's even text to convert.&lt;/p&gt;

&lt;p&gt;None of this shows up until you throw real-world files at the system, which is exactly why "it works on my test files" and "it works in production" are such different claims.&lt;/p&gt;

&lt;p&gt;This is also, honestly, the reason I keep a browser-based tool bookmarked for quick one-off conversions rather than running everything through my own pipeline every time. When I just need a PDF turned into a clean Word file or a JPG without spinning up a job, I use &lt;a href="https://pdfconveter.com/" rel="noopener noreferrer"&gt;PDF Conveter&lt;/a&gt; instead of reinventing that particular wheel for a single file. It's a decent reminder, too: building your own pipeline makes sense when conversion is core to your product, but for a one-off task, borrowing a tool that's already handled these edge cases is the more sensible engineering call.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell my past version
&lt;/h2&gt;

&lt;p&gt;Design for failure before you design for the happy path. Log at each stage boundary, not just at the top level, so you can tell whether a job died in parsing versus rendering versus delivery. And build your verify step early, because "no error thrown" is a very low bar for "the user got what they needed."&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Client-Side vs Server-Side File Processing: Trade-offs Every Developer Should Know</title>
      <dc:creator>Simon Briggs</dc:creator>
      <pubDate>Sat, 19 Sep 2026 04:10:24 +0000</pubDate>
      <link>https://dev.to/simon_briggs_6607e52a5245/client-side-vs-server-side-file-processing-trade-offs-every-developer-should-know-4dde</link>
      <guid>https://dev.to/simon_briggs_6607e52a5245/client-side-vs-server-side-file-processing-trade-offs-every-developer-should-know-4dde</guid>
      <description>&lt;p&gt;A few months back, I watched a junior dev on our team spend two days building a slick in-browser CSV parser for an admin dashboard, only for it to fall over the moment a client uploaded a 40,000-row export. The browser tab froze. Support got a ticket titled "app is broken???" within the hour. The fix wasn't a better parser. It was realizing the processing belonged on the server all along.&lt;/p&gt;

&lt;p&gt;That's the thing about the client-side vs server-side decision. It's rarely about which one is "better." It's about which failure mode you're willing to live with.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Pitch for Client-Side Processing
&lt;/h2&gt;

&lt;p&gt;Doing the work in the browser is seductive for good reason. No round trip to a server means no network latency, no upload wait, and (usually) no server compute bill. For a lot of file tasks, this is genuinely the right call.&lt;/p&gt;

&lt;p&gt;Here's a simple example: resizing an image before upload, using the Canvas API.&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;resizeImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;maxWidth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;800&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bitmap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createImageBitmap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
 &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;maxWidth&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;bitmap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
 &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;canvas&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
 &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;bitmap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;bitmap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

 &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2d&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
 &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drawImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bitmap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

 &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBlob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image/jpeg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.85&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;p&gt;This runs entirely on the user's machine. No file ever leaves their device until they hit submit, which is also a privacy win worth mentioning to anyone building for regulated industries. If someone's uploading a scanned medical form or a signed contract, keeping it client-side until the last possible second is a real feature, not just a performance trick.&lt;/p&gt;

&lt;p&gt;Client-side also wins for anything interactive: cropping, drawing on an image, previewing a PDF page before conversion, live-validating a spreadsheet as someone edits it. The user expects instant feedback, and a server round trip for every keystroke would feel broken even if it technically worked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where It Falls Apart
&lt;/h2&gt;

&lt;p&gt;The browser is not a server. It doesn't have consistent memory limits, it doesn't have predictable CPU allocation, and it's running on whatever device the user happens to own, which might be a five-year-old Android phone with three other tabs open playing music.&lt;/p&gt;

&lt;p&gt;That CSV parser I mentioned earlier failed for a boring reason: parsing 40,000 rows with a naive regex-based approach on the main thread blocks rendering entirely. The browser doesn't multitask gracefully unless you explicitly hand work off to a Web Worker, and even then, you're capped by whatever RAM the device has, not by what your task actually needs.&lt;/p&gt;

&lt;p&gt;File format conversion is a good example of a task that looks simple until you actually try to do it right in-browser. Converting PDF to DOCX, or extracting tables out of a PDF while preserving formatting, involves parsing binary structures, handling fonts, dealing with embedded images, and reconstructing layout logic that libraries like docx.js or pdf-lib only partially support. You can get 70% of the way there client-side. The last 30%, the part where tables don't collapse and fonts don't randomly swap to Times New Roman, usually needs a proper server-side rendering engine.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Server-Side Trade
&lt;/h2&gt;

&lt;p&gt;Push the work to a server, and you trade latency and bandwidth for consistency and power. You control the environment. You know exactly what CPU, memory, and libraries are available, so a PDF conversion that needs a full rendering engine (think something built on a headless browser or a dedicated PDF library like PDFBox or MuPDF) just works, regardless of whether your user is on an iPhone SE or a gaming rig.&lt;/p&gt;

&lt;p&gt;A minimal Node.js endpoint for something like this might look like:&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="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/convert&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;upload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;single&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;file&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;convertPdfToDocx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// heavy lifting happens here&lt;/span&gt;
   &lt;span class="nx"&gt;res&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/vnd.openxmlformats-officedocument.wordprocessingml.document&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
   &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
 &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;422&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Conversion failed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
 &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="c1"&gt;// clean up temp files immediately, don't let uploads linger on disk&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;p&gt;The obvious cost is that now you're paying for compute, managing scaling under load, and dealing with upload size limits. You also own a new attack surface. Every file upload endpoint is a potential vector for zip bombs, malformed files designed to crash your parser, or just someone uploading a 2GB file to see what happens. If you're going server-side, budget real time for validation and sandboxing, not just the happy path.&lt;/p&gt;

&lt;p&gt;There's also the privacy angle again, but flipped. Anything that touches your server is now something you're responsible for storing, logging, and eventually deleting. That's a compliance conversation, not just an engineering one, especially if you're handling anything under GDPR or HIPAA.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Framework, Not a Rule
&lt;/h2&gt;

&lt;p&gt;After going back and forth on this more times than I'd like to admit, the questions that actually help me decide are:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How big can the file realistically get?&lt;/strong&gt; &lt;br&gt;
Under a few MB, client-side is usually fine. Once you're regularly dealing with tens of MB or more, server-side gives you predictable performance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does the operation need format fidelity?&lt;/strong&gt; &lt;br&gt;
Simple transforms (resize, crop, basic compression) are fine in-browser. Anything requiring true document rendering, like preserving PDF table structure or complex font handling, tends to need server-grade tooling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is the data sensitive before the user commits to submitting it?&lt;/strong&gt; &lt;br&gt;
If yes, keep it client-side as long as possible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do you need it to work identically across every device?&lt;/strong&gt; &lt;br&gt;
Server-side is the only way to guarantee that.&lt;/p&gt;

&lt;p&gt;A lot of production apps end up doing both. Quick previews and light edits happen client-side for responsiveness, and the final, heavy transformation happens server-side once the user hits confirm. That hybrid pattern is honestly underused. It gives you the snappy UX without pretending the browser can do everything a dedicated backend can.&lt;/p&gt;

&lt;p&gt;This is basically the architecture behind most solid file tools, including the conversion stack we run on &lt;a href="https://pdfconveter.com/" rel="noopener noreferrer"&gt;PDF Conveter&lt;/a&gt;. Simple stuff like previewing a page or reordering PDFs happens instantly on the client. Still, the actual PDF-to-Word or PDF-to-Excel conversion, where formatting fidelity actually matters, gets handled server-side. Hence, it comes out clean regardless of what device or browser someone's using.&lt;/p&gt;

&lt;p&gt;If you're building something similar and want to see how a hybrid approach handles edge cases like table preservation or font mapping, it's worth poking around a working example instead of guessing from docs alone.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>backend</category>
      <category>python</category>
    </item>
    <item>
      <title>Understanding the File API: How Browsers Read Files Without a Server</title>
      <dc:creator>Simon Briggs</dc:creator>
      <pubDate>Fri, 18 Sep 2026 04:22:02 +0000</pubDate>
      <link>https://dev.to/simon_briggs_6607e52a5245/understanding-the-file-api-how-browsers-read-files-without-a-server-f4g</link>
      <guid>https://dev.to/simon_briggs_6607e52a5245/understanding-the-file-api-how-browsers-read-files-without-a-server-f4g</guid>
      <description>&lt;p&gt;A few years back, I watched a junior dev spend an entire afternoon trying to figure out why his "upload preview" feature hit the server on every file selection, even before the user clicked submit. He'd built a full endpoint just to generate a thumbnail. Turns out he didn't need a server at all. The browser could do it. He just didn't know the File API existed.&lt;/p&gt;

&lt;p&gt;That moment stuck with me because it's such a common gap. We treat "reading a file" as something that inherently requires a backend, a POST request, some processing pipeline. But modern browsers have quietly shipped a set of APIs that let JavaScript read file contents directly, client-side, before anything ever touches your server.&lt;/p&gt;

&lt;p&gt;Let's break down how it actually works.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Core Objects: File and Blob
&lt;/h2&gt;

&lt;p&gt;Everything starts with two interfaces: &lt;code&gt;Blob&lt;/code&gt; and &lt;code&gt;File&lt;/code&gt;. A &lt;code&gt;File&lt;/code&gt; is really just a &lt;code&gt;Blob&lt;/code&gt; with extra metadata (name, last modified date, MIME type). You'll usually encounter files through an &lt;code&gt;&amp;lt;input type="file"&amp;gt;&lt;/code&gt; element or a drag-and-drop event.&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;input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;input[type="file"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;change&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;       &lt;span class="c1"&gt;// "invoice.pdf"&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;       &lt;span class="c1"&gt;// bytes&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;       &lt;span class="c1"&gt;// "application/pdf"&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lastModified&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;p&gt;At this point, nothing has been "opened" or "read" yet. You just have a reference and its metadata. The actual bytes are still sitting untouched, and that distinction matters for performance. A &lt;code&gt;File&lt;/code&gt; object for a 2GB video costs you almost nothing until you decide to read it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Actually Reading the Bytes
&lt;/h2&gt;

&lt;p&gt;This is where &lt;code&gt;FileReader&lt;/code&gt; comes in, and it's the piece most people remember once they've used it once.&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;reader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FileReader&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// the file contents&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="nx"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readAsText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;        &lt;span class="c1"&gt;// for plain text&lt;/span&gt;
&lt;span class="c1"&gt;// or&lt;/span&gt;
&lt;span class="nx"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readAsDataURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;     &lt;span class="c1"&gt;// base64-encoded, great for image previews&lt;/span&gt;
&lt;span class="c1"&gt;// or&lt;/span&gt;
&lt;span class="nx"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readAsArrayBuffer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// raw binary, for parsing binary formats&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;FileReader&lt;/code&gt; is asynchronous and event-based, which trips people up if they're used to synchronous file I/O from server-side languages. You don't get the result back from the function call. You get it in the &lt;code&gt;onload&lt;/code&gt; callback, because reading a large file can take real time and you don't want to freeze the main thread.&lt;/p&gt;

&lt;p&gt;That &lt;code&gt;readAsDataURL&lt;/code&gt; method powers nearly every instant image preview you've ever seen on an upload form. No server round trip needed, just:&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="nx"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;img#preview&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="nx"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readAsDataURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Newer, Cleaner Way: Promises
&lt;/h2&gt;

&lt;p&gt;If callback-based &lt;code&gt;FileReader&lt;/code&gt; feels dated, that's fair. Most modern codebases lean on the &lt;code&gt;File.text()&lt;/code&gt; and &lt;code&gt;File.arrayBuffer()&lt;/code&gt; methods instead, which return native Promises:&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&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;p&gt;Same underlying mechanism, far less ceremony. If you're writing anything new in 2026, this is the version worth reaching for. &lt;code&gt;readAsText&lt;/code&gt; isn't deprecated, but it's the kind of API you inherit from older code rather than write fresh.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where This Actually Gets Useful
&lt;/h2&gt;

&lt;p&gt;Here's the part that surprises people who've only used the File API for image previews: you can parse structured file formats entirely in the browser. CSV parsing, JSON validation, even reading the internal XML of a &lt;code&gt;.docx&lt;/code&gt; file (which is really just a zip archive), all doable client-side with &lt;code&gt;arrayBuffer()&lt;/code&gt; and a library like JSZip.&lt;/p&gt;

&lt;p&gt;I once built a small internal tool that validated CSV exports before letting a user submit them, catching malformed rows, wrong column counts, and encoding issues, all before a single byte reached our API. It cut our support tickets around "upload failed" errors by a noticeable margin, because users got instant, specific feedback instead of a generic server error three seconds later.&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;validateCSV&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;headerCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bad&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;headerCount&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;bad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&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;p&gt;This is a genuinely useful pattern for anyone building form-heavy tools, dashboards, or admin panels. Fail fast, fail client-side, and only ship valid data upstream.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where It Breaks Down
&lt;/h2&gt;

&lt;p&gt;The File API is powerful, but it's not magic. A few honest limitations worth knowing before you lean on it too hard:&lt;/p&gt;

&lt;p&gt;Large files can still choke the main thread if you're doing heavy parsing synchronously after the read. Pairing &lt;code&gt;FileReader&lt;/code&gt; or &lt;code&gt;arrayBuffer()&lt;/code&gt; with a Web Worker is the right move once you're past toy examples.&lt;/p&gt;

&lt;p&gt;You can't write back to the original file on disk. The File API is read-only by design, for obvious security reasons. If a user needs to "save" a modified version, you're generating a new Blob and triggering a download, not editing the original.&lt;/p&gt;

&lt;p&gt;And browser support for some binary parsing edge cases (particularly around older or obscure file formats) still varies enough that you'll want to test across engines, not just assume Chrome behavior is universal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where PDFs Fit Into This
&lt;/h2&gt;

&lt;p&gt;PDFs are a fun edge case here, because they're binary, often large, and structurally complex enough that fully parsing one client-side (extracting text, splitting pages, converting formats) isn't something you'd want to hand-roll with &lt;code&gt;arrayBuffer()&lt;/code&gt; alone unless you enjoy pain.&lt;/p&gt;

&lt;p&gt;For quick, no-backend needs, like grabbing file size or previewing the first page as an image, the File API gets you surprisingly far. But once you need actual conversion work (PDF to Word, merging pages, compressing a bloated scan), that's a different problem than "reading bytes in a browser," and it's usually smarter to reach for a dedicated tool rather than reinventing a PDF parser from scratch. I keep &lt;a href="https://pdfconveter.com/" rel="noopener noreferrer"&gt;PDF Conveter&lt;/a&gt; bookmarked for exactly that gap. It's the boring, reliable step after the File API has done its job of getting the file into the browser in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Takeaway
&lt;/h2&gt;

&lt;p&gt;The File API is one of those quietly foundational browser features that doesn't get much hype because it's not flashy; it's just useful. Once you understand that reading a file and uploading a file are two separate steps, and that the browser is fully capable of the first one on its own, a lot of "why do we need a server for this" moments start disappearing from your codebase.&lt;/p&gt;

&lt;p&gt;Next time you're tempted to spin up an endpoint just to peek inside a file the user already handed you, ask whether the browser could've just told you itself.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>programming</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I Debugged a Silent Upload Failure for 6 Hours. Here's the Root Cause.</title>
      <dc:creator>Simon Briggs</dc:creator>
      <pubDate>Thu, 17 Sep 2026 05:31:41 +0000</pubDate>
      <link>https://dev.to/simon_briggs_6607e52a5245/i-debugged-a-silent-upload-failure-for-6-hours-heres-the-root-cause-4b5g</link>
      <guid>https://dev.to/simon_briggs_6607e52a5245/i-debugged-a-silent-upload-failure-for-6-hours-heres-the-root-cause-4b5g</guid>
      <description>&lt;p&gt;It was 11 PM on a Tuesday. QA had flagged one line in the bug tracker: "File upload doesn't work sometimes." No stack trace. No error in the logs. No 4xx or 5xx response. Just a 200 OK and a file that never showed up in storage.&lt;/p&gt;

&lt;p&gt;If you've done backend work for more than a year, you already know the specific dread this causes. A crash is a gift. A crash tells you exactly where to look. A silent failure tells you nothing, and it hands you six hours of your evening to figure out why "nothing went wrong" and "nothing worked" are somehow both true at the same time.&lt;/p&gt;

&lt;p&gt;Here's what actually happened, and why the fix took so long to find.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Setup
&lt;/h2&gt;

&lt;p&gt;The stack was straightforward: an Express API, &lt;code&gt;multer&lt;/code&gt; handling multipart uploads, and a React frontend sending &lt;code&gt;FormData&lt;/code&gt; through &lt;code&gt;fetch&lt;/code&gt;. Nothing exotic. The kind of setup that works in a thousand tutorials without a single caveat mentioned.&lt;/p&gt;

&lt;p&gt;The endpoint looked something like this:&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="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlencoded&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;extended&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;

&lt;span class="c1"&gt;// ... other routes ...&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/upload&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;upload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;single&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;file&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;No file received&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
 &lt;span class="p"&gt;}&lt;/span&gt;
 &lt;span class="nf"&gt;saveToStorage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
 &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&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;p&gt;Simple enough. It worked in local dev, every single time. It worked in staging, most of the time. In production, it failed intermittently, for some users, on some files, with zero pattern anyone could pin down.&lt;/p&gt;

&lt;h2&gt;
  
  
  The First Four Hours: Chasing Ghosts
&lt;/h2&gt;

&lt;p&gt;The first instinct was to blame the frontend. Maybe the &lt;code&gt;FormData&lt;/code&gt; object wasn't being built correctly. Maybe a race condition in the file input's &lt;code&gt;onChange&lt;/code&gt; handler was grabbing a stale reference. I added logging before the fetch call, confirmed the file was attached, confirmed the request was firing, and confirmed the payload size looked right in the Network tab.&lt;/p&gt;

&lt;p&gt;Frontend was clean. Move to the backend.&lt;/p&gt;

&lt;p&gt;Next theory: file size limits. Multer has a &lt;code&gt;limits&lt;/code&gt; config, and silently rejecting oversized files felt like a plausible culprit. I bumped every limit I could find, redeployed, and the bug still showed up on files well under any threshold.&lt;/p&gt;

&lt;p&gt;Then I suspected the reverse proxy. Nginx has its own &lt;code&gt;client_max_body_size&lt;/code&gt;, and if that's set too low, it can truncate or reject a request in ways that don't always surface clearly to the app layer. I checked the config. It was fine, generous even.&lt;/p&gt;

&lt;p&gt;Four hours in, I had ruled out the frontend, the file size limits, and the proxy. I had also, somewhere around hour three, started questioning my career choices. This is the part of debugging nobody puts in the tutorial: the long stretch where every reasonable hypothesis is wrong, and you start suspecting the compiler, the runtime, and possibly the concept of computers in general.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Breakthrough
&lt;/h2&gt;

&lt;p&gt;The turning point came from something almost embarrassingly small. I added a raw &lt;code&gt;console.log(req.headers['content-type'])&lt;/code&gt; at the very top of the middleware stack, before &lt;code&gt;multer&lt;/code&gt; ever touched the request. Most requests logged the expected &lt;code&gt;multipart/form-data; boundary=...&lt;/code&gt;. But on the failing ones, by the time the log fired inside the upload handler, &lt;code&gt;req.body&lt;/code&gt; was already a fully parsed, empty object.&lt;/p&gt;

&lt;p&gt;That was the clue. &lt;code&gt;req.body&lt;/code&gt; should not exist yet at that point in a multipart request. Multer builds it. Something upstream was already consuming the request stream.&lt;/p&gt;

&lt;h2&gt;
  
  
  The culprit was &lt;code&gt;express.json()&lt;/code&gt;.
&lt;/h2&gt;

&lt;p&gt;Body-parsing middleware in Express reads the incoming request stream to parse it. &lt;code&gt;express.json()&lt;/code&gt; is supposed to skip requests that aren't JSON, and it usually does, based on the &lt;code&gt;Content-Type&lt;/code&gt; header. But under specific conditions, involving a proxy or client that slightly reordered or duplicated headers, the check misfired. &lt;code&gt;express.json()&lt;/code&gt; began consuming the stream on a request it should have ignored. By the time the request reached &lt;code&gt;multer&lt;/code&gt;, the stream had already been partially drained. Multer received a request it could not fully read, found no valid file part, and quietly moved on. No file, no error, no file attribute. Just an empty &lt;code&gt;req.file&lt;/code&gt;, an unhelpful &lt;code&gt;if&lt;/code&gt; check that didn't catch the actual cause, and a 200 response, because nothing had technically thrown.&lt;/p&gt;

&lt;p&gt;Two middleware functions were racing to read the same stream, and the loser lost silently.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix
&lt;/h2&gt;

&lt;p&gt;The fix was almost anticlimactic after six hours of searching:&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;// Only parse JSON/urlencoded bodies for routes that need them&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/json-routes&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/json-routes&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlencoded&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;extended&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;

&lt;span class="c1"&gt;// Upload route never touches body-parsing middleware&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/upload&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;upload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;single&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;file&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;No file received&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
 &lt;span class="p"&gt;}&lt;/span&gt;
 &lt;span class="nf"&gt;saveToStorage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
 &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&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;p&gt;Scoping body-parsers to the routes that actually need them, instead of applying them globally with &lt;code&gt;app.use()&lt;/code&gt;, removed the race entirely. &lt;code&gt;multer&lt;/code&gt; now gets the raw stream first and every time, because nothing else is allowed to touch it beforehand.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Actual Lesson
&lt;/h2&gt;

&lt;p&gt;The root cause wasn't a bug in Express, or in multer, or in the frontend. It was an architectural assumption: that global middleware is always safe because it's "just parsing." Middleware order in Express is a shared resource. Two pieces of middleware that both want to read the request stream will eventually collide, and when they do, the failure mode is often silence rather than a crash, because neither one considers the other's job a failure condition.&lt;/p&gt;

&lt;p&gt;If there's a takeaway worth pinning to your desk, it's this: audit what your global middleware actually touches, not just what you assume it touches. &lt;code&gt;express.json()&lt;/code&gt; feels inert on a route with no JSON body. It isn't. It reads first and asks questions later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On a side note&lt;/strong&gt;, this whole ordeal happened while I was also rebuilding an internal tool for handling document conversions on the side, which made the irony sharper: half my week was spent making sure files got where they needed to go reliably, and the other half was spent using &lt;a href="https://pdfconveter.com" rel="noopener noreferrer"&gt;PDF Conveter&lt;/a&gt; to quickly merge and compress PDFs for a client deliverable, because sometimes you just need a browser-based tool that doesn't ask you to think about middleware order at all.&lt;/p&gt;

&lt;p&gt;If you've hit your own "silent failure" that turned out to be a middleware ordering issue, or something equally invisible, I'd genuinely like to hear it in the comments. These bugs rarely make it into blog posts because they're not glamorous, but they're the ones that teach the most about how our frameworks actually work under the hood.&lt;/p&gt;

</description>
      <category>backend</category>
      <category>express</category>
      <category>json</category>
    </item>
    <item>
      <title>Why a Word Document Isn’t Always the Best Format for Sharing</title>
      <dc:creator>Simon Briggs</dc:creator>
      <pubDate>Wed, 16 Sep 2026 06:59:53 +0000</pubDate>
      <link>https://dev.to/simon_briggs_6607e52a5245/why-a-word-document-isnt-always-the-best-format-for-sharing-19mn</link>
      <guid>https://dev.to/simon_briggs_6607e52a5245/why-a-word-document-isnt-always-the-best-format-for-sharing-19mn</guid>
      <description>&lt;p&gt;Microsoft Word is one of the most useful tools for creating documents. People use it for reports, resumes, invoices, assignments, invitations, forms, business documents, and countless other types of content.&lt;/p&gt;

&lt;p&gt;But there is an important difference between creating a document and sharing one.&lt;/p&gt;

&lt;p&gt;Word is built around editable content. That makes it excellent when someone needs to revise, collaborate on, or update a document. But when the goal is simply to let someone view the finished content, a Word file may not always be the most convenient format.&lt;/p&gt;

&lt;p&gt;Depending on the situation, a PDF or an image format such as JPG may be easier to share, preview, and view.&lt;/p&gt;

&lt;p&gt;So why isn't Word always the best format for sharing? The answer usually comes down to editability, formatting, compatibility, and how the recipient plans to use the content.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Word Is Built for Editing, Not Just Viewing&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The biggest advantage of a Word document is also its main limitation when sharing finished content: it is editable.&lt;/p&gt;

&lt;p&gt;If you are working with a colleague on a report, sending a draft to a client, or creating a document someone else needs to update, Word makes sense. The recipient can open the file, change text, add comments, adjust formatting, and continue working on it.&lt;/p&gt;

&lt;p&gt;But if the document is already finished and you don't want anyone to change it, sending an editable .docx file may offer more functionality than the recipient actually needs. A completed invoice, event invitation, announcement, product information sheet, certificate, or one-page notice usually just needs to be seen, not edited.&lt;/p&gt;

&lt;p&gt;Someone receiving a finished menu, invitation, or information sheet often only wants to open it and read it. This is one reason images can be useful for sharing certain types of Word content: a JPG can be previewed instantly on most devices instead of being treated as an editable file to open and manage.&lt;/p&gt;

&lt;p&gt;The right format depends less on how the document was created and more on what the recipient needs to do with it. Ask a simple question before sending: does the recipient actually need to edit this?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Word Formatting Can Look Different on Another Device&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A document can look exactly the way you intended on your computer but display differently when opened elsewhere. Fonts, software versions, page settings, or rendering differences can affect how the document appears.&lt;/p&gt;

&lt;p&gt;This can lead to problems such as:&lt;/p&gt;

&lt;p&gt;Text moving onto another line&lt;br&gt;
Different fonts appearing&lt;br&gt;
Tables becoming harder to read&lt;br&gt;
Images shifting position&lt;br&gt;
Page breaks changing&lt;br&gt;
Headers or footers moving&lt;br&gt;
Content spilling onto an additional page&lt;/p&gt;

&lt;p&gt;For a document where visual presentation matters, even a small formatting change can be noticeable. Imagine creating a one-page announcement with a carefully arranged title, image, and contact details. You send the Word file, but the recipient's version of the software displays a different font. The text wraps differently, and the contact information moves to a second page. The information is still correct, but the presentation is no longer what you designed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why Convert Word to JPG?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Converting Word to JPG turns a finished document page into a visual image. A Word document contains editable text, formatting, objects, and document structure. A JPG represents the visible page as a flat image, which can be helpful when the visual appearance of the page matters more than its editability.&lt;/p&gt;

&lt;p&gt;For example, you might create an invitation in Word with a specific font, a custom layout, decorative elements, images, dates, and contact information. Instead of sharing the editable Word document, you can convert the finished page into a JPG so the recipient can focus on viewing the design rather than opening and editing the original file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When Is Word to JPG Conversion Useful?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Sharing a single page visually. A one-page document meant to be viewed rather than edited, such as a meeting notice or event announcement, works well as an image shared directly in a message or social post.&lt;/p&gt;

&lt;p&gt;Sharing through messaging platforms. Instead of asking someone to download and open a Word document, you can share a JPG that previews directly within many messaging apps.&lt;/p&gt;

&lt;p&gt;Creating social media content. Social platforms are primarily visual. An announcement, checklist, quotation, or informational page created in Word can be converted into an image to use as visual content.&lt;/p&gt;

&lt;p&gt;Preserving a finished visual layout. Sharing a page as an image reduces the chance that someone accidentally edits the document while viewing it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Word to JPG vs. Word to PDF: What's the Difference?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Word-to-JPG and Word-to-PDF conversions solve different problems. A PDF is still a document-oriented format, commonly used to distribute a finished document while maintaining its page structure. JPG turns the page into an image instead.&lt;/p&gt;

&lt;p&gt;If someone needs to revise the content, Word is usually appropriate. If someone needs to read a finished multi-page document, PDF is more practical. If the goal is to share a page visually, JPG is the better option. There isn't one universally correct format; the purpose of the file should decide the choice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;JPG Isn't Always the Right Choice&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Image conversion isn't a solution for every document. JPG isn't designed for convenient text editing or structured data manipulation. Converting a long report into dozens of JPG images can make it harder to search or copy information, and a spreadsheet containing calculations should generally stay in a spreadsheet format when the recipient needs to work with the data. A lengthy contract or formal document is also usually more convenient as a PDF than as a collection of images.&lt;/p&gt;

&lt;p&gt;Think of JPG as a visual-sharing format, not a replacement for every document format.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What Happens to Text, Tables, and Images When You Convert Word to JPG&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When a Word page is converted into a JPG, its visible content becomes part of the image and no longer behaves like the original editable document. Text becomes part of the image rather than remaining independently editable, and tables and images are rendered as part of the page.&lt;/p&gt;

&lt;p&gt;This preserves the appearance of a finished page but also creates a limitation: if you later find a spelling mistake, you can't edit the JPG text the way you would in Word. You would need to correct the original document and create a new image. This is why it's a good idea to keep the original Word file even after converting a copy to JPG.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to Keep Word-to-JPG Conversion Clear and Readable&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Conversion quality matters, particularly when the document contains small text or detailed tables. Before sharing a converted image, check:&lt;/p&gt;

&lt;p&gt;Resolution – make sure the text remains sharp enough to read&lt;br&gt;
Page dimensions – confirm the entire page is included and nothing important is cropped&lt;br&gt;
Layout – check that margins, images, spacing, and alignment look correct&lt;br&gt;
Readability – open the JPG at its intended viewing size and confirm the information is easy to understand&lt;/p&gt;

&lt;p&gt;For multi-page Word documents, also decide whether each page should become a separate JPG image.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consider the Platform Before Choosing a Format&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For email, Word, PDF, and JPG can all be appropriate depending on the purpose. For messaging, an image is often easier to preview when the recipient only needs to see a short visual document. For social media, an image usually works better since these platforms are built around visual content. For professional collaboration, Word remains the better choice because recipients may need to comment on or edit the content.&lt;/p&gt;

&lt;p&gt;Don't choose a format simply because it's familiar. Consider where the file is going and what the recipient is expected to do with it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't Forget About Privacy Before Sharing&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;File format is only one part of responsible document sharing. Before sending a document, review it for information that shouldn't be shared, including comments, tracked changes, document properties, hidden content, metadata, and unnecessary personal information.&lt;/p&gt;

&lt;p&gt;Converting a document to JPG doesn't automatically make sensitive information private. Anything visible on the original page will generally remain visible in the resulting image. If you use an online conversion service, also review its privacy and file-handling practices before uploading confidential documents.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Simplest Decision Rule&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the recipient needs to work on the document, keep it editable in Word. If they only need to see the finished page, consider a fixed format such as PDF or an image such as JPG.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Frequently Asked Questions&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Can I convert a Word document to JPG? Yes. A Word document can be converted into JPG images, with each page typically represented as an image. The exact process depends on the software or online conversion tool you use.&lt;/p&gt;

&lt;p&gt;Why would I convert Word to JPG? The main reason is visual sharing. JPG can be useful when you want to share a finished Word page as an image rather than provide an editable document.&lt;/p&gt;

&lt;p&gt;Is JPG better than Word for sharing? Not necessarily. Word is better when the recipient needs to edit or collaborate on the document. JPG can be more convenient when the recipient only needs to view visual content.&lt;/p&gt;

&lt;p&gt;Should I convert Word to JPG or PDF? It depends on the purpose. PDF is generally more suitable for document-style distribution, while JPG is useful when you want to share the page as visual content.&lt;/p&gt;

&lt;p&gt;Does converting Word to JPG preserve the formatting? A good conversion should reproduce the visible layout of the Word page as an image. Conversion quality can vary, so always check the resulting image for text clarity, cropping, spacing, and layout.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Final Thoughts&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A Word document isn't always the best format for sharing because the best format depends on what happens after you press Send. Word is excellent for creating, editing, reviewing, and collaborating on documents. But when a document is finished and the recipient only needs to view it, PDF or JPG may be more convenient.&lt;/p&gt;

&lt;p&gt;Before sharing any document, ask yourself: does the recipient need to edit this, or do they simply need to see it? That one question can help you choose between Word, PDF, JPG, or another format and make the sharing experience simpler for everyone.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>software</category>
      <category>productivity</category>
      <category>tools</category>
    </item>
    <item>
      <title>Why Server-Side PDF Generation Is Harder Than It Looks</title>
      <dc:creator>Simon Briggs</dc:creator>
      <pubDate>Tue, 15 Sep 2026 04:02:53 +0000</pubDate>
      <link>https://dev.to/simon_briggs_6607e52a5245/why-server-side-pdf-generation-is-harder-than-it-looks-36o4</link>
      <guid>https://dev.to/simon_briggs_6607e52a5245/why-server-side-pdf-generation-is-harder-than-it-looks-36o4</guid>
      <description>&lt;p&gt;It's 2 AM, and your on-call phone is buzzing. The invoice generation service that passed every test in staging is now throwing errors in production. Customers can't download their receipts. You SSH into the container, check the logs, and find the culprit: &lt;code&gt;Error: Failed to load font "Helvetica Neue"&lt;/code&gt;. The font exists on your laptop. It does not exist on the Docker image running in production. Nobody thought to check, because nobody expected a PDF to care.&lt;/p&gt;

&lt;p&gt;This is the moment most developers meet the real complexity of server-side PDF generation. On paper, it looks like a solved problem: take some data, run it through a library, get back a file. In practice, PDFs sit at an uncomfortable intersection of typography, layout engines, binary file formats, and server infrastructure, and each of those layers has its own way of breaking your afternoon.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Font Problem Nobody Warns You About
&lt;/h2&gt;

&lt;p&gt;Fonts are the single most common source of "it worked on my machine" bugs in PDF generation. Your local development environment has a full desktop OS with dozens of installed fonts. Your production server, especially if it's a slim Docker image based on Alpine or a minimal Debian build, has almost none.&lt;/p&gt;

&lt;p&gt;If your PDF library falls back silently when a font is missing, you get inconsistent output: text that looks fine locally renders in a generic fallback font in production, quietly breaking your brand guidelines. If it fails loudly, you get outages like the one above.&lt;/p&gt;

&lt;p&gt;The fix isn't glamorous, but it's non-negotiable: bundle the fonts you need directly into your build, and pin the exact font files rather than relying on font family names that might resolve differently across environments.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Don't rely on system fonts being present&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; ./fonts /usr/share/fonts/custom&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;fc-cache &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Headless Browsers Are Not Free
&lt;/h2&gt;

&lt;p&gt;A huge share of modern PDF generation runs through headless Chromium via Puppeteer or Playwright, because it's the easiest way to get accurate CSS rendering. This works beautifully in a demo and becomes a resource management problem at scale.&lt;/p&gt;

&lt;p&gt;Each browser instance can consume 100–300MB of memory, and spinning one up per request is a fast route to an out-of-memory crash under load. The common mistake is treating &lt;code&gt;page.pdf()&lt;/code&gt; like a stateless function call instead of what it actually is: launching and tearing down an entire browser process.&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;// This pattern will eventually take down your server under concurrent load&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/generate-pdf&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;puppeteer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
 &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newPage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
 &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setContent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
 &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pdf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;A4&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
 &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
 &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pdf&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;p&gt;A browser instance pool, with a hard cap on concurrent renders and a queue for overflow requests, is what actually survives production traffic. It's more code, but it's the difference between a feature and an incident.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layout Engines Don't Think in Pages
&lt;/h2&gt;

&lt;p&gt;Browsers were built to render infinite scrolling documents, not fixed-height pages. The moment you ask a browser-based renderer to paginate content, you inherit every quirk of how CSS handles page breaks: tables that split awkwardly mid-row, images that get sliced in half, and headers that repeat on every page whether you want them to or not.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;break-inside: avoid&lt;/code&gt; and &lt;code&gt;page-break-before&lt;/code&gt; help, but they're inconsistently supported depending on which rendering engine sits underneath your library. Testing a single template against a handful of edge cases (a table with 200 rows, a table with one row, an empty state) will save you from discovering these issues from an angry customer email instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Async Timing Trap
&lt;/h2&gt;

&lt;p&gt;This one catches almost everyone at least once. If your HTML content loads data asynchronously, fetches an image from a CDN, or renders a chart with JavaScript, your PDF renderer might snapshot the page before any of that finishes loading. The result is a PDF with blank spaces where charts should be, and it's maddening to debug because it happens intermittently, usually correlating with server load rather than anything in your code.&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;// Race condition waiting to happen&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setContent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pdf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Better: wait for network activity to settle&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setContent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;networkidle0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pdf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even &lt;code&gt;networkidle0&lt;/code&gt; isn't bulletproof for JS-rendered charts, so many teams add an explicit signal, like a &lt;code&gt;window.renderComplete = true&lt;/code&gt; flag set after their chart library finishes drawing, and poll for it before capturing the page.&lt;/p&gt;

&lt;h2&gt;
  
  
  File Size Creeps Up Silently
&lt;/h2&gt;

&lt;p&gt;A PDF with embedded high-resolution images and full font files can balloon in size fast, which matters when you're emailing invoices or serving downloads to users on slow connections. Compressing images before embedding them and subsetting fonts (including only the glyphs you actually use, not the entire character set) can cut file sizes by 60–80% with no visible quality loss. It's an easy win that most teams skip until someone complains about a 40MB invoice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where a Simpler Tool Actually Helps
&lt;/h2&gt;

&lt;p&gt;Not every PDF need justifies a server pipeline. A good chunk of the PDF work developers get asked for is one-off: convert a report to a different format, merge a couple of files, compress something before sending it. Standing up and maintaining a rendering service for that kind of task is overkill. For those cases, I've gotten into the habit of pointing teammates to &lt;a href="https://pdfconveter.com" rel="noopener noreferrer"&gt;PDF Conveter&lt;/a&gt;, a free browser-based toolkit that handles conversions, merging, and compression without needing infrastructure or code. It's not a replacement for a proper generation pipeline when you're producing PDFs programmatically at scale, but it removes a surprising amount of one-off busywork that would otherwise land on an engineer's plate.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Lesson
&lt;/h2&gt;

&lt;p&gt;Server-side PDF generation looks like a library integration problem until you're running it in production. Then it becomes a font management problem, a memory management problem, a layout engine quirks problem, and an async timing problem, all wearing the same trench coat. None of these are individually hard to solve, but each one is easy to miss until it costs you a 2 AM page.&lt;/p&gt;

&lt;p&gt;The teams that get this right treat PDF generation the same way they'd treat any other stateful, resource-intensive service: with pooling, monitoring, explicit timeouts, and tests that check actual rendered output, not just that the function didn't throw. Do that upfront, and the next 2 AM page will be about something else entirely.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>backend</category>
      <category>ai</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
