<?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: Yatin Davra</title>
    <description>The latest articles on DEV Community by Yatin Davra (@yatindavra23).</description>
    <link>https://dev.to/yatindavra23</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%2F1224164%2F2746758e-8b79-4cab-9c0c-fd4056490f6f.png</url>
      <title>DEV Community: Yatin Davra</title>
      <link>https://dev.to/yatindavra23</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yatindavra23"/>
    <language>en</language>
    <item>
      <title>How I ended up contributing five ways to shape an LLM's output</title>
      <dc:creator>Yatin Davra</dc:creator>
      <pubDate>Fri, 03 Jul 2026 06:46:44 +0000</pubDate>
      <link>https://dev.to/yatindavra23/how-i-ended-up-contributing-five-ways-to-shape-an-llms-output-18cf</link>
      <guid>https://dev.to/yatindavra23/how-i-ended-up-contributing-five-ways-to-shape-an-llms-output-18cf</guid>
      <description>&lt;p&gt;A few months back I was on a project that needed LLMs to return actual structured data, not prose I'd have to regex my way through. Simple enough in theory, except every provider wanted the shape described differently - Anthropic's SDK wanted a tool schema, OpenAI had its own response-format, Ollama was basically raw prompting and hoping. I was rewriting the same schema three times per feature and writing my own retry-on-invalid-output loop for anything that didn't guarantee output.&lt;/p&gt;

&lt;p&gt;That's how I found &lt;a href="https://github.com/aviasoletechnologies/shapecraft" rel="noopener noreferrer"&gt;ShapeCraft&lt;/a&gt; (&lt;code&gt;@aviasole/shapecraft&lt;/code&gt; on npm). Define a schema once, hand it a model, get back validated data plus a &lt;code&gt;guaranteeLevel&lt;/code&gt; telling you how much to trust it - "guaranteed" means something different on OpenAI's native schema enforcement than on Anthropic's prompt-and-validate approach than on Ollama's token-level grammar constraints.&lt;/p&gt;

&lt;p&gt;It already supported Zod out of the box:&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;z&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="s2"&gt;zod&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;generate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;openai&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="s2"&gt;@aviasole/shapecraft&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;schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="na"&gt;score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&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;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;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;openai&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gpt-4o-mini&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That covered most of what I needed. But a few things in my actual project didn't fit "define a Zod schema," so instead of working around it, I opened some PRs.&lt;/p&gt;

&lt;p&gt;Part of the data I was extracting already had a JSON Schema, generated by another tool upstream. Rewriting it as Zod types just to satisfy the library felt like busywork, so that became the first addition - pass the JSON Schema straight 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;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;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;jsonSchema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="na"&gt;score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;number&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;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;name&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="s2"&gt;score&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;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I hit cases where I didn't need an object at all, just a string in a specific format. Wrapping that in an object schema felt like putting a hat on a hat, so next came a plain regex option:&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;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;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;\d{4}&lt;/span&gt;&lt;span class="sr"&gt;-&lt;/span&gt;&lt;span class="se"&gt;\d{2}&lt;/span&gt;&lt;span class="sr"&gt;-&lt;/span&gt;&lt;span class="se"&gt;\d{2}&lt;/span&gt;&lt;span class="sr"&gt;$/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;What is today's date?&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that, a rule that only made sense as logic, not a type - something like "required only if this other field equals X." Technically expressible as a schema, miserable to read. So I added a way to pass a validator function directly, plus a &lt;code&gt;hint&lt;/code&gt; so the model still has something to aim for:&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;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;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;hint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&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;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The one that mattered most to me was separate from this project entirely. I also do Tally/TDL and GST integration work, and that world runs on XML - nested tags, required fields buried a few levels deep. None of the above helped there since the output isn't JSON-shaped at all. So the last addition was template-based XML: give it an example with typed placeholders, model fills them in.&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;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;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;xml&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&amp;lt;book&amp;gt;
  &amp;lt;title&amp;gt;{string}&amp;lt;/title&amp;gt;
  &amp;lt;author&amp;gt;{string}&amp;lt;/author&amp;gt;
  &amp;lt;year&amp;gt;{number}&amp;lt;/year&amp;gt;
&amp;lt;/book&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;title&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="s2"&gt;author&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;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Extract: "Clean Code" by Robert C. Martin, 2008.&lt;/span&gt;&lt;span class="dl"&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 one took the most back-and-forth. Placeholders are deliberately limited to &lt;code&gt;{string}&lt;/code&gt;, &lt;code&gt;{number}&lt;/code&gt;, &lt;code&gt;{boolean}&lt;/code&gt;, and a typo throws before the model is even called - that's a template bug, not something worth a retry.&lt;/p&gt;

&lt;p&gt;The trickier part was literal text. Anything outside the &lt;code&gt;{}&lt;/code&gt; is supposed to survive untouched, but it's best-effort by default - a model will occasionally "improve" fixed text that reads like an instruction, since nothing marks it as untouchable. There's an &lt;code&gt;enforceLiterals: true&lt;/code&gt; flag that force-corrects every literal after the fact if you need that guaranteed. Or, often simpler, just leave the fixed value out of the template and splice it into &lt;code&gt;result.data&lt;/code&gt; yourself afterward.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;required&lt;/code&gt; also checks non-emptiness at any depth, so &lt;code&gt;&amp;lt;items&amp;gt;&amp;lt;/items&amp;gt;&lt;/code&gt; still triggers a retry instead of counting as present. And &lt;code&gt;arrays&lt;/code&gt; plus &lt;code&gt;parse: true&lt;/code&gt; gets you back a parsed JS object instead of raw XML, with specific nodes coerced into arrays even with a single item.&lt;/p&gt;

&lt;p&gt;One caveat worth knowing up front: XML generation is prompt-driven on every backend, no token-level grammar constraint like Ollama has for JSON. It leans on the model being capable, especially with deeply nested templates.&lt;/p&gt;

&lt;p&gt;All five of these route through the same &lt;code&gt;generate()&lt;/code&gt; call underneath - same retries, same &lt;code&gt;guaranteeLevel&lt;/code&gt;, same error types (&lt;code&gt;SchemaViolationError&lt;/code&gt;, &lt;code&gt;MaxRetriesExceededError&lt;/code&gt;). Different doors into the same room, which is probably why it was easy to keep adding them.&lt;/p&gt;

&lt;p&gt;Genuinely curious about a few things: if you've built template-based XML generation before, does &lt;code&gt;enforceLiterals&lt;/code&gt; feel like the right approach, or is there a cleaner way without a re-serialize pass? Is there a schema style still missing that you've actually needed in production - YAML, something protobuf-shaped? And for the validator path, is a &lt;code&gt;hint&lt;/code&gt; object enough context, or would you want more?&lt;/p&gt;

&lt;p&gt;Repo's here: &lt;a href="https://github.com/aviasoletechnologies/shapecraft" rel="noopener noreferrer"&gt;github.com/aviasoletechnologies/shapecraft&lt;/a&gt;. Package is &lt;code&gt;@aviasole/shapecraft&lt;/code&gt;. I'm using it across a few different projects now, so I'll keep adding to it as I run into more edge cases.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>node</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>How to Create Multi-Page TIFF Files in Node.js (Without ImageMagick)</title>
      <dc:creator>Yatin Davra</dc:creator>
      <pubDate>Tue, 14 Apr 2026 13:11:31 +0000</pubDate>
      <link>https://dev.to/yatindavra23/how-to-create-multi-page-tiff-files-in-nodejs-without-imagemagick-im0</link>
      <guid>https://dev.to/yatindavra23/how-to-create-multi-page-tiff-files-in-nodejs-without-imagemagick-im0</guid>
      <description>&lt;p&gt;If you've ever tried to create a multi-page TIFF in Node.js, you know the pain. Most solutions require ImageMagick as a system dependency, which is a headache in Docker, serverless environments, or CI pipelines. Others wrap native binaries with brittle &lt;code&gt;child_process&lt;/code&gt;calls.&lt;/p&gt;

&lt;p&gt;I ran into this exact problem while building a document processing pipeline — I needed to merge multiple scanned images into a single multi-page TIFF programmatically, with no external system dependencies.&lt;/p&gt;

&lt;p&gt;After digging through the options, I ended up writing a pure Node.js solution and published it as an npm package: &lt;code&gt;multi-page-tiff&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Problem&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Multi-page TIFF files store multiple images as linked Image File Directories (IFDs) inside a single .tiff file. Creating one correctly means writing the TIFF binary structure yourself — most image libraries only handle reading, not writing multi-page TIFFs.&lt;/p&gt;

&lt;p&gt;Common workarounds people try:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ImageMagick&lt;/strong&gt; via &lt;code&gt;child_process&lt;/code&gt;— works but requires ImageMagick installed, breaks in serverless&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sharp&lt;/code&gt; — great library but doesn't support writing multi-page TIFFs&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tiff-multipage&lt;/code&gt; — last published 3 years ago, minimal maintenance&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tiff-to-png&lt;/code&gt; — converts TIFF to PNG, not the other way&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Solution&lt;/p&gt;

&lt;p&gt;&lt;code&gt;npm install multi-page-tiff&lt;/code&gt;&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;imagesToTiff&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;imagesToTiffBuffer&lt;/span&gt; &lt;span class="p"&gt;}&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;multi-page-tiff&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;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;images&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&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;./page1.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&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;./page2.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&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;./page3.png&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="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;imagesToTiff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;images&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.tiff&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Get a Buffer (useful for uploading, streaming, etc.)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tiffBuffer&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;imagesToTiffBuffer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;images&lt;/span&gt;&lt;span class="p"&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;writeFileSync&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.tiff&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tiffBuffer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. No native binaries, no system dependencies. Works on AWS Lambda, Vercel, Docker, Windows — anywhere Node.js runs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use Cases&lt;/strong&gt;&lt;br&gt;
Multi-page TIFFs are common in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fax systems — fax files are almost always multi-page TIFFs&lt;/li&gt;
&lt;li&gt;Document scanning — scanners output multi-page TIFFs&lt;/li&gt;
&lt;li&gt;Medical imaging — DICOM-adjacent workflows often use TIFF&lt;/li&gt;
&lt;li&gt;Legal/archival — PDF alternatives for scanned documents&lt;/li&gt;
&lt;li&gt;Insurance and banking — document submission pipelines&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why Not Just Use PDF?&lt;/strong&gt;&lt;br&gt;
TIFF is lossless, supports higher bit depths (16-bit, 32-bit), and is required in many enterprise, government, and medical workflows where PDF is not accepted. If your system specification says TIFF, there's usually no substitute.&lt;/p&gt;

&lt;p&gt;Links&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;npm: &lt;a href="https://www.npmjs.com/package/multi-page-tiff" rel="noopener noreferrer"&gt;npmjs.com/package/multi-page-tiff&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/yatindavra/multi-page-tiff" rel="noopener noreferrer"&gt;github.com/yatindavra/multi-page-tiff&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've been fighting with multi-page TIFF generation in Node.js, give it a try and let me know in the comments if you run into any issues.&lt;br&gt;
check my work at &lt;a href="https://yatindavra.vercel.app" rel="noopener noreferrer"&gt;https://yatindavra.vercel.app&lt;/a&gt;&lt;/p&gt;

</description>
      <category>node</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>npm</category>
    </item>
  </channel>
</rss>
