<?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: Nathmaxx</title>
    <description>The latest articles on DEV Community by Nathmaxx (@nathmaxx).</description>
    <link>https://dev.to/nathmaxx</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%2F3919390%2F7dc9e6b5-e8a7-4f48-93de-359fe92fe6ff.png</url>
      <title>DEV Community: Nathmaxx</title>
      <link>https://dev.to/nathmaxx</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nathmaxx"/>
    <language>en</language>
    <item>
      <title>Convert a whole folder of images to WebP — locally, no upload, one command</title>
      <dc:creator>Nathmaxx</dc:creator>
      <pubDate>Sat, 11 Jul 2026 07:40:47 +0000</pubDate>
      <link>https://dev.to/nathmaxx/convert-a-whole-folder-of-images-to-webp-locally-no-upload-one-command-42od</link>
      <guid>https://dev.to/nathmaxx/convert-a-whole-folder-of-images-to-webp-locally-no-upload-one-command-42od</guid>
      <description>&lt;p&gt;Every few months I hit the same wall: a folder full of &lt;code&gt;.jpg&lt;/code&gt; and &lt;code&gt;.png&lt;/code&gt; files that need to be WebP before they go anywhere near production. And every few months I catch myself doing the dumbest possible thing — dragging them one by one into some online converter, waiting for the upload, downloading them back, renaming them.&lt;/p&gt;

&lt;p&gt;It doesn't scale, and honestly I'm never thrilled about uploading a client's assets to a random SaaS just to save a few kilobytes.&lt;/p&gt;

&lt;p&gt;So the workflow below does the whole folder in &lt;strong&gt;one command, locally&lt;/strong&gt; — nothing leaves your machine, no account, originals left untouched. It runs on &lt;a href="https://assetopt.tech" rel="noopener noreferrer"&gt;&lt;code&gt;@assetopt/cli&lt;/code&gt;&lt;/a&gt; (open source, MIT), a small CLI I've been building that optimizes images &lt;em&gt;and&lt;/em&gt; CSS, JS and SVG in one pass.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why WebP at all
&lt;/h2&gt;

&lt;p&gt;WebP is typically &lt;strong&gt;25–35% smaller&lt;/strong&gt; than an equivalent JPEG or PNG at matched quality. It's one of the cheapest wins available for page weight — the format is universally supported now, and the only friction left is &lt;em&gt;doing it in bulk without a painful workflow&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @assetopt/cli
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or run it once, without installing anything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @assetopt/cli &lt;span class="nt"&gt;--help&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The one-command version
&lt;/h2&gt;

&lt;p&gt;Point &lt;code&gt;assetopt&lt;/code&gt; at a folder and it recurses into it. By default it &lt;strong&gt;recompresses images in their original format&lt;/strong&gt; — to turn JPG and PNG into WebP, you say so once in a config file.&lt;/p&gt;

&lt;p&gt;Create a &lt;code&gt;.assetoptrc&lt;/code&gt; that routes both source formats to WebP:&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;"images"&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;"formatMatrix"&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;"jpeg"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"webp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"png"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"webp"&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;"quality"&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;"webp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;82&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;span class="nl"&gt;"output"&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;"dir"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./optimized"&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;Then convert the folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;assetopt optimize ./images
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every &lt;code&gt;.jpg&lt;/code&gt; and &lt;code&gt;.png&lt;/code&gt; under &lt;code&gt;./images&lt;/code&gt; is written as &lt;code&gt;.webp&lt;/code&gt; into &lt;code&gt;./optimized&lt;/code&gt;, mirroring the source tree. Your originals stay exactly where they are.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preview the savings before touching your disk
&lt;/h2&gt;

&lt;p&gt;This is the part I actually use the most. &lt;code&gt;analyze&lt;/code&gt; reports the exact before/after sizes as a dry run — it writes nothing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;assetopt analyze ./images
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get a per-file table and a total. When the numbers look right, swap &lt;code&gt;analyze&lt;/code&gt; for &lt;code&gt;optimize&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tune the quality, fast
&lt;/h2&gt;

&lt;p&gt;WebP quality is a 1–100 dial (default &lt;code&gt;82&lt;/code&gt;). Lower means smaller files and more visible artifacts:&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;"images"&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;"formatMatrix"&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;"jpeg"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"webp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"png"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"webp"&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;"quality"&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;"webp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;75&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;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;Re-run &lt;code&gt;assetopt analyze ./images&lt;/code&gt; after each change. There's an &lt;strong&gt;incremental cache&lt;/strong&gt; on by default, so only the files you actually changed get re-encoded — tuning the quality dial is basically instant instead of re-crunching the whole folder each time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real numbers
&lt;/h2&gt;

&lt;p&gt;Running the config above on a sample pack (JPG/PNG → WebP):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After (WebP)&lt;/th&gt;
&lt;th&gt;Saved&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Large PNG export (opaque photo)&lt;/td&gt;
&lt;td&gt;5.42 MB&lt;/td&gt;
&lt;td&gt;552 KB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;−89.8%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JPEG photo&lt;/td&gt;
&lt;td&gt;1.74 MB&lt;/td&gt;
&lt;td&gt;1.44 MB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;−17.4%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The PNG loses the most, because a photo saved as PNG is hugely oversized to begin with — WebP is simply the right container for it. A photo already saved as JPEG is closer to optimal, so the win is smaller but still real. You can &lt;a href="https://assetopt.tech/#try" rel="noopener noreferrer"&gt;reproduce these exact figures&lt;/a&gt; by downloading the pack and running the commands above.&lt;/p&gt;

&lt;h2&gt;
  
  
  When WebP isn't the best target
&lt;/h2&gt;

&lt;p&gt;For &lt;strong&gt;transparent&lt;/strong&gt; PNGs (logos, icons), AVIF often keeps the alpha channel at a smaller size than WebP. assetopt can route by content — opaque → WebP, transparent → AVIF — automatically.&lt;/p&gt;




&lt;p&gt;That's the whole loop: preview, convert, done — locally, no upload. If you want the full write-up (config reference, wiring it into a build step, the CI quality gate), it lives here:&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://assetopt.tech/guides/convert-jpg-png-to-webp-bulk" rel="noopener noreferrer"&gt;Convert JPG &amp;amp; PNG to WebP in bulk — the full guide&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you give it a spin I'd genuinely like to hear what savings you get on your own assets — drop a comment.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>node</category>
      <category>webperf</category>
    </item>
    <item>
      <title>Stop writing TypeScript interfaces by hand - generate them from JSON, CSV, and OpenAPI in one command</title>
      <dc:creator>Nathmaxx</dc:creator>
      <pubDate>Fri, 08 May 2026 07:55:01 +0000</pubDate>
      <link>https://dev.to/nathmaxx/stop-writing-typescript-interfaces-by-hand-generate-them-from-json-csv-and-openapi-in-one-561h</link>
      <guid>https://dev.to/nathmaxx/stop-writing-typescript-interfaces-by-hand-generate-them-from-json-csv-and-openapi-in-one-561h</guid>
      <description>&lt;p&gt;You're integrating a third-party API. You open the docs, find a sample response, and start typing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Product&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// wait, is currency a string or an object?&lt;/span&gt;
    &lt;span class="c1"&gt;// what's the shape of variants[]?&lt;/span&gt;
    &lt;span class="c1"&gt;// does stock ever go null?&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Twenty minutes later you have a fragile interface you're not fully confident in — and the moment the API ships a new field, it's already stale.&lt;/p&gt;

&lt;p&gt;There's a better way.&lt;/p&gt;




&lt;h2&gt;
  
  
  snaptype: point it at the source, get types back
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://snaptype.dev" rel="noopener noreferrer"&gt;snaptype&lt;/a&gt; is a CLI that generates TypeScript interfaces and Zod schemas by reading your actual data — live API endpoints, OpenAPI specs, JSON files, CSV files.&lt;/p&gt;

&lt;p&gt;No config. No code to write. One command.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;code&gt;from-url&lt;/code&gt; — types straight from a live endpoint
&lt;/h2&gt;

&lt;p&gt;This is the one I use most. Give it any JSON endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx snaptype from-url https://api.example.com/products &lt;span class="nt"&gt;--zod&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; src/types/product.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;snaptype fetches the response and generates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Product&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;USD&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;EUR&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;GBP&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;stock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;variants&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Variant&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Variant&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;S&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;M&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;L&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;XL&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;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ProductSchema&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;id&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="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;price&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="na"&gt;currency&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;enum&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;USD&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;EUR&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;GBP&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
    &lt;span class="na"&gt;stock&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="nf"&gt;nullable&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;createdAt&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="nx"&gt;iso&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;variants&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;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;VariantSchema&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 few things worth noticing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Nested objects are expanded&lt;/strong&gt; — &lt;code&gt;variants[]&lt;/code&gt; became its own &lt;code&gt;Variant&lt;/code&gt; interface automatically&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enums are inferred&lt;/strong&gt; — &lt;code&gt;currency&lt;/code&gt; and &lt;code&gt;size&lt;/code&gt; had a small set of repeated values, so snaptype made them union types instead of &lt;code&gt;string&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nullables are detected&lt;/strong&gt; — &lt;code&gt;stock&lt;/code&gt; was &lt;code&gt;null&lt;/code&gt; in some responses, so it became &lt;code&gt;number | null&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Semantic types&lt;/strong&gt; — &lt;code&gt;createdAt&lt;/code&gt; became &lt;code&gt;z.iso.datetime()&lt;/code&gt;, not a generic &lt;code&gt;z.string()&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is inference against real data, not a naive field-by-field mapping.&lt;/p&gt;




&lt;h2&gt;
  
  
  Real-world example: GitHub API
&lt;/h2&gt;

&lt;p&gt;Point it at any real endpoint — here the GitHub user API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx snaptype from-url https://api.github.com/users/torvalds &lt;span class="nt"&gt;--zod&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; src/types/github-user.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Done. You now have typed access to every field in that response — including nested objects — with Zod schemas you can use directly for runtime validation.&lt;/p&gt;

&lt;p&gt;When GitHub ships a new field, re-run the command. No manual diffing.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;code&gt;from-openapi&lt;/code&gt; — one file per schema, from any spec
&lt;/h2&gt;

&lt;p&gt;If the API you're integrating has an OpenAPI or Swagger spec (most do), this is even better. Point it at the spec file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx snaptype from-openapi ./openapi.yaml &lt;span class="nt"&gt;-o&lt;/span&gt; src/types/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or directly at the hosted spec URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx snaptype from-openapi https://petstore3.swagger.io/api/v3/openapi.json &lt;span class="nt"&gt;-o&lt;/span&gt; src/types/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;snaptype reads every schema definition in the spec and generates one typed file per schema. YAML and JSON both work.&lt;/p&gt;

&lt;p&gt;For a spec that defines &lt;code&gt;Pet&lt;/code&gt;, &lt;code&gt;Order&lt;/code&gt;, and &lt;code&gt;User&lt;/code&gt;, you get three files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/types/Pet.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Pet&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt;        &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;photoUrls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="nl"&gt;status&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt;    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;available&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sold&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt;      &lt;span class="nx"&gt;Tag&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="nl"&gt;category&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt;  &lt;span class="nx"&gt;Category&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// src/types/Order.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Order&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt;        &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;petId&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt;     &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt;  &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;shipDate&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt;  &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;status&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt;    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;placed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;approved&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;delivered&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt;  &lt;span class="nx"&gt;boolean&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;Before, setting up a typed client from an OpenAPI spec meant:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Finding (or writing) a code generator&lt;/li&gt;
&lt;li&gt;Configuring it&lt;/li&gt;
&lt;li&gt;Running it&lt;/li&gt;
&lt;li&gt;Cleaning up the generated mess&lt;/li&gt;
&lt;li&gt;Updating it every time the spec changes&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now it's one command.&lt;/p&gt;




&lt;h2&gt;
  
  
  Works with private and authenticated APIs
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;One-off request&lt;/strong&gt; — pass headers directly with &lt;code&gt;-H&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx snaptype from-url https://api.example.com/me &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--zod&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; src/types/me.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Multiple endpoints on the same API&lt;/strong&gt; — store the headers in &lt;code&gt;.snaptyperc&lt;/code&gt; so you don't repeat yourself:&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;"baseUrl"&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://api.example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"headers"&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;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bearer my-token"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"X-Tenant"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"acme"&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;Then every &lt;code&gt;from-url&lt;/code&gt; call picks them up automatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx snaptype from-url /users &lt;span class="nt"&gt;-o&lt;/span&gt; src/types/user.ts
npx snaptype from-url /products &lt;span class="nt"&gt;-o&lt;/span&gt; src/types/product.ts
npx snaptype from-url /orders &lt;span class="nt"&gt;-o&lt;/span&gt; src/types/order.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Headers set in &lt;code&gt;.snaptyperc&lt;/code&gt; are defaults — anything passed with &lt;code&gt;-H&lt;/code&gt; on the CLI takes priority if the same key appears in both.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;stdin fallback&lt;/strong&gt; — if the endpoint requires a flow you can't replicate in a single request (custom auth, cookies, proxy), pipe the response in from curl:&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;-s&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; https://api.example.com/me &lt;span class="se"&gt;\&lt;/span&gt;
  | npx snaptype from-stdin &lt;span class="nt"&gt;--zod&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; src/types/me.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  JSON files and CSV too
&lt;/h2&gt;

&lt;p&gt;If you're working from a local file rather than a live endpoint:&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;# Local JSON file&lt;/span&gt;
npx snaptype from-json ./user.json &lt;span class="nt"&gt;--zod&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; src/types/user.ts

&lt;span class="c"&gt;# CSV export from a database or spreadsheet&lt;/span&gt;
npx snaptype from-csv ./orders.csv &lt;span class="nt"&gt;--zod&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; src/types/order.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The inference is the same — enums, nullables, semantic types, nested objects.&lt;/p&gt;




&lt;h2&gt;
  
  
  Zero config to start
&lt;/h2&gt;

&lt;p&gt;No setup needed. If you want to pin defaults, add a &lt;code&gt;.snaptyperc&lt;/code&gt;:&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;"zod"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"outDir"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"src/types"&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;Then every command just picks up those defaults automatically.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try it right now
&lt;/h2&gt;

&lt;p&gt;Run it directly with npx — no install needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx snaptype from-url https://api.github.com/users/torvalds &lt;span class="nt"&gt;--zod&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; src/types/github-user.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or add it as a dev dependency if you want a fixed version and faster runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; snaptype
snaptype from-url https://api.github.com/users/torvalds &lt;span class="nt"&gt;--zod&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; src/types/github-user.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Full docs at &lt;a href="https://snaptype.dev" rel="noopener noreferrer"&gt;snaptype.dev&lt;/a&gt; · Source on &lt;a href="https://github.com/Nathmaxx/snaptype" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If it saves you time, a ⭐ on the repo goes a long way — it helps other developers find the tool.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built this after one too many mornings spent re-typing interfaces from API docs. If from-url or from-openapi saves you time on a real project, drop a comment — I'm curious what APIs people are working with.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Feedback welcome
&lt;/h2&gt;

&lt;p&gt;snaptype is still early-stage and I'm actively shaping what it becomes. If you hit a use case it doesn't handle, a command that behaves unexpectedly, or a feature you'd find genuinely useful — open an issue on &lt;a href="https://github.com/Nathmaxx/snaptype/issues" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; or just leave a comment below. Every piece of feedback helps.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>tooling</category>
    </item>
  </channel>
</rss>
