<?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: 날다람쥐</title>
    <description>The latest articles on DEV Community by 날다람쥐 (@flyingsquirrel0419).</description>
    <link>https://dev.to/flyingsquirrel0419</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%2F3864074%2F66d516bd-8d6e-4ccb-bf9b-388546f0a65a.jpeg</url>
      <title>DEV Community: 날다람쥐</title>
      <link>https://dev.to/flyingsquirrel0419</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/flyingsquirrel0419"/>
    <language>en</language>
    <item>
      <title>claude-occ: Use Claude Code with Your Own Provider Gateway</title>
      <dc:creator>날다람쥐</dc:creator>
      <pubDate>Thu, 09 Jul 2026 15:00:36 +0000</pubDate>
      <link>https://dev.to/flyingsquirrel0419/claude-occ-use-claude-code-with-your-own-provider-gateway-45eh</link>
      <guid>https://dev.to/flyingsquirrel0419/claude-occ-use-claude-code-with-your-own-provider-gateway-45eh</guid>
      <description>&lt;h1&gt;
  
  
  claude-occ: Use Claude Code with Your Own Provider Gateway
&lt;/h1&gt;

&lt;p&gt;I built &lt;strong&gt;claude-occ&lt;/strong&gt;, a local gateway proxy that lets &lt;strong&gt;Claude Code&lt;/strong&gt; route requests through your own providers while keeping the normal &lt;code&gt;claude&lt;/code&gt; workflow.&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/flyingsquirrel0419/claude-occ" rel="noopener noreferrer"&gt;https://github.com/flyingsquirrel0419/claude-occ&lt;/a&gt;&lt;br&gt;&lt;br&gt;
npm: &lt;a href="https://www.npmjs.com/package/claude-occ" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/claude-occ&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If this looks useful, a GitHub star would help the project reach more Claude Code users:&lt;/p&gt;

&lt;p&gt;⭐ &lt;a href="https://github.com/flyingsquirrel0419/claude-occ" rel="noopener noreferrer"&gt;https://github.com/flyingsquirrel0419/claude-occ&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  What is claude-occ?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;claude-occ&lt;/code&gt; installs an &lt;code&gt;occ&lt;/code&gt; command that runs a local Claude Code-compatible gateway.&lt;/p&gt;

&lt;p&gt;Claude Code talks to a local &lt;code&gt;occ&lt;/code&gt; daemon through the Anthropic Messages API, and &lt;code&gt;occ&lt;/code&gt; routes requests to configured providers such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Anthropic-compatible APIs&lt;/li&gt;
&lt;li&gt;OpenAI-compatible APIs&lt;/li&gt;
&lt;li&gt;OpenRouter&lt;/li&gt;
&lt;li&gt;Google Gemini API&lt;/li&gt;
&lt;li&gt;Azure OpenAI&lt;/li&gt;
&lt;li&gt;local servers like Ollama, vLLM, or LM Studio&lt;/li&gt;
&lt;li&gt;custom OpenAI-compatible endpoints&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal is simple:&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;-g&lt;/span&gt; claude-occ
occ init
claude
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why I made it
&lt;/h2&gt;

&lt;p&gt;I liked the idea behind &lt;code&gt;opencodex&lt;/code&gt;, but Claude Code has a different integration surface.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;opencodex&lt;/code&gt; targets Codex and &lt;code&gt;/v1/responses&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;claude-occ&lt;/code&gt; targets Claude Code and &lt;code&gt;/v1/messages&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Claude Code already speaks the Anthropic Messages API, so a local authenticated gateway can sit between Claude Code and the provider you actually want to use.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;claude-occ&lt;/code&gt; can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;start a local gateway on &lt;code&gt;127.0.0.1&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;expose configured models through &lt;code&gt;/v1/models&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;show routed provider models in Claude Code's &lt;code&gt;/model&lt;/code&gt; picker&lt;/li&gt;
&lt;li&gt;route &lt;code&gt;/v1/messages&lt;/code&gt; to supported upstream APIs&lt;/li&gt;
&lt;li&gt;install a Claude launcher shim so &lt;code&gt;claude&lt;/code&gt; auto-starts the proxy&lt;/li&gt;
&lt;li&gt;restore native Claude Code behavior with &lt;code&gt;occ native&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;keep config in &lt;code&gt;~/.claude-occ/config.json&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;store provider keys directly or as environment-variable references&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example model IDs look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;openrouter/anthropic/claude-sonnet-5
umans/umans-coder
local/llama3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Important security boundary
&lt;/h2&gt;

&lt;p&gt;Claude Code subscription OAuth stays native-only.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;claude-occ&lt;/code&gt; does &lt;strong&gt;not&lt;/strong&gt; extract, forward, or reuse Claude Code subscription tokens as upstream credentials.&lt;/p&gt;

&lt;p&gt;If you want Claude Code's built-in subscription mode, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;occ native
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That stops the proxy and restores native Claude Code behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;

&lt;p&gt;Install from npm:&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;-g&lt;/span&gt; claude-occ
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;occ init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start Claude Code normally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The npm package is intentionally small. During install, it detects your OS and CPU architecture, then downloads the matching native &lt;code&gt;occ&lt;/code&gt; binary from GitHub Releases.&lt;/p&gt;

&lt;p&gt;Supported binaries include Linux, macOS, and Windows for x64 and arm64.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common commands
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;occ init
occ start
occ ensure
occ stop
occ restart
occ status
occ health
occ &lt;span class="nb"&gt;env
&lt;/span&gt;occ &lt;span class="nb"&gt;enable
&lt;/span&gt;occ native
occ restore back
occ uninstall
occ provider list
occ provider add openrouter &lt;span class="nt"&gt;--api-key&lt;/span&gt; &lt;span class="s1"&gt;'${OPENROUTER_API_KEY}'&lt;/span&gt; &lt;span class="nt"&gt;--set-default&lt;/span&gt;
occ models
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Current limitations
&lt;/h2&gt;

&lt;p&gt;This is a first public release, so some compatibility commands are intentionally lightweight:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;occ gui&lt;/code&gt; currently prints the local dashboard URL.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;occ service&lt;/code&gt; is compatibility-mode behavior, not a full OS service manager.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;occ sync-cache&lt;/code&gt; is a no-op because Claude Code does not expose the same writable model cache pattern as Codex.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I would rather document those limits clearly than pretend everything is finished.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it and give feedback
&lt;/h2&gt;

&lt;p&gt;If you use Claude Code and want to route it through your own providers, try it:&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;-g&lt;/span&gt; claude-occ
occ init
claude
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub repo:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/flyingsquirrel0419/claude-occ" rel="noopener noreferrer"&gt;https://github.com/flyingsquirrel0419/claude-occ&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If the project seems useful, please leave a star. It helps a lot with visibility and makes it easier to find early users who can test providers, report edge cases, and suggest better defaults.&lt;/p&gt;

&lt;p&gt;⭐ &lt;a href="https://github.com/flyingsquirrel0419/claude-occ" rel="noopener noreferrer"&gt;https://github.com/flyingsquirrel0419/claude-occ&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Feedback, issues, and provider compatibility reports are welcome.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>rust</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I turned a Claude Code-only web reader into a normal MCP server</title>
      <dc:creator>날다람쥐</dc:creator>
      <pubDate>Tue, 07 Jul 2026 14:26:13 +0000</pubDate>
      <link>https://dev.to/flyingsquirrel0419/i-turned-a-claude-code-only-web-reader-into-a-normal-mcp-server-2i9l</link>
      <guid>https://dev.to/flyingsquirrel0419/i-turned-a-claude-code-only-web-reader-into-a-normal-mcp-server-2i9l</guid>
      <description>&lt;p&gt;I kept running into the same problem while building with agents:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The page is public. A browser can read it. But my agent gets blocked, redirected, rate-limited, or handed a shell of HTML with no useful content.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There was already a fun project in this space: &lt;a href="https://github.com/fivetaku/insane-search" rel="noopener noreferrer"&gt;insane-search&lt;/a&gt;. It is clever, aggressive, and useful, but the original workflow is centered around Claude Code.&lt;/p&gt;

&lt;p&gt;I wanted the same kind of public-web resilience as a regular MCP server, so any MCP-compatible client could call it.&lt;/p&gt;

&lt;p&gt;That became &lt;strong&gt;unlimited-search&lt;/strong&gt;.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/flyingsquirrel0419" rel="noopener noreferrer"&gt;
        flyingsquirrel0419
      &lt;/a&gt; / &lt;a href="https://github.com/flyingsquirrel0419/unlimited-search" rel="noopener noreferrer"&gt;
        unlimited-search
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      MCP server and CLI for reading public web pages through resilient public routes.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;unlimited-search&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;English | &lt;a href="https://github.com/flyingsquirrel0419/unlimited-search/README.ko.md" rel="noopener noreferrer"&gt;한국어&lt;/a&gt; | &lt;a href="https://github.com/flyingsquirrel0419/unlimited-search/README.zh.md" rel="noopener noreferrer"&gt;中文&lt;/a&gt; | &lt;a href="https://github.com/flyingsquirrel0419/unlimited-search/README.ja.md" rel="noopener noreferrer"&gt;日本語&lt;/a&gt; | &lt;a href="https://github.com/flyingsquirrel0419/unlimited-search/README.es.md" rel="noopener noreferrer"&gt;Español&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/flyingsquirrel0419/unlimited-search/actions/workflows/test.yml" rel="noopener noreferrer"&gt;&lt;img src="https://github.com/flyingsquirrel0419/unlimited-search/actions/workflows/test.yml/badge.svg" alt="test"&gt;&lt;/a&gt;
&lt;a href="https://github.com/flyingsquirrel0419/unlimited-search/LICENSE" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/798509b4df525f56802b56f8096862487f08023e3d7561c68656f8dab10d0d6e/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4170616368652d2d322e302d626c75652e737667" alt="License: Apache-2.0"&gt;&lt;/a&gt;
&lt;a href="https://github.com/flyingsquirrel0419/unlimited-search/pyproject.toml" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/80e0513e4d59d218a9c8635abd0d685f4bfe282e479684f36b2354dfefab2d40/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f707974686f6e2d332e31322532422d626c75652e737667" alt="Python 3.12+"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;From public pages to usable signal.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;
  &lt;a rel="noopener noreferrer" href="https://github.com/flyingsquirrel0419/unlimited-search/assets/hero.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fflyingsquirrel0419%2Funlimited-search%2FHEAD%2Fassets%2Fhero.png" width="860" alt="unlimited-search hero showing public URLs routed through routes, HTTP, RSS, archives, and media into clean structured text for MCP and CLI."&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;&lt;code&gt;unlimited-search&lt;/code&gt; is a Python CLI and MCP server for reading public web content when a normal direct fetch is not enough. It combines platform public routes, browser-like HTTP identities, content fallbacks, public archive fallbacks, and media metadata extraction behind one local tool.&lt;/p&gt;

&lt;p&gt;It is built for agents and automation that need usable text from public URLs. It is not intended to bypass logins, paywalls, CAPTCHA, private networks, account restrictions, IP bans, or access controls.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Quickstart&lt;/h2&gt;
&lt;/div&gt;

&lt;p&gt;Prerequisite: Python 3.12 or newer.&lt;/p&gt;

&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;python -m pip install unlimited-search
unlimited-search &lt;span class="pl-c1"&gt;read&lt;/span&gt; https://en.wikipedia.org/wiki/OpenAI --max-content-chars 800&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;The command returns JSON with page &lt;code&gt;content&lt;/code&gt;, a &lt;code&gt;verdict&lt;/code&gt;, request &lt;code&gt;metadata&lt;/code&gt;, and an attempt &lt;code&gt;trace&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;For MCP clients, install the package and register the stdio server:&lt;/p&gt;
&lt;div class="highlight highlight-source-json notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;{
  &lt;span class="pl-ent"&gt;"mcpServers"&lt;/span&gt;: {
    &lt;span class="pl-ent"&gt;"unlimited-search"&lt;/span&gt;: {
      &lt;span class="pl-ent"&gt;"command"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;unlimited-search&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
      &lt;span class="pl-ent"&gt;"args"&lt;/span&gt;&lt;/pre&gt;…
&lt;/div&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/flyingsquirrel0419/unlimited-search" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;



&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;unlimited-search&lt;/code&gt; is a Python CLI and MCP server for reading public web pages through multiple public routes and fallbacks.&lt;/p&gt;

&lt;p&gt;It can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;read one public URL&lt;/li&gt;
&lt;li&gt;read many public URLs in one MCP call&lt;/li&gt;
&lt;li&gt;diagnose why a page is hard to access&lt;/li&gt;
&lt;li&gt;extract public media metadata with &lt;code&gt;yt-dlp&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;fall back through public platform routes, browser-like HTTP fetching, public archives, feeds, and metadata extraction&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It does &lt;strong&gt;not&lt;/strong&gt; try to bypass logins, paywalls, CAPTCHA, private networks, account restrictions, or real access controls.&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;python &lt;span class="nt"&gt;-m&lt;/span&gt; pip &lt;span class="nb"&gt;install &lt;/span&gt;unlimited-search
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then add it to your MCP client:&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;"mcpServers"&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;"unlimited-search"&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;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"unlimited-search"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&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="s2"&gt;"serve"&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;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;Restart your MCP client and you should get these tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;read_public_url&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;read_public_urls&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;diagnose_access&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;extract_media&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Quick test
&lt;/h2&gt;

&lt;p&gt;You can also use it directly from the terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;unlimited-search &lt;span class="nb"&gt;read &lt;/span&gt;https://example.com &lt;span class="nt"&gt;--max-content-chars&lt;/span&gt; 1000
unlimited-search diagnose https://example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output is JSON with the recovered content, final URL, verdict, metadata, and a trace of what was attempted.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I built it
&lt;/h2&gt;

&lt;p&gt;Most agents are good at reasoning over text once the text is in context.&lt;/p&gt;

&lt;p&gt;The annoying part is getting the text.&lt;/p&gt;

&lt;p&gt;Modern public websites are not consistent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;some have useful public APIs&lt;/li&gt;
&lt;li&gt;some render almost everything client-side&lt;/li&gt;
&lt;li&gt;some expose RSS or Atom feeds&lt;/li&gt;
&lt;li&gt;some have Open Graph or JSON-LD metadata but thin HTML&lt;/li&gt;
&lt;li&gt;some work only with a browser-like TLS identity&lt;/li&gt;
&lt;li&gt;some are easier to read through a public archive snapshot&lt;/li&gt;
&lt;li&gt;some media pages are better handled as metadata, not raw HTML&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So instead of pretending every URL is the same, &lt;code&gt;unlimited-search&lt;/code&gt; tries a layered strategy.&lt;/p&gt;

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

&lt;p&gt;For a URL, the reader tries the least invasive public route first:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Platform-specific public routes for sites like Reddit, X/Twitter, YouTube metadata, Bluesky, Mastodon, Hacker News, Stack Overflow, Wikipedia, GitHub, npm, PyPI, Naver Blog, Google Scholar, Amazon, and more.&lt;/li&gt;
&lt;li&gt;Browser-like HTTP fetching with multiple identities, URL variants, referer strategies, and HTTP transport fallback.&lt;/li&gt;
&lt;li&gt;Content fallbacks such as Jina Reader JSON, RSS/Atom discovery, common feed paths, OGP, JSON-LD, Schema.org, and Next.js metadata.&lt;/li&gt;
&lt;li&gt;Public archive fallbacks through Wayback and archive.today/archive.ph best-effort snapshots.&lt;/li&gt;
&lt;li&gt;Media metadata extraction through &lt;code&gt;yt-dlp&lt;/code&gt; for public media URLs.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Every result includes a verdict:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;strong_ok&lt;/code&gt;: a high-confidence public route worked&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;weak_ok&lt;/code&gt;: content was recovered, but may be partial or fallback-based&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;suspect_ok&lt;/code&gt;: something useful was found, but the caller should inspect it&lt;/li&gt;
&lt;li&gt;failure verdicts for blocked, unsafe, login-only, CAPTCHA, private-network, or unusable results&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Example MCP call
&lt;/h2&gt;

&lt;p&gt;Ask your MCP client something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Use unlimited-search to read this public URL and summarize the page:
https://example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Behind the scenes, the model can call:&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;"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://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;"max_content_chars"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4000&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;And get back content plus a trace, not just a silent failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  What makes it different from a normal fetch?
&lt;/h2&gt;

&lt;p&gt;A normal fetch asks:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can I GET this URL?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;code&gt;unlimited-search&lt;/code&gt; asks:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Is there a legitimate public route to useful text for this URL?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That difference matters for agent workflows.&lt;/p&gt;

&lt;p&gt;If the direct HTML fetch is bad, the tool can try a public API. If the page is metadata-rich but content-light, it can salvage metadata. If the live page is flaky, it can try public archive routes. If the page is media, it can return structured media metadata instead of dumping a giant HTML shell.&lt;/p&gt;

&lt;p&gt;The goal is not to be magical. The goal is to be practical.&lt;/p&gt;

&lt;h2&gt;
  
  
  Safety boundary
&lt;/h2&gt;

&lt;p&gt;This is important: &lt;code&gt;unlimited-search&lt;/code&gt; is a &lt;strong&gt;public-content reader&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It is designed to stop when the target requires:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;authentication&lt;/li&gt;
&lt;li&gt;payment&lt;/li&gt;
&lt;li&gt;CAPTCHA&lt;/li&gt;
&lt;li&gt;private network access&lt;/li&gt;
&lt;li&gt;account-specific authorization&lt;/li&gt;
&lt;li&gt;hard anti-abuse bypassing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It also rejects private, loopback, link-local, multicast, reserved, and metadata-service targets by default.&lt;/p&gt;

&lt;p&gt;That boundary makes the tool useful for agents without turning it into a bypass tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  The insane-search inspiration
&lt;/h2&gt;

&lt;p&gt;I like the spirit of &lt;a href="https://github.com/fivetaku/insane-search" rel="noopener noreferrer"&gt;insane-search&lt;/a&gt;: make public web reading less fragile for agent workflows.&lt;/p&gt;

&lt;p&gt;The difference is packaging and interface.&lt;/p&gt;

&lt;p&gt;Instead of keeping the workflow tied to one agent environment, &lt;code&gt;unlimited-search&lt;/code&gt; exposes the capability as a regular MCP server:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;install with &lt;code&gt;pip&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;run over stdio&lt;/li&gt;
&lt;li&gt;call from any MCP-compatible client&lt;/li&gt;
&lt;li&gt;inspect traces and verdicts&lt;/li&gt;
&lt;li&gt;use it as a CLI when you do not need MCP&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That makes it easier to plug into different local agent setups.&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; pip &lt;span class="nb"&gt;install &lt;/span&gt;unlimited-search
unlimited-search &lt;span class="nb"&gt;read &lt;/span&gt;https://dev.to &lt;span class="nt"&gt;--max-content-chars&lt;/span&gt; 1500
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/flyingsquirrel0419/unlimited-search" rel="noopener noreferrer"&gt;https://github.com/flyingsquirrel0419/unlimited-search&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;PyPI:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pypi.org/project/unlimited-search/" rel="noopener noreferrer"&gt;https://pypi.org/project/unlimited-search/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If this saves you from writing yet another fragile page-fetching tool, a GitHub star would help the project get discovered.&lt;/p&gt;

&lt;p&gt;And if you test it against a weird public site, I would love to hear what worked, what failed, and what fallback should be added next.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>opensource</category>
      <category>python</category>
    </item>
    <item>
      <title>Most apps do not need a full date library, so I built a 2KB one</title>
      <dc:creator>날다람쥐</dc:creator>
      <pubDate>Sun, 21 Jun 2026 07:56:53 +0000</pubDate>
      <link>https://dev.to/flyingsquirrel0419/most-apps-do-not-need-a-full-date-library-so-i-built-a-2kb-one-2egp</link>
      <guid>https://dev.to/flyingsquirrel0419/most-apps-do-not-need-a-full-date-library-so-i-built-a-2kb-one-2egp</guid>
      <description>&lt;p&gt;Most frontend and Node.js apps need date utilities.&lt;/p&gt;

&lt;p&gt;But in many projects, the actual needs are pretty small:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;format a date&lt;/li&gt;
&lt;li&gt;parse an ISO string&lt;/li&gt;
&lt;li&gt;add a few days or months&lt;/li&gt;
&lt;li&gt;compare two dates&lt;/li&gt;
&lt;li&gt;get the start or end of a day/week/month&lt;/li&gt;
&lt;li&gt;check weekends or leap years&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For those common cases, pulling in a larger date library can feel heavier than necessary.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;date-light&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is date-light?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;date-light&lt;/code&gt; is a small, zero-dependency date utility library for TypeScript and JavaScript.&lt;/p&gt;

&lt;p&gt;It provides &lt;strong&gt;39 common date helpers&lt;/strong&gt; in about &lt;strong&gt;2.18KB minzipped&lt;/strong&gt;.&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;date-light
ts
import &lt;span class="o"&gt;{&lt;/span&gt; addDays, differenceInDays, format, startOfWeek &lt;span class="o"&gt;}&lt;/span&gt; from &lt;span class="s2"&gt;"date-light"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

const launch &lt;span class="o"&gt;=&lt;/span&gt; new Date&lt;span class="o"&gt;(&lt;/span&gt;2026, 5, 30, 14, 30&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
const reminder &lt;span class="o"&gt;=&lt;/span&gt; addDays&lt;span class="o"&gt;(&lt;/span&gt;launch, &lt;span class="nt"&gt;-7&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

format&lt;span class="o"&gt;(&lt;/span&gt;launch, &lt;span class="s2"&gt;"yyyy-MM-dd HH:mm"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
// &lt;span class="s2"&gt;"2026-06-30 14:30"&lt;/span&gt;

format&lt;span class="o"&gt;(&lt;/span&gt;startOfWeek&lt;span class="o"&gt;(&lt;/span&gt;launch&lt;span class="o"&gt;)&lt;/span&gt;, &lt;span class="s2"&gt;"yyyy-MM-dd"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
// &lt;span class="s2"&gt;"2026-06-29"&lt;/span&gt;

differenceInDays&lt;span class="o"&gt;(&lt;/span&gt;launch, reminder&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
// 7
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also try it in the docs/playground site:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://date-light.flyingsquirrel.me" rel="noopener noreferrer"&gt;https://date-light.flyingsquirrel.me&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I made it
&lt;/h2&gt;

&lt;p&gt;I like &lt;code&gt;date-fns&lt;/code&gt;. It has a great API and covers a huge range of date operations.&lt;/p&gt;

&lt;p&gt;But most apps I work on only use a small subset of that surface area.&lt;/p&gt;

&lt;p&gt;Common examples:&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="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;yyyy-MM-dd&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;parseISO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-01-15&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;addDays&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;addMonths&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&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="nf"&gt;differenceInDays&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;startOfMonth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;endOfMonth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;isSameDay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I wanted a tiny library that focused on those common operations and avoided everything else:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;no locale system&lt;/li&gt;
&lt;li&gt;no timezone database&lt;/li&gt;
&lt;li&gt;no plugins&lt;/li&gt;
&lt;li&gt;no chainable wrapper&lt;/li&gt;
&lt;li&gt;no runtime dependencies&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Just the date helpers most apps actually ship.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it includes
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;date-light&lt;/code&gt; currently includes helpers for:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Area&lt;/th&gt;
&lt;th&gt;Examples&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Format &amp;amp; parse&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;format&lt;/code&gt;, &lt;code&gt;parseISO&lt;/code&gt;, &lt;code&gt;parse&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Add &amp;amp; subtract&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;addDays&lt;/code&gt;, &lt;code&gt;addMonths&lt;/code&gt;, &lt;code&gt;addYears&lt;/code&gt;, &lt;code&gt;subDays&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Difference&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;differenceInDays&lt;/code&gt;, &lt;code&gt;differenceInMonths&lt;/code&gt;, &lt;code&gt;differenceInYears&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compare&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;isBefore&lt;/code&gt;, &lt;code&gt;isAfter&lt;/code&gt;, &lt;code&gt;isEqual&lt;/code&gt;, &lt;code&gt;isSameDay&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Query&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;isWeekend&lt;/code&gt;, &lt;code&gt;isLeapYear&lt;/code&gt;, &lt;code&gt;isValid&lt;/code&gt;, &lt;code&gt;getDaysInMonth&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Start &amp;amp; end&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;startOfDay&lt;/code&gt;, &lt;code&gt;startOfWeek&lt;/code&gt;, &lt;code&gt;endOfMonth&lt;/code&gt;, &lt;code&gt;endOfYear&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All functions are typed and return new &lt;code&gt;Date&lt;/code&gt; instances instead of mutating the input.&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;addMonths&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;date-light&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;original&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;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2026&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;31&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;next&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;addMonths&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;original&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;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;original&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// Jan 31, 2026&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;next&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// Feb 28, 2026&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Handling date edge cases
&lt;/h2&gt;

&lt;p&gt;Dates have a lot of sharp edges, so I added tests for the cases that usually cause problems.&lt;/p&gt;

&lt;p&gt;For example, month-end clamping:&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="nf"&gt;addMonths&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2026&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;31&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="c1"&gt;// Feb 28, 2026&lt;/span&gt;

&lt;span class="nf"&gt;addMonths&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2024&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;31&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="c1"&gt;// Feb 29, 2024&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Leap years:&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="nf"&gt;isLeapYear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2024&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;1&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="c1"&gt;// true&lt;/span&gt;

&lt;span class="nf"&gt;isLeapYear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1900&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;1&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="c1"&gt;// false&lt;/span&gt;

&lt;span class="nf"&gt;isLeapYear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2000&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;1&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="c1"&gt;// true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And date-only ISO parsing:&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="nf"&gt;parseISO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-01-15&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// local midnight, matching date-fns behavior&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Package support
&lt;/h2&gt;

&lt;p&gt;The package ships with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ESM&lt;/li&gt;
&lt;li&gt;CommonJS&lt;/li&gt;
&lt;li&gt;TypeScript declarations&lt;/li&gt;
&lt;li&gt;zero runtime dependencies&lt;/li&gt;
&lt;li&gt;tree-shakeable exports&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The package entrypoints are tested before release.&lt;/p&gt;

&lt;h2&gt;
  
  
  Size
&lt;/h2&gt;

&lt;p&gt;The current library build is about &lt;strong&gt;2.18KB minzipped&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That is the main goal of the project: keep the useful common date helpers small enough that you do not have to think much about adding them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What date-light is not
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;date-light&lt;/code&gt; is intentionally not a complete date platform.&lt;/p&gt;

&lt;p&gt;It does not try to replace:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;full &lt;code&gt;date-fns&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Luxon&lt;/li&gt;
&lt;li&gt;Temporal&lt;/li&gt;
&lt;li&gt;timezone-aware libraries&lt;/li&gt;
&lt;li&gt;advanced locale formatting&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you need full i18n, timezone conversions, duration objects, or complex calendar logic, you should use a more complete library.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;date-light&lt;/code&gt; is for the smaller common case:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“I just need the date helpers I use all the time.”&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;p&gt;Docs and playground:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://date-light.flyingsquirrel.me" rel="noopener noreferrer"&gt;https://date-light.flyingsquirrel.me&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;GitHub:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/flyingsquirrel0419/date-light" rel="noopener noreferrer"&gt;https://github.com/flyingsquirrel0419/date-light&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;npm:&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;date-light
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example:&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;format&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;parseISO&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;addDays&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;differenceInDays&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;startOfMonth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;endOfMonth&lt;/span&gt;&lt;span class="p"&gt;,&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;date-light&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;date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseISO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-06-30&lt;/span&gt;&lt;span class="dl"&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="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;yyyy-MM-dd&lt;/span&gt;&lt;span class="dl"&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="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;startOfMonth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;yyyy-MM-dd&lt;/span&gt;&lt;span class="dl"&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="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;endOfMonth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;yyyy-MM-dd&lt;/span&gt;&lt;span class="dl"&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="nf"&gt;differenceInDays&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;addDays&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;date&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 try it, feedback is welcome.&lt;/p&gt;

&lt;p&gt;And if you find the project useful, a GitHub star would help a lot:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/flyingsquirrel0419/date-light" rel="noopener noreferrer"&gt;https://github.com/flyingsquirrel0419/date-light&lt;/a&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>typescript</category>
      <category>npm</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I built date-light: a tiny zero-dependency date utility for JavaScript</title>
      <dc:creator>날다람쥐</dc:creator>
      <pubDate>Sat, 06 Jun 2026 17:33:59 +0000</pubDate>
      <link>https://dev.to/flyingsquirrel0419/i-built-date-light-a-tiny-zero-dependency-date-utility-for-javascript-2157</link>
      <guid>https://dev.to/flyingsquirrel0419/i-built-date-light-a-tiny-zero-dependency-date-utility-for-javascript-2157</guid>
      <description>&lt;p&gt;I built &lt;strong&gt;date-light&lt;/strong&gt;, a small zero-dependency date utility library for JavaScript and TypeScript.&lt;/p&gt;

&lt;p&gt;I wanted something focused on the common date operations I use most often: formatting, parsing, date math, comparisons, and start/end helpers, without pulling in a larger date library for simple use cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it includes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;39 date utility functions&lt;/li&gt;
&lt;li&gt;Zero runtime dependencies&lt;/li&gt;
&lt;li&gt;TypeScript types&lt;/li&gt;
&lt;li&gt;ESM and CommonJS builds&lt;/li&gt;
&lt;li&gt;date-fns-style format tokens&lt;/li&gt;
&lt;li&gt;A docs/playground site for quickly trying the API&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Playground
&lt;/h2&gt;

&lt;p&gt;You can try it here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://date-light.flyingsquirrel.me/?v=20260607" rel="noopener noreferrer"&gt;https://date-light.flyingsquirrel.me/?v=20260607&lt;/a&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;date-light
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It also works with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;yarn add date-light
pnpm add date-light
bun add date-light
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Example
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&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;format&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;addDays&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;differenceInDays&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;date-light&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;today&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;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-06-07T10:30:00&lt;/span&gt;&lt;span class="dl"&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="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;today&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;yyyy-MM-dd&lt;/span&gt;&lt;span class="dl"&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="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;addDays&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;today&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;yyyy-MM-dd&lt;/span&gt;&lt;span class="dl"&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="nf"&gt;differenceInDays&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-06-14&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;today&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why I made it
&lt;/h2&gt;

&lt;p&gt;For many projects, I only need a focused set of date helpers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Format a date for UI&lt;/li&gt;
&lt;li&gt;Parse an ISO or pattern-based date string&lt;/li&gt;
&lt;li&gt;Add or subtract days, months, years, hours, minutes, or seconds&lt;/li&gt;
&lt;li&gt;Compare two dates&lt;/li&gt;
&lt;li&gt;Snap dates to day, week, month, or year boundaries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wanted the package to stay small, typed, and easy to evaluate before installing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;p&gt;Docs/playground:&lt;br&gt;
&lt;a href="https://date-light.flyingsquirrel.me/?v=20260607" rel="noopener noreferrer"&gt;https://date-light.flyingsquirrel.me/?v=20260607&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;GitHub:&lt;br&gt;
&lt;a href="https://github.com/flyingsquirrel0419/date-light" rel="noopener noreferrer"&gt;https://github.com/flyingsquirrel0419/date-light&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;npm:&lt;br&gt;
&lt;a href="https://www.npmjs.com/package/date-light" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/date-light&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I would love feedback on the API naming, date edge cases, and whether the playground/docs make the package easy to evaluate.&lt;/p&gt;

&lt;p&gt;If you find the project useful, a GitHub star would also help other developers discover it.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>typescript</category>
      <category>opensource</category>
    </item>
    <item>
      <title>date-light: A 1.8KB Alternative to date-fns You Might Actually Like</title>
      <dc:creator>날다람쥐</dc:creator>
      <pubDate>Sat, 30 May 2026 14:46:20 +0000</pubDate>
      <link>https://dev.to/flyingsquirrel0419/date-light-a-18kb-alternative-to-date-fns-you-might-actually-like-48fk</link>
      <guid>https://dev.to/flyingsquirrel0419/date-light-a-18kb-alternative-to-date-fns-you-might-actually-like-48fk</guid>
      <description>&lt;p&gt;If you've ever shipped a frontend app and watched your bundle bloat because of date utilities, you're not alone. &lt;code&gt;date-fns&lt;/code&gt; is fantastic — but importing 20 functions costs you 18 KB. &lt;code&gt;dayjs&lt;/code&gt; is lighter at 3 KB, but it wraps every &lt;code&gt;Date&lt;/code&gt; in a mutable object.&lt;/p&gt;

&lt;p&gt;I wanted something different: &lt;strong&gt;pure functions, zero dependencies, and a bundle so small you forget it's there.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's &lt;code&gt;date-light&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is it?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;date-light&lt;/code&gt; is a zero-dependency, fully typed date utility library for JavaScript and TypeScript. It covers the 20 most-used &lt;code&gt;date-fns&lt;/code&gt; operations with &lt;strong&gt;39 functions&lt;/strong&gt; — and the entire thing weighs &lt;strong&gt;1.79 KB minzipped&lt;/strong&gt;.&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;date-light
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&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;format&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;addDays&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;differenceInDays&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isBefore&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;startOfDay&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;date-light&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;date&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;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2026&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="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;yyyy-MM-dd HH:mm:ss&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// "2026-06-30 14:30:45"&lt;/span&gt;
&lt;span class="nf"&gt;addDays&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Tue Jul 07 2026&lt;/span&gt;
&lt;span class="nf"&gt;differenceInDays&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2026&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;1&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// 180&lt;/span&gt;
&lt;span class="nf"&gt;isBefore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt; &lt;span class="c1"&gt;// true&lt;/span&gt;
&lt;span class="nf"&gt;startOfDay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Mon Jun 30 2026 00:00:00&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same pattern syntax as &lt;code&gt;date-fns&lt;/code&gt;. Most projects can migrate with a single import change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not just use date-fns?
&lt;/h2&gt;

&lt;p&gt;You absolutely can, and if you need all 252 functions, you should. But for most projects, you use maybe 15-20. Here's what that costs:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Library&lt;/th&gt;
&lt;th&gt;minzipped&lt;/th&gt;
&lt;th&gt;What you get&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;date-light&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.79 KB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;39 functions, fully tree-shakeable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dayjs&lt;/td&gt;
&lt;td&gt;2.97 KB&lt;/td&gt;
&lt;td&gt;Core only (plugins add more)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;date-fns (20 functions)&lt;/td&gt;
&lt;td&gt;18.34 KB&lt;/td&gt;
&lt;td&gt;Just the 20 most-used functions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;date-fns (full)&lt;/td&gt;
&lt;td&gt;261.3 KB&lt;/td&gt;
&lt;td&gt;All 252 functions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's a &lt;strong&gt;10x size difference&lt;/strong&gt; for the same functionality most people actually use.&lt;/p&gt;

&lt;h2&gt;
  
  
  How fast is it?
&lt;/h2&gt;

&lt;p&gt;Benchmarks on Node.js 24 — nanoseconds per operation (lower = better):&lt;/p&gt;

&lt;h3&gt;
  
  
  vs date-fns v4
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Function&lt;/th&gt;
&lt;th&gt;date-light&lt;/th&gt;
&lt;th&gt;date-fns&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;format&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;668 ns&lt;/td&gt;
&lt;td&gt;1,495 ns&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.2x faster&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;parseISO&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;138 ns&lt;/td&gt;
&lt;td&gt;1,218 ns&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8.8x faster&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;differenceInDays&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;117 ns&lt;/td&gt;
&lt;td&gt;946 ns&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8.1x faster&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;isWeekend&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;4 ns&lt;/td&gt;
&lt;td&gt;39 ns&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;9.8x faster&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;isLeapYear&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;5 ns&lt;/td&gt;
&lt;td&gt;41 ns&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8.2x faster&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;startOfDay&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;47 ns&lt;/td&gt;
&lt;td&gt;49 ns&lt;/td&gt;
&lt;td&gt;~same&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  vs dayjs v1
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Function&lt;/th&gt;
&lt;th&gt;date-light&lt;/th&gt;
&lt;th&gt;dayjs&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;addDays&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;80 ns&lt;/td&gt;
&lt;td&gt;642 ns&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8x faster&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;addMonths&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;133 ns&lt;/td&gt;
&lt;td&gt;2,067 ns&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;15.5x faster&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;isBefore&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;56 ns&lt;/td&gt;
&lt;td&gt;531 ns&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;9.5x faster&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Why so fast? &lt;code&gt;date-light&lt;/code&gt; calls native &lt;code&gt;Date&lt;/code&gt; methods directly. No wrapper objects (dayjs), no shared internal modules (date-fns). Pure functions with zero overhead.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's included?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Format &amp;amp; Parse:&lt;/strong&gt; &lt;code&gt;format&lt;/code&gt;, &lt;code&gt;parseISO&lt;/code&gt;, &lt;code&gt;parse&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Add &amp;amp; Subtract:&lt;/strong&gt; &lt;code&gt;addDays&lt;/code&gt;, &lt;code&gt;addMonths&lt;/code&gt;, &lt;code&gt;addYears&lt;/code&gt;, &lt;code&gt;addHours&lt;/code&gt;, &lt;code&gt;addMinutes&lt;/code&gt;, &lt;code&gt;addSeconds&lt;/code&gt;, &lt;code&gt;subDays&lt;/code&gt;, &lt;code&gt;subMonths&lt;/code&gt;, &lt;code&gt;subYears&lt;/code&gt;, &lt;code&gt;subHours&lt;/code&gt;, &lt;code&gt;subMinutes&lt;/code&gt;, &lt;code&gt;subSeconds&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Difference:&lt;/strong&gt; &lt;code&gt;differenceInDays&lt;/code&gt;, &lt;code&gt;differenceInHours&lt;/code&gt;, &lt;code&gt;differenceInMinutes&lt;/code&gt;, &lt;code&gt;differenceInSeconds&lt;/code&gt;, &lt;code&gt;differenceInMonths&lt;/code&gt;, &lt;code&gt;differenceInYears&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compare:&lt;/strong&gt; &lt;code&gt;isBefore&lt;/code&gt;, &lt;code&gt;isAfter&lt;/code&gt;, &lt;code&gt;isEqual&lt;/code&gt;, &lt;code&gt;isSameDay&lt;/code&gt;, &lt;code&gt;isSameMonth&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Query:&lt;/strong&gt; &lt;code&gt;isWeekend&lt;/code&gt;, &lt;code&gt;isLeapYear&lt;/code&gt;, &lt;code&gt;isValid&lt;/code&gt;, &lt;code&gt;getDaysInMonth&lt;/code&gt;, &lt;code&gt;getWeekOfYear&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Start &amp;amp; End:&lt;/strong&gt; &lt;code&gt;startOfDay&lt;/code&gt;, &lt;code&gt;endOfDay&lt;/code&gt;, &lt;code&gt;startOfWeek&lt;/code&gt;, &lt;code&gt;endOfWeek&lt;/code&gt;, &lt;code&gt;startOfMonth&lt;/code&gt;, &lt;code&gt;endOfMonth&lt;/code&gt;, &lt;code&gt;startOfYear&lt;/code&gt;, &lt;code&gt;endOfYear&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Design decisions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;date-fns compatible pattern syntax.&lt;/strong&gt; &lt;code&gt;yyyy-MM-dd&lt;/code&gt;, not &lt;code&gt;YYYY-MM-DD&lt;/code&gt;. If you're migrating from &lt;code&gt;date-fns&lt;/code&gt;, most code works as-is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pure functions, immutable.&lt;/strong&gt; Every function returns a new &lt;code&gt;Date&lt;/code&gt;. No mutation, no surprises.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Named exports only.&lt;/strong&gt; No default export, no class instances. Tree-shaking works perfectly out of the box.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Month clamping matches date-fns.&lt;/strong&gt; &lt;code&gt;addMonths(new Date(2026, 0, 31), 1)&lt;/code&gt; returns Feb 28, not Mar 3. Same behavior you'd expect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Calendar days vs physical time.&lt;/strong&gt; &lt;code&gt;addDays&lt;/code&gt; uses calendar-day semantics (preserves time-of-day across DST). &lt;code&gt;addHours&lt;/code&gt; uses physical time (exact N hours later). This is the correct behavior — and it's what &lt;code&gt;date-fns&lt;/code&gt; does too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migration from date-fns
&lt;/h2&gt;

&lt;p&gt;One import change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- import { format, addDays, differenceInDays } from 'date-fns';
&lt;/span&gt;&lt;span class="gi"&gt;+ import { format, addDays, differenceInDays } from 'date-light';
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Same function names, same arguments, same pattern tokens.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;npm:&lt;/strong&gt; &lt;a href="https://www.npmjs.com/package/date-light" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/date-light&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/flyingsquirrel0419/date-light" rel="noopener noreferrer"&gt;https://github.com/flyingsquirrel0419/date-light&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full API docs:&lt;/strong&gt; See the README on GitHub&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;If this sounds useful for your next project, I'd appreciate a star on GitHub. It helps others find it too.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/flyingsquirrel0419/date-light" rel="noopener noreferrer"&gt;https://github.com/flyingsquirrel0419/date-light&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>typescript</category>
      <category>npm</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Your GitHub Profile README Is Boring. Here's How to Fix It with SVG and GitHub Actions.</title>
      <dc:creator>날다람쥐</dc:creator>
      <pubDate>Tue, 12 May 2026 12:04:15 +0000</pubDate>
      <link>https://dev.to/flyingsquirrel0419/your-github-profile-readme-is-boring-heres-how-to-fix-it-with-svg-and-github-actions-3pim</link>
      <guid>https://dev.to/flyingsquirrel0419/your-github-profile-readme-is-boring-heres-how-to-fix-it-with-svg-and-github-actions-3pim</guid>
      <description>&lt;p&gt;Most GitHub profiles look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Hi, I'm John 👋&lt;/span&gt;
I'm a developer who likes coding.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that's it. A plain heading, maybe a bullet list of tech, nothing else.&lt;/p&gt;

&lt;p&gt;Your profile is the first thing a recruiter, collaborator, or fellow developer sees when they look you up. You've got about three seconds to make an impression. A wall of plain Markdown isn't making one.&lt;/p&gt;

&lt;p&gt;Here's what the good profiles have in common: &lt;strong&gt;animated headers, auto-updating stats, and dynamic content that actually runs on GitHub's own infrastructure&lt;/strong&gt; — all via GitHub Actions, no external servers required.&lt;/p&gt;

&lt;p&gt;I put together a repo that walks you through exactly how to do this, step by step.&lt;/p&gt;




&lt;h2&gt;
  
  
  What You Can Build
&lt;/h2&gt;

&lt;p&gt;Here's what a finished profile can include — all generated automatically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An animated gradient header banner (updates on every page load)&lt;/li&gt;
&lt;li&gt;A typing animation for your intro line&lt;/li&gt;
&lt;li&gt;Auto-updating stat cards: commits, stars, PRs, streak&lt;/li&gt;
&lt;li&gt;A language breakdown chart&lt;/li&gt;
&lt;li&gt;A tech stack badge grid&lt;/li&gt;
&lt;li&gt;A snake animation that eats your actual contribution graph&lt;/li&gt;
&lt;li&gt;Custom GitHub Actions that pull in blog posts, WakaTime stats, weather — anything with an API&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No design skills required. Every piece is copy-paste ready.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Tools
&lt;/h2&gt;

&lt;p&gt;These are the services that power the best GitHub profiles. They're all free.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/kyechan99/capsule-render" rel="noopener noreferrer"&gt;capsule-render&lt;/a&gt;&lt;/strong&gt; — generates animated header banners via a single &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; tag. Waving gradients, glitch effects, colorful text. You just pass URL parameters.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;![&lt;/span&gt;&lt;span class="nv"&gt;header&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://capsule-render.vercel.app/api?type=waving&amp;amp;color=gradient&amp;amp;height=200&amp;amp;text=Hello%20World&amp;amp;animation=fadeIn&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;&lt;a href="https://github.com/DenverCoder1/readme-typing-svg" rel="noopener noreferrer"&gt;readme-typing-svg&lt;/a&gt;&lt;/strong&gt; — same idea, but for typing animations. Pass your lines of text as URL params, get back an SVG that types them out in sequence.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;![&lt;/span&gt;&lt;span class="nv"&gt;Typing SVG&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://readme-typing-svg.demolab.com?font=Fira+Code&amp;amp;lines=Software+Engineer;Open+Source+Enthusiast;Always+learning&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;&lt;a href="https://github.com/anuraghazra/github-readme-stats" rel="noopener noreferrer"&gt;github-readme-stats&lt;/a&gt;&lt;/strong&gt; — stat cards for your profile. Languages, commit counts, streaks. Hosted, free, just put your username in the URL.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;![&lt;/span&gt;&lt;span class="nv"&gt;Stats&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://github-readme-stats.vercel.app/api?username=YOUR_USERNAME&amp;amp;show_icons=true&amp;amp;theme=tokyonight&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;&lt;a href="https://github.com/lowlighter/metrics" rel="noopener noreferrer"&gt;lowlighter/metrics&lt;/a&gt;&lt;/strong&gt; — the more powerful alternative. Runs as a GitHub Action inside your profile repo, generates a full metrics image, and commits it back automatically. Supports dozens of plugins: calendar heatmap, achievements, languages over time, and more.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/Platane/snk" rel="noopener noreferrer"&gt;Platane/snk&lt;/a&gt;&lt;/strong&gt; — generates a snake animation that eats the squares on your contribution graph. Also a GitHub Action. Runs on a schedule, commits the SVG, you embed it in your README.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://shields.io" rel="noopener noreferrer"&gt;shields.io&lt;/a&gt;&lt;/strong&gt; — clean, consistent badges for tech stack, social links, build status, anything.&lt;/p&gt;




&lt;h2&gt;
  
  
  How GitHub Actions Makes This Dynamic
&lt;/h2&gt;

&lt;p&gt;Here's the part that makes this more interesting than a static README.&lt;/p&gt;

&lt;p&gt;GitHub lets you create a repository with the same name as your username. Whatever README is in that repo gets displayed on your profile page. That's the trick everyone knows.&lt;/p&gt;

&lt;p&gt;What fewer people do is &lt;strong&gt;run GitHub Actions inside that repo on a schedule&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A workflow like this runs every day at midnight:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Update Profile&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*'&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;update&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Generate metrics&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;lowlighter/metrics@latest&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.METRICS_TOKEN }}&lt;/span&gt;
          &lt;span class="na"&gt;filename&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;metrics.svg&lt;/span&gt;
          &lt;span class="na"&gt;base&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;header, activity, community, repositories&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Commit changes&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;git config user.name "github-actions[bot]"&lt;/span&gt;
          &lt;span class="s"&gt;git config user.email "github-actions[bot]@users.noreply.github.com"&lt;/span&gt;
          &lt;span class="s"&gt;git add metrics.svg&lt;/span&gt;
          &lt;span class="s"&gt;git diff --cached --quiet || git commit -m "chore: update metrics"&lt;/span&gt;
          &lt;span class="s"&gt;git push&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your profile README then just references &lt;code&gt;metrics.svg&lt;/code&gt; as a normal image. Every day it's fresh. No server. No hosting. GitHub does the compute for free.&lt;/p&gt;




&lt;h2&gt;
  
  
  Three Templates to Start From
&lt;/h2&gt;

&lt;p&gt;Rather than building from scratch, the repo includes three starting points:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Minimal&lt;/strong&gt; — capsule-render header, typing SVG intro, shields.io badges, one stats card. Clean and professional. Good if you want something polished without the maintenance overhead of Actions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Developer&lt;/strong&gt; — everything in Minimal plus a streak card, language breakdown, and activity section. More visual, still no Actions required.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Advanced&lt;/strong&gt; — the full setup. GitHub Actions for auto-updating metrics, the snake animation workflow, and a custom Action that pulls in your latest blog posts. This is the one that makes people ask "how did you do that."&lt;/p&gt;




&lt;h2&gt;
  
  
  Quick Start
&lt;/h2&gt;

&lt;p&gt;Three steps:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Create your profile repo.&lt;/strong&gt; New repository, name it exactly the same as your GitHub username. Initialize with a README. GitHub recognizes this automatically and displays it on your profile.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Pick a template.&lt;/strong&gt; Go to &lt;a href="https://github.com/flyingsquirrel0419/awesome-git-profile/tree/main/profiles" rel="noopener noreferrer"&gt;&lt;code&gt;profiles/&lt;/code&gt;&lt;/a&gt; and copy the README from whichever template fits. Paste it into your profile repo's README.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Swap in your username.&lt;/strong&gt; Every &lt;code&gt;YOUR_USERNAME&lt;/code&gt; placeholder gets replaced with your actual username. Push, and you're live.&lt;/p&gt;

&lt;p&gt;The guides in the repo walk through each piece in detail — from the initial repo setup through writing your own custom GitHub Actions.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Repo
&lt;/h2&gt;

&lt;p&gt;Everything covered here — guides, templates, copy-paste snippets, workflow files — is at:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/flyingsquirrel0419/awesome-git-profile" rel="noopener noreferrer"&gt;github.com/flyingsquirrel0419/awesome-git-profile&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If it's useful, a ⭐ on the repo helps other developers find it. There's also a &lt;a href="https://github.com/flyingsquirrel0419/awesome-git-profile/blob/main/showcase/README.md" rel="noopener noreferrer"&gt;community showcase&lt;/a&gt; — once your profile is set up, submit a PR to add it.&lt;/p&gt;




&lt;p&gt;The guides go in order from zero to a finished profile, but they're also written to be standalone. If you just need the snake animation, go straight to &lt;a href="https://github.com/flyingsquirrel0419/awesome-git-profile/blob/main/guides/06-snake.md" rel="noopener noreferrer"&gt;guide 06&lt;/a&gt;. If you want to write a custom Action that fetches your latest blog posts, &lt;a href="https://github.com/flyingsquirrel0419/awesome-git-profile/blob/main/guides/08-actions-deep.md" rel="noopener noreferrer"&gt;guide 08&lt;/a&gt; covers that.&lt;/p&gt;

&lt;p&gt;Happy building.&lt;/p&gt;

</description>
      <category>github</category>
      <category>tutorial</category>
      <category>githubactions</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Layercache 2.0.0: Millisecond TTLs, Stale-Preserving Expiration, and Better Redis Coverage</title>
      <dc:creator>날다람쥐</dc:creator>
      <pubDate>Fri, 01 May 2026 18:22:10 +0000</pubDate>
      <link>https://dev.to/flyingsquirrel0419/layercache-200-millisecond-ttls-stale-preserving-expiration-and-better-redis-coverage-p9h</link>
      <guid>https://dev.to/flyingsquirrel0419/layercache-200-millisecond-ttls-stale-preserving-expiration-and-better-redis-coverage-p9h</guid>
      <description>&lt;p&gt;Layercache &lt;code&gt;2.0.0&lt;/code&gt; is out.&lt;/p&gt;

&lt;p&gt;This release is intentionally a major version because it changes the public TTL semantics from seconds to milliseconds. That is a breaking change, but it also makes Layercache much more consistent with Node.js timers, Redis &lt;code&gt;PX&lt;/code&gt; / &lt;code&gt;PTTL&lt;/code&gt;, and most JavaScript runtime APIs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Breaking: TTL values are now milliseconds
&lt;/h3&gt;

&lt;p&gt;Before &lt;code&gt;2.0.0&lt;/code&gt;, TTL examples and parts of the public API used seconds.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;2.0.0&lt;/code&gt;, TTL-related values are consistently milliseconds:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cache&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;CacheStack&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;layers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;memory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&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;cache&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="s2"&gt;user:1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;staleWhileRevalidate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;staleIfError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This applies to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;layer defaults&lt;/li&gt;
&lt;li&gt;operation-level &lt;code&gt;ttl&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;negativeTtl&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;staleWhileRevalidate&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;staleIfError&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ttlJitter&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;refreshAhead&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;adaptive TTL policies&lt;/li&gt;
&lt;li&gt;TTL policy return values&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Redis writes now use &lt;code&gt;PX&lt;/code&gt;, Redis TTL reads use &lt;code&gt;PTTL&lt;/code&gt;, and CLI inspect output reports &lt;code&gt;ttlMs&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  New stale-preserving expiration APIs
&lt;/h2&gt;

&lt;p&gt;Layercache &lt;code&gt;2.0.0&lt;/code&gt; adds expiration APIs that mark entries stale without deleting their stale windows:&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;await&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expireByTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;product&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;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expireByTags&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;product&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;pricing&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;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expireByPattern&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;product:*&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;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expireByPrefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;product:&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 is useful when you want revalidation to happen, but still want existing cached values to be served during &lt;code&gt;stale-while-revalidate&lt;/code&gt; or &lt;code&gt;stale-if-error&lt;/code&gt; windows.&lt;/p&gt;

&lt;p&gt;Instead of forcing a hard delete, these APIs let the cache degrade more gracefully.&lt;/p&gt;

&lt;h2&gt;
  
  
  Context-aware cache entry options
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;contextOptions&lt;/code&gt; can now derive TTLs and tags from the resolved cache context:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cache&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;CacheStack&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;layers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;memory&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;contextOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;kind&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="na"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;set&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orgId&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;`org:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orgId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works for fetched values and direct &lt;code&gt;set()&lt;/code&gt; calls.&lt;/p&gt;

&lt;p&gt;It makes it easier to keep cache metadata close to the data being cached.&lt;/p&gt;

&lt;h2&gt;
  
  
  Better Redis integration coverage
&lt;/h2&gt;

&lt;p&gt;This release adds real Redis integration coverage for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;RedisLayer&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RedisInvalidationBus&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RedisTagIndex&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;distributed single-flight&lt;/li&gt;
&lt;li&gt;multi-instance behavior&lt;/li&gt;
&lt;li&gt;millisecond TTL handling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The integration suite now runs against a real Redis service through Docker Compose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Documentation site
&lt;/h2&gt;

&lt;p&gt;Layercache now includes a Next.js docs site with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MDX docs&lt;/li&gt;
&lt;li&gt;search&lt;/li&gt;
&lt;li&gt;playground&lt;/li&gt;
&lt;li&gt;markdown export&lt;/li&gt;
&lt;li&gt;sitemap&lt;/li&gt;
&lt;li&gt;robots.txt&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.well-known&lt;/code&gt; API and agent metadata routes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Docs are available at:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://layercache.flyingsquirrel.me" rel="noopener noreferrer"&gt;https://layercache.flyingsquirrel.me&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Smaller npm package
&lt;/h2&gt;

&lt;p&gt;The npm package is now limited to runtime build artifacts.&lt;/p&gt;

&lt;p&gt;The package includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;dist/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;package.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;README.md&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LICENSE&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Benchmarks, examples, and stale build artifacts are excluded from the published package.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migration guide
&lt;/h2&gt;

&lt;p&gt;The main migration step is converting TTL values from seconds to milliseconds.&lt;/p&gt;

&lt;p&gt;Before:&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="nx"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;
&lt;span class="nx"&gt;staleWhileRevalidate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;
&lt;span class="nx"&gt;staleIfError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After:&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="nx"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;
&lt;span class="nx"&gt;staleWhileRevalidate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;
&lt;span class="nx"&gt;staleIfError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you previously used numeric TTLs, audit every TTL-related option before upgrading.&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;layercache@2.0.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Release notes
&lt;/h2&gt;

&lt;p&gt;Full release notes are available here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/flyingsquirrel0419/layercache/releases/tag/v2.0.0" rel="noopener noreferrer"&gt;https://github.com/flyingsquirrel0419/layercache/releases/tag/v2.0.0&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Support the project
&lt;/h2&gt;

&lt;p&gt;If Layercache looks useful, please consider giving the repository a star on GitHub:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/flyingsquirrel0419/layercache" rel="noopener noreferrer"&gt;https://github.com/flyingsquirrel0419/layercache&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;Layercache &lt;code&gt;2.0.0&lt;/code&gt; is a breaking release, but the goal is simple: make TTL behavior explicit, consistent, and easier to reason about across memory, Redis, disk, CLI output, and documentation.&lt;/p&gt;

</description>
      <category>node</category>
      <category>typescript</category>
      <category>redis</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I tried every TypeScript Result library. So I built a better one.</title>
      <dc:creator>날다람쥐</dc:creator>
      <pubDate>Tue, 28 Apr 2026 16:22:34 +0000</pubDate>
      <link>https://dev.to/flyingsquirrel0419/i-tried-every-typescript-result-library-so-i-built-a-better-one-3a5h</link>
      <guid>https://dev.to/flyingsquirrel0419/i-tried-every-typescript-result-library-so-i-built-a-better-one-3a5h</guid>
      <description>&lt;p&gt;I've been writing TypeScript for years, and &lt;code&gt;try/catch&lt;/code&gt; has always bothered me.&lt;/p&gt;

&lt;p&gt;Not because error handling is hard — but because &lt;strong&gt;errors are invisible in the type system.&lt;/strong&gt; A function that says &lt;code&gt;Promise&amp;lt;User&amp;gt;&lt;/code&gt; might throw. Or might not. You genuinely can't tell without reading the implementation.&lt;/p&gt;

&lt;p&gt;So I went looking for a library that solves this properly.&lt;/p&gt;

&lt;p&gt;I tried them all. None of them were quite right.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;&lt;a href="https://github.com/flyingsquirrel0419/verdict-ts" rel="noopener noreferrer"&gt;verdict-ts&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The problem, quickly
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// This function signature is lying to you&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&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="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="c1"&gt;// It can actually do this at runtime&lt;/span&gt;
&lt;span class="c1"&gt;// Uncaught Error: Network timeout&lt;/span&gt;
&lt;span class="c1"&gt;// Uncaught SyntaxError: Unexpected token in JSON&lt;/span&gt;
&lt;span class="c1"&gt;// Uncaught TypeError: Cannot read properties of null&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The solution Rust came up with: make failure part of the return type.&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;// This function tells the truth&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&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="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ApiError&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the compiler won't let you access the user without handling the error case first. Failures are visible. The type doesn't lie.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why not the existing libraries?
&lt;/h2&gt;

&lt;p&gt;Great question — and honestly the main reason I'm writing this post. Let's go through them.&lt;/p&gt;




&lt;h3&gt;
  
  
  neverthrow
&lt;/h3&gt;

&lt;p&gt;The most popular option, and for good reason — it works well and is actively maintained. But it has one fundamental design choice I kept bumping into:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It's class-based.&lt;/strong&gt;&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ok&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="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;neverthrow&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;ResultOk&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Classes mean prototype chains, and prototype chains cause real problems:&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;// ❌ Breaks across Worker boundaries&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;worker&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;Worker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./worker.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postMessage&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;// structured clone strips the prototype&lt;/span&gt;
&lt;span class="c1"&gt;// Other side receives a plain object — methods are gone&lt;/span&gt;

&lt;span class="c1"&gt;// ❌ Breaks across iframes&lt;/span&gt;
&lt;span class="c1"&gt;// ❌ JSON.stringify loses the methods&lt;/span&gt;
&lt;span class="c1"&gt;// ❌ structuredClone loses the methods&lt;/span&gt;

&lt;span class="c1"&gt;// This silently fails at runtime even though TypeScript says it's fine&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're building for Cloudflare Workers, Next.js Edge Runtime, or anything that crosses a serialization boundary — classes are a footgun.&lt;/p&gt;

&lt;p&gt;Also, at &lt;strong&gt;112KB unpacked&lt;/strong&gt;, it's larger than I'd want for a utility that goes into other packages as a dependency.&lt;/p&gt;




&lt;h3&gt;
  
  
  true-myth
&lt;/h3&gt;

&lt;p&gt;Solid functional programming library. If you want &lt;code&gt;Maybe&amp;lt;T&amp;gt;&lt;/code&gt; alongside &lt;code&gt;Result&amp;lt;T, E&amp;gt;&lt;/code&gt;, it's excellent.&lt;/p&gt;

&lt;p&gt;But: &lt;strong&gt;793KB unpacked.&lt;/strong&gt; That's not a typo.&lt;/p&gt;

&lt;p&gt;It also requires you to buy into its full worldview — &lt;code&gt;Maybe&lt;/code&gt;, &lt;code&gt;Task&lt;/code&gt;, the whole functional ecosystem. If you just want &lt;code&gt;Result&lt;/code&gt;, you're bringing in a lot you won't use.&lt;/p&gt;




&lt;h3&gt;
  
  
  ts-results
&lt;/h3&gt;

&lt;p&gt;Spiritually the closest to what I wanted. But:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Last published: May 2022.&lt;/strong&gt; Three years without an update.&lt;/li&gt;
&lt;li&gt;TypeScript has changed a lot since then — inference has gotten smarter, and ts-results doesn't take advantage of it.&lt;/li&gt;
&lt;li&gt;Issues have been piling up without responses.&lt;/li&gt;
&lt;li&gt;Weaker tuple inference in &lt;code&gt;combine()&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  result.ts
&lt;/h3&gt;

&lt;p&gt;Has a dependency (&lt;code&gt;maybe.ts&lt;/code&gt;). That immediately ruled it out — if I'm adding this as a dependency to my own packages, I don't want transitive deps creeping in.&lt;/p&gt;




&lt;h2&gt;
  
  
  What verdict-ts does differently
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Plain objects, not classes&lt;/strong&gt;&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="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="nf"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// Literally: { ok: true, value: 42 }&lt;/span&gt;

&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;      &lt;span class="c1"&gt;// ✅ works&lt;/span&gt;
&lt;span class="nf"&gt;structuredClone&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;// ✅ works&lt;/span&gt;
&lt;span class="nf"&gt;postMessage&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;// ✅ works across Workers&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No prototype, no &lt;code&gt;instanceof&lt;/code&gt;, no serialization surprises. It's just data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Zero dependencies&lt;/strong&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;"dependencies"&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;When verdict-ts goes into your SDK as a dependency, nothing comes with it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. 491 bytes gzipped&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For comparison: neverthrow is ~4KB gzipped, true-myth is ~12KB. verdict-ts is smaller than most SVG icons (491B — yes, really).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Proper tuple inference in &lt;code&gt;combine()&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every Result library has &lt;code&gt;combine()&lt;/code&gt;. Most of them return &lt;code&gt;Result&amp;lt;T[], E&amp;gt;&lt;/code&gt;, which loses the tuple type:&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;// Other libraries:&lt;/span&gt;
&lt;span class="nf"&gt;combine&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nf"&gt;ok&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="nf"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hello&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;
&lt;span class="c1"&gt;// Result&amp;lt;(number | string)[], Error&amp;gt;  ← types are merged, index info lost&lt;/span&gt;

&lt;span class="c1"&gt;// verdict-ts:&lt;/span&gt;
&lt;span class="nf"&gt;combine&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nf"&gt;ok&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="nf"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hello&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;
&lt;span class="c1"&gt;// Result&amp;lt;[number, string], Error&amp;gt;  ← tuple preserved, index 0 is number, index 1 is string&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This matters for validation:&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="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="nf"&gt;combine&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="nf"&gt;validateEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;    &lt;span class="c1"&gt;// Result&amp;lt;string, ValidationError&amp;gt;&lt;/span&gt;
  &lt;span class="nf"&gt;validateAge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;age&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;        &lt;span class="c1"&gt;// Result&amp;lt;number, ValidationError&amp;gt;&lt;/span&gt;
  &lt;span class="nf"&gt;validateUsername&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;// Result&amp;lt;string, ValidationError&amp;gt;&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="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;age&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;username&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="c1"&gt;//   ^^^^^ string  ^^^ number  ^^^^^^^^ string&lt;/span&gt;
    &lt;span class="c1"&gt;// TypeScript knows all three types at each index&lt;/span&gt;
    &lt;span class="nf"&gt;createUser&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;age&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;username&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&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;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="nf"&gt;showError&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;message&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;5. &lt;code&gt;AsyncResult&amp;lt;T, E&amp;gt;&lt;/code&gt; type alias&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A small thing that makes async code much cleaner:&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;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AsyncResult&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;verdict-ts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Instead of this&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&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="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ApiError&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;

&lt;span class="c1"&gt;// Write this&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&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="nx"&gt;AsyncResult&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ApiError&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The full API
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&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;ok&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="c1"&gt;// constructors&lt;/span&gt;
  &lt;span class="nx"&gt;trySync&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;// wrap synchronous throwables&lt;/span&gt;
  &lt;span class="nx"&gt;tryAsync&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;         &lt;span class="c1"&gt;// wrap async throwables&lt;/span&gt;
  &lt;span class="nx"&gt;combine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;// merge multiple Results&lt;/span&gt;
  &lt;span class="nx"&gt;isOk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isErr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;// type guards&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;verdict-ts&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="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Ok&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;Result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;AsyncResult&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;verdict-ts&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;&lt;strong&gt;Creating Results:&lt;/strong&gt;&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="nf"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                     &lt;span class="c1"&gt;// Ok&amp;lt;number&amp;gt;&lt;/span&gt;
&lt;span class="nf"&gt;err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&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;oops&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;     &lt;span class="c1"&gt;// Err&amp;lt;Error&amp;gt;&lt;/span&gt;
&lt;span class="nf"&gt;err&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;         &lt;span class="c1"&gt;// Err&amp;lt;{ code: number }&amp;gt;&lt;/span&gt;

&lt;span class="nf"&gt;trySync&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;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;str&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;                  &lt;span class="c1"&gt;// Result&amp;lt;unknown, Error&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;tryAsync&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;fetch&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="c1"&gt;// Result&amp;lt;Response, Error&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Transforming:&lt;/strong&gt;&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="nx"&gt;result&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;value&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="c1"&gt;// transform Ok value&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mapErr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AppError&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;message&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;  &lt;span class="c1"&gt;// transform Err&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flatMap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;                      &lt;span class="c1"&gt;// Ok → another Result&lt;/span&gt;
    &lt;span class="nx"&gt;value&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="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;negative&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;&lt;strong&gt;Extracting:&lt;/strong&gt;&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="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unwrap&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;           &lt;span class="c1"&gt;// value or throws&lt;/span&gt;
&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unwrapOr&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="c1"&gt;// value or default&lt;/span&gt;
&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`got &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;err&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`failed: &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="s2"&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;
  
  
  Real-world example: API client
&lt;/h2&gt;

&lt;p&gt;This is the pattern that made me want to build this. When you write an SDK, your functions should tell the truth about what can fail:&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;tryAsync&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ok&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="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;verdict-ts&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="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AsyncResult&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;verdict-ts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ApiError&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;network&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;message&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="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;not_found&lt;/span&gt;&lt;span class="dl"&gt;'&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;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unauthorized&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&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="nx"&gt;AsyncResult&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ApiError&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;response&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;tryAsync&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;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/users/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;response&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mapErr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;network&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;message&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;message&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flatMap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&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="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;err&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unauthorized&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;err&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;not_found&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="k"&gt;as&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;id&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;tryAsync&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;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="nf"&gt;mapErr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;network&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;message&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;message&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="c1"&gt;// Caller gets full type safety on the error&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;getUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;123&lt;/span&gt;&lt;span class="dl"&gt;'&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="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&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;renderProfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&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;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="k"&gt;switch &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;kind&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;network&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;showNetworkError&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;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;not_found&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;showNotFound&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;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unauthorized&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;redirectToLogin&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// TypeScript ensures all cases are handled&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;No try/catch. No &lt;code&gt;unknown&lt;/code&gt; errors. Every failure mode is in the type.&lt;/p&gt;




&lt;h2&gt;
  
  
  Quick comparison table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;verdict-ts&lt;/th&gt;
&lt;th&gt;neverthrow&lt;/th&gt;
&lt;th&gt;true-myth&lt;/th&gt;
&lt;th&gt;ts-results&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Size (gzipped)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;491B&lt;/td&gt;
&lt;td&gt;~4KB&lt;/td&gt;
&lt;td&gt;~12KB&lt;/td&gt;
&lt;td&gt;~3KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Dependencies&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Class-based&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;JSON-serializable&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Tuple inference&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AsyncResult type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Active maintenance&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Edge Runtime safe&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;⚠️&lt;/td&gt;
&lt;td&gt;⚠️&lt;/td&gt;
&lt;td&gt;⚠️&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&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;verdict-ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&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;ok&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;tryAsync&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;verdict-ts&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;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;tryAsync&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;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.github.com/users/torvalds&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&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="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;user&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unwrapOr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unknown&lt;/span&gt;&lt;span class="dl"&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;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// "Linus Torvalds"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;npm: &lt;a href="https://npmjs.com/package/verdict-ts" rel="noopener noreferrer"&gt;npmjs.com/package/verdict-ts&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If this was useful, a ⭐ on GitHub goes a long way — it helps other developers find the project when they're searching for exactly this kind of library.&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://github.com/flyingsquirrel0419/verdict-ts" rel="noopener noreferrer"&gt;github.com/flyingsquirrel0419/verdict-ts&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;What's your current approach to error handling in TypeScript? Still on try/catch, or have you switched to Result types? Would love to hear in the comments 👇&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I got tired of AI agents trashing my codebase, so I built a skill to fix that</title>
      <dc:creator>날다람쥐</dc:creator>
      <pubDate>Mon, 27 Apr 2026 17:25:00 +0000</pubDate>
      <link>https://dev.to/flyingsquirrel0419/i-got-tired-of-ai-agents-trashing-my-codebase-so-i-built-a-skill-to-fix-that-mgc</link>
      <guid>https://dev.to/flyingsquirrel0419/i-got-tired-of-ai-agents-trashing-my-codebase-so-i-built-a-skill-to-fix-that-mgc</guid>
      <description>&lt;p&gt;Every AI coding agent I've used hits the same wall. It always starts the same way.&lt;/p&gt;

&lt;p&gt;You ask it to add a feature. It rewrites half the file in its preferred naming convention. Your &lt;code&gt;snake_case&lt;/code&gt; Python suddenly has &lt;code&gt;camelCase&lt;/code&gt; crammed in. The test framework you carefully chose? Gone, replaced by a different one the model apparently likes better.&lt;/p&gt;

&lt;p&gt;So you fight it back into shape. Then you ask it to fix a bug. Same thing happens. It "tidies up" a few unrelated functions while it's in there. Opens three files you didn't ask it to touch. Leaves a commented-out &lt;code&gt;console.log&lt;/code&gt; in production code.&lt;/p&gt;

&lt;p&gt;Then the worst one: you hit three failures in a row on a gnarly bug, and the agent just keeps trying random things. No backoff. No ask-for-help. Just vibes and increasingly desperate code changes until the whole file is a mess.&lt;/p&gt;

&lt;p&gt;I've context-window-pasted my way through this more times than I want to admit. Not because the agents are bad — they're genuinely powerful — but because there's no agreed-upon &lt;em&gt;discipline&lt;/em&gt; baked in. No "read the room before you touch anything." No "when you're stuck three times in a row, stop and ask."&lt;/p&gt;

&lt;p&gt;So I spent some time vibe coding a skill file to fix that, and turned it into &lt;a href="https://github.com/flyingsquirrel0419/squirrel-skill" rel="noopener noreferrer"&gt;🐿️ Squirrel&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What it actually does
&lt;/h2&gt;

&lt;p&gt;Squirrel is a single Markdown file (&lt;code&gt;SKILL.md&lt;/code&gt;) that you drop into your AI agent's instruction path. It installs a full 8-phase engineering discipline into your agent, without you having to remind it every session.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[1] 🔍 Discover  → Audit the project before touching a single line
[2] 📋 Plan      → Task list with dependencies and done-criteria
[3] 💻 Build     → Write or modify code
[4] 🧪 Test      → Run existing tests, write new ones
[5] 🐛 Bug Hunt  → Static analysis + manual checklist
[6] ✨ Polish    → Lint, format, type check
[7] 📖 Document  → README + inline docs (update, don't overwrite)
[8] 🚀 Ship      → Final checklist before handoff
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key insight is &lt;strong&gt;Step 0&lt;/strong&gt; — before the agent does anything, it figures out what kind of project it's looking at:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What it sees&lt;/th&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Empty directory&lt;/td&gt;
&lt;td&gt;🆕 Greenfield — start from scratch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Source files, no tests&lt;/td&gt;
&lt;td&gt;🔧 In-Progress — audit first, then improve&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Source + tests + CI + README&lt;/td&gt;
&lt;td&gt;🏗️ Mature — targeted improvements only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"fix this bug / add this feature"&lt;/td&gt;
&lt;td&gt;🎯 Targeted — abbreviated audit, scoped work&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Then it &lt;em&gt;announces the mode&lt;/em&gt; to you. So you know it read your code before it started writing.&lt;/p&gt;




&lt;h2&gt;
  
  
  The "respect existing code" problem
&lt;/h2&gt;

&lt;p&gt;This is the thing I cared most about getting right. Squirrel explicitly teaches the agent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Match the existing naming convention — if the project uses &lt;code&gt;snake_case&lt;/code&gt;, don't introduce &lt;code&gt;camelCase&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Use the existing test framework — don't swap in a new one because it's newer&lt;/li&gt;
&lt;li&gt;Read 2-3 similar files before writing a new one, to understand the pattern&lt;/li&gt;
&lt;li&gt;Touch only what's necessary — "add a password reset endpoint" is not permission to refactor the auth module&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In practice this means adding something like this to the SKILL.md:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## For existing code:&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Match the codebase's style**&lt;/span&gt; — check .eslintrc, pyproject.toml, rustfmt.toml
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Read before writing**&lt;/span&gt; — look at 2-3 similar existing functions first
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Touch only what's necessary**&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Leave the codebase better than you found it**&lt;/span&gt;, scoped to what you touched
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sounds obvious, right? But without it being explicit in the agent's instruction context, most models default to "write it my way."&lt;/p&gt;




&lt;h2&gt;
  
  
  Solving the infinite debugging loop
&lt;/h2&gt;

&lt;p&gt;The part I found most satisfying to design: the &lt;strong&gt;3-Strike Rule&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strike 1:&lt;/strong&gt; Fix the specific error. Run tests. Move on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strike 2:&lt;/strong&gt; Re-read the code more carefully. Try a different approach.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strike 3:&lt;/strong&gt; STOP. Revert. Write a failure report. Ask the user.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## After Strike 3:&lt;/span&gt;
&lt;span class="p"&gt;1.&lt;/span&gt; STOP all edits
&lt;span class="p"&gt;2.&lt;/span&gt; REVERT to last known working state (git stash)
&lt;span class="p"&gt;3.&lt;/span&gt; Write a failure report:
&lt;span class="p"&gt;   -&lt;/span&gt; What I tried
&lt;span class="p"&gt;   -&lt;/span&gt; What went wrong
&lt;span class="p"&gt;   -&lt;/span&gt; Where I think the problem is
&lt;span class="p"&gt;   -&lt;/span&gt; What I've ruled out
&lt;span class="p"&gt;4.&lt;/span&gt; ASK THE USER
&lt;span class="p"&gt;5.&lt;/span&gt; NEVER: leave code broken, delete failing tests, shotgun-debug
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This alone has saved me from agent death spirals more than once. Instead of watching it make 12 increasingly confused changes to the same function, it stops at 3, tells me what it knows, and asks. Like a junior engineer would.&lt;/p&gt;




&lt;h2&gt;
  
  
  Works on 8 platforms, install in one line
&lt;/h2&gt;

&lt;p&gt;The whole thing is just Markdown. Every major AI coding agent reads Markdown instructions — the YAML frontmatter is consumed by OpenCode, silently ignored by everything else.&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;# Auto-detect your agent and install&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/flyingsquirrel0419/squirrel-skill/main/install.sh | bash

&lt;span class="c"&gt;# Or for a specific platform&lt;/span&gt;
bash install.sh &lt;span class="nt"&gt;--platform&lt;/span&gt; cursor
bash install.sh &lt;span class="nt"&gt;--platform&lt;/span&gt; claude-code
bash install.sh &lt;span class="nt"&gt;--platform&lt;/span&gt; aider
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Supported: OpenCode, Codex, Claude Code, Cursor, Windsurf, Aider, Cline, GitHub Copilot.&lt;/p&gt;

&lt;p&gt;If you just want minimal setup that covers four platforms at once, drop &lt;code&gt;AGENTS.md&lt;/code&gt; in your project root. Natively read by Codex, Cursor, Cline, and Claude Code.&lt;/p&gt;

&lt;p&gt;For Cursor specifically, add the frontmatter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Squirrel full-cycle development skill&lt;/span&gt;
&lt;span class="na"&gt;alwaysApply&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then paste the SKILL.md content below it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reference files included
&lt;/h2&gt;

&lt;p&gt;Squirrel ships with supplementary templates the agent loads on demand — not all upfront, only when relevant:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Loaded when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;references/plan_template.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Phase 1, creating Plan.md&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;references/readme_template.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Phase 7, writing a new README&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;references/stack_hints.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Phase 3, unfamiliar languages or stacks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;references/ci_templates.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Phase 8, setting up GitHub Actions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The CI templates cover Node.js, Python, Go, Rust — ready-to-use starting points, not drop-in guarantees.&lt;/p&gt;




&lt;h2&gt;
  
  
  What vibe coding this taught me
&lt;/h2&gt;

&lt;p&gt;I built Squirrel as a vibe coding exercise — give the agent a clear goal, iterate fast, let it do the heavy lifting. It was the first time I really leaned into that workflow end-to-end on something I cared about shipping.&lt;/p&gt;

&lt;p&gt;The irony isn't lost on me: I needed better agent discipline to build a skill that teaches agents discipline. Every time the agent went sideways during development, I'd notice what rule was missing and add it to SKILL.md. The 3-Strike Rule came from a particularly painful afternoon of watching it loop on a bash parsing edge case.&lt;/p&gt;

&lt;p&gt;By the end, the skill file had basically written itself — not because the AI wrote it, but because the failures showed me exactly what needed to be in it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# One liner&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/flyingsquirrel0419/squirrel-skill/main/install.sh | bash

&lt;span class="c"&gt;# Then tell your agent what you want:&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; squirrel this project — add tests, fix lint errors, write README
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; build me a REST API &lt;span class="k"&gt;for &lt;/span&gt;a todo app with TypeScript
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; fix this bug &lt;span class="k"&gt;in &lt;/span&gt;src/auth/login.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent announces which mode it detected, runs the phases, and gives you a summary at the end.&lt;/p&gt;




&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/flyingsquirrel0419/squirrel-skill" rel="noopener noreferrer"&gt;https://github.com/flyingsquirrel0419/squirrel-skill&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If this is useful to you, a ⭐ on GitHub genuinely helps — it's what tells me whether to keep building this out. Thanks for reading.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>opensource</category>
      <category>vibecoding</category>
    </item>
    <item>
      <title>I got tired of deploying broken configs, so I built dotenv-scan</title>
      <dc:creator>날다람쥐</dc:creator>
      <pubDate>Mon, 27 Apr 2026 16:38:00 +0000</pubDate>
      <link>https://dev.to/flyingsquirrel0419/i-got-tired-of-deploying-broken-configs-so-i-built-dotenv-scan-47lb</link>
      <guid>https://dev.to/flyingsquirrel0419/i-got-tired-of-deploying-broken-configs-so-i-built-dotenv-scan-47lb</guid>
      <description>&lt;p&gt;Every team I've worked on has had this incident at least once.&lt;/p&gt;

&lt;p&gt;Friday afternoon deploy. CI is green. You push to production. Five minutes later, someone's pinging you in Slack: the app is crashing on startup. You SSH in, check the logs, and there it is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error: DATABASE_URL is not defined
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You forgot to add the new environment variable to production. The &lt;code&gt;.env&lt;/code&gt; file on your machine has it. The &lt;code&gt;.env.example&lt;/code&gt; in the repo doesn't. Nobody noticed during review.&lt;/p&gt;

&lt;p&gt;Half an hour of your Friday afternoon gone.&lt;/p&gt;

&lt;p&gt;The fix is always the same — add the variable, redeploy, move on. But the problem keeps coming back. Because there's no automated check. You're relying on code review and memory.&lt;/p&gt;

&lt;p&gt;And it's not just missing variables. The opposite problem is just as real: &lt;code&gt;.env&lt;/code&gt; files that accumulate junk. &lt;code&gt;OLD_REDIS_URL&lt;/code&gt; from a migration you finished six months ago. &lt;code&gt;LEGACY_API_KEY&lt;/code&gt; for a service you sunset last quarter. They sit there, silently, because nobody wants to delete something they're not 100% sure is unused.&lt;/p&gt;

&lt;p&gt;I've fixed this manually too many times. So I built &lt;a href="https://github.com/flyingsquirrel0419/dotenv-scan" rel="noopener noreferrer"&gt;dotenv-scan&lt;/a&gt; to do it automatically.&lt;/p&gt;




&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;dotenv-scan&lt;/code&gt; scans your codebase for every env variable your code actually uses, then compares that against what's in your &lt;code&gt;.env&lt;/code&gt; and &lt;code&gt;.env.example&lt;/code&gt;. One command, three answers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx dotenv-scan scan
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dotenv-scan v1.0.0  ·  scanned 47 files in 284ms

❌  Missing  (3)
   DATABASE_URL      src/db.ts:12, src/config.ts:8
   JWT_SECRET        src/auth/middleware.ts:4
   STRIPE_API_KEY    src/payments/stripe.ts:22

⚠️  Undocumented  (2)
   INTERNAL_API_KEY
   DEBUG_MODE

🗑️  Unused  (1)
   OLD_REDIS_URL

✅  OK  (8)
   PORT, NODE_ENV, API_BASE_URL, ... (and 5 more)

────────────────────────────────────────
Run `dotenv-scan generate` to update .env.example
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No config file. No setup. Just &lt;code&gt;npx&lt;/code&gt; and go.&lt;/p&gt;




&lt;h2&gt;
  
  
  The three problems it catches
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Missing&lt;/strong&gt; — your code does &lt;code&gt;process.env.DATABASE_URL&lt;/code&gt; but &lt;code&gt;.env&lt;/code&gt; doesn't have it. This is the Friday deploy problem. &lt;code&gt;dotenv-scan&lt;/code&gt; catches it before you push.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unused&lt;/strong&gt; — &lt;code&gt;.env&lt;/code&gt; has &lt;code&gt;OLD_REDIS_URL&lt;/code&gt; but no file in your codebase references it. Safe to delete. &lt;code&gt;dotenv-scan&lt;/code&gt; tells you which ones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Undocumented&lt;/strong&gt; — &lt;code&gt;.env&lt;/code&gt; has &lt;code&gt;INTERNAL_API_KEY&lt;/code&gt; but &lt;code&gt;.env.example&lt;/code&gt; doesn't mention it. The next developer to clone your repo has no idea this variable needs to exist. &lt;code&gt;dotenv-scan&lt;/code&gt; flags it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Plugging it into CI
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;check&lt;/code&gt; command is designed for pipelines. It exits with code &lt;code&gt;1&lt;/code&gt; if any variables are missing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# GitHub Actions&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Check env variables&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx dotenv-scan check&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole setup. Now your Friday deploy problem becomes a PR-time failure instead.&lt;/p&gt;

&lt;p&gt;If you want to be strict — fail on unused and undocumented too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotenv-scan check &lt;span class="nt"&gt;--strict&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Auto-generating &lt;code&gt;.env.example&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The part I actually use the most is &lt;code&gt;generate&lt;/code&gt;. It writes (or updates) &lt;code&gt;.env.example&lt;/code&gt; from your scan results:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotenv-scan generate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's idempotent. If a key already exists in &lt;code&gt;.env.example&lt;/code&gt;, it's preserved. New variables found in the scan get added. Variables in &lt;code&gt;.env.example&lt;/code&gt; that are no longer referenced in your code get flagged.&lt;/p&gt;

&lt;p&gt;One command to keep your docs in sync with reality.&lt;/p&gt;




&lt;h2&gt;
  
  
  Multi-language support
&lt;/h2&gt;

&lt;p&gt;It's not just JavaScript. The scanner understands:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Language&lt;/th&gt;
&lt;th&gt;Patterns&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;JavaScript / TypeScript&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;process.env.VAR&lt;/code&gt;, &lt;code&gt;process.env['VAR']&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;os.environ['VAR']&lt;/code&gt;, &lt;code&gt;os.getenv('VAR')&lt;/code&gt;, &lt;code&gt;os.environ.get('VAR')&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Go&lt;/td&gt;
&lt;td&gt;&lt;code&gt;os.Getenv("VAR")&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ruby&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ENV['VAR']&lt;/code&gt;, &lt;code&gt;ENV.fetch('VAR')&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So if you have a monorepo with a Node.js API and a Python worker, one scan covers both.&lt;/p&gt;




&lt;h2&gt;
  
  
  The interesting part: detecting dynamic access
&lt;/h2&gt;

&lt;p&gt;The hardest case to handle was &lt;code&gt;process.env[dynamicKey]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Static analysis can't tell you which variable this reads. The key is computed at runtime — maybe it comes from a config file, maybe it's user input, maybe it's constructed from an enum. You can't enumerate it.&lt;/p&gt;

&lt;p&gt;I made a deliberate call here: don't try to be clever. Instead, detect the pattern and warn the user:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;⚠️  Dynamic access detected
   process.env[key]   src/config/loader.ts:34

   Static analysis can't determine which variables are accessed here.
   Make sure these variables are covered in your .env.example manually.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Silently ignoring it would be worse — you'd get a false "all clear" and miss variables. Failing hard would be too noisy for codebases that use this pattern intentionally. A warning with the exact location felt right.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture in one diagram
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CLI (Commander.js)
    │
    ├── Scanner ── Walker (fast-glob) ── Extractors (per-language regex)
    │                                              │
    │                                     EnvRef[] (used variables)
    │
    ├── Parser ── dotenv parser ── EnvDef[] (defined variables)
    │
    └── Analyzer ── compares used ↔ defined ↔ documented
                         │
                    AnalysisResult
                         │
                    ┌────┴────┐
                 Reporter   Generator
                (text/json)  (.env.example)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each layer is independently testable. The scanner doesn't know about &lt;code&gt;.env&lt;/code&gt; files. The analyzer doesn't know about file systems. 88 tests, zero mocks of the actual fs calls in integration tests — they run against real fixture files.&lt;/p&gt;




&lt;h2&gt;
  
  
  Zero runtime dependencies (almost)
&lt;/h2&gt;

&lt;p&gt;Three runtime deps, all small:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;chalk&lt;/code&gt; — terminal colors&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;commander&lt;/code&gt; — CLI argument parsing
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;fast-glob&lt;/code&gt; — file walking&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's it. No dotenv library — the parser is custom because I needed comment-preservation and multiline value support that dotenv packages tend to strip. No framework. Ships as a standalone CLI that you can &lt;code&gt;npx&lt;/code&gt; without worrying about what it pulls in.&lt;/p&gt;




&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# No install required&lt;/span&gt;
npx dotenv-scan scan

&lt;span class="c"&gt;# Or install globally&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; dotenv-scan
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/flyingsquirrel0419/dotenv-scan" rel="noopener noreferrer"&gt;https://github.com/flyingsquirrel0419/dotenv-scan&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If dotenv-scan saves you from a bad deploy, a ⭐ on the repo goes a long way. It's also the best way to let me know it's useful so I keep working on it.&lt;/p&gt;




&lt;p&gt;The thing I keep coming back to is how small the fix is relative to how painful the problem is. One &lt;code&gt;npx&lt;/code&gt; command in CI, and the whole class of "missing env variable in production" goes away. I wish I'd built this years ago.&lt;/p&gt;

&lt;p&gt;Happy to dig into any of the implementation details in the comments — the multi-language extractor design and the dynamic access detection decision both have interesting tradeoffs worth talking through.&lt;/p&gt;

</description>
      <category>node</category>
      <category>typescript</category>
      <category>opensource</category>
      <category>dotenv</category>
    </item>
    <item>
      <title>I Audited My Own Open Source Library and Found 9 Security Bugs. Here's Every One.</title>
      <dc:creator>날다람쥐</dc:creator>
      <pubDate>Sun, 26 Apr 2026 01:14:50 +0000</pubDate>
      <link>https://dev.to/flyingsquirrel0419/i-audited-my-own-open-source-library-and-found-9-security-bugs-heres-every-one-3dkc</link>
      <guid>https://dev.to/flyingsquirrel0419/i-audited-my-own-open-source-library-and-found-9-security-bugs-heres-every-one-3dkc</guid>
      <description>&lt;p&gt;Hey dev.to 👋&lt;/p&gt;

&lt;p&gt;If you've read my &lt;a href="https://dev.to/flyingsquirrel0419/i-got-tired-of-wiring-the-same-caching-stack-every-project-so-i-built-layercache-52e2"&gt;previous post&lt;/a&gt; about &lt;strong&gt;layercache&lt;/strong&gt;, you know it's a multi-layer caching library for Node.js — Memory → Redis → Disk behind a single &lt;code&gt;get()&lt;/code&gt; call, with stampede prevention, tag invalidation, circuit breaking, and all the production-grade stuff you eventually need.&lt;/p&gt;

&lt;p&gt;Today I'm releasing &lt;strong&gt;v1.3.3&lt;/strong&gt;, and it's different from all the previous releases.&lt;/p&gt;

&lt;p&gt;No new features. No benchmark numbers. No shiny API additions.&lt;/p&gt;

&lt;p&gt;Just nine bugs I found in my own library. I want to walk through all of them — what they were, why they happened, and what I did to fix them.&lt;/p&gt;

&lt;p&gt;Some are embarrassing. All of them are real.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I did a full security audit
&lt;/h2&gt;

&lt;p&gt;When you're building in the open and people start actually using the thing, you feel differently about the code. I went back through the internals with fresh eyes and a specific question: &lt;em&gt;what could go wrong in production under real load?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Turns out: a lot.&lt;/p&gt;

&lt;p&gt;Here's everything I found, roughly in severity order.&lt;/p&gt;




&lt;h2&gt;
  
  
  VULN-1 (HIGH): Unbounded memory growth in &lt;code&gt;keyEpochs&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The bug&lt;/strong&gt;: &lt;code&gt;CacheStackMaintenance&lt;/code&gt; uses a &lt;code&gt;Map&amp;lt;string, number&amp;gt;&lt;/code&gt; called &lt;code&gt;keyEpochs&lt;/code&gt; to track write invalidation — every time a key is deleted or updated, its epoch is bumped so stale write-behind operations know to skip it. The map grew forever. No cap, no pruning. In a long-running service writing lots of unique keys, this is a slow memory leak that only gets worse over time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix&lt;/strong&gt;: Added &lt;code&gt;MAX_KEY_EPOCHS = 50_000&lt;/code&gt; and a pruning step after every &lt;code&gt;bumpKeyEpochs()&lt;/code&gt; call. When the map exceeds the limit, the oldest 10% (lowest epoch values) get evicted.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gi"&gt;+ const MAX_KEY_EPOCHS = 50_000
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;  bumpKeyEpochs(keys: string[]): void {
    for (const key of keys) {
      this.keyEpochs.set(key, this.currentKeyEpoch(key) + 1)
    }
&lt;span class="gi"&gt;+   this.pruneKeyEpochsIfNeeded()
&lt;/span&gt;  }
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gi"&gt;+ private pruneKeyEpochsIfNeeded(): void {
+   if (this.keyEpochs.size &amp;lt;= MAX_KEY_EPOCHS) return
+   const sorted = [...this.keyEpochs.entries()].sort((a, b) =&amp;gt; a[1] - b[1])
+   const toDelete = Math.ceil(sorted.length * 0.1)
+   for (let i = 0; i &amp;lt; toDelete; i++) {
+     this.keyEpochs.delete(sorted[i][0])
+   }
+ }
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This one stings because it's exactly the kind of bug that's invisible in tests — you only see it after the process has been running for days and memory graphs start climbing.&lt;/p&gt;




&lt;h2&gt;
  
  
  VULN-2 (MED-HIGH): Unbounded queue in &lt;code&gt;FetchRateLimiter&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The bug&lt;/strong&gt;: &lt;code&gt;FetchRateLimiter&lt;/code&gt; queues fetcher requests per-bucket when rate limits are hit. The queue itself had no bound. Under sustained high contention on a single cache key, that queue would grow without limit — eventually consuming unbounded memory and causing backpressure to pile up indefinitely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix&lt;/strong&gt;: Added &lt;code&gt;MAX_QUEUE_PER_BUCKET = 10_000&lt;/code&gt;. When a bucket's queue is full, new requests bypass the rate limiter entirely rather than blocking (availability &amp;gt; strict throttling in this failure mode).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gi"&gt;+ const MAX_QUEUE_PER_BUCKET = 10_000
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;  return new Promise&amp;lt;T&amp;gt;((resolve, reject) =&amp;gt; {
    const bucketKey = this.resolveBucketKey(normalized, context)
    const queue = this.queuesByBucket.get(bucketKey) ?? []
&lt;span class="gi"&gt;+   if (queue.length &amp;gt;= MAX_QUEUE_PER_BUCKET) {
+     this.rateLimitBypasses += 1
+     task().then(resolve, reject)
+     return
+   }
&lt;/span&gt;    queue.push({ bucketKey, options: normalized, task, resolve, reject })
    ...
  })
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The bypass counter is exposed via metrics so you can see when it's happening in production.&lt;/p&gt;




&lt;h2&gt;
  
  
  VULN-3 (MEDIUM): CLI accepted unvalidated input before hitting Redis
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The bug&lt;/strong&gt;: The admin CLI (&lt;code&gt;npx layercache keys --pattern "..."&lt;/code&gt;, &lt;code&gt;invalidate --tag "..."&lt;/code&gt;, etc.) didn't validate keys, patterns, or tags before passing them to Redis operations. The runtime &lt;code&gt;CacheStack&lt;/code&gt; enforces strict validation on all inputs — the CLI was just... not doing that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix&lt;/strong&gt;: The same &lt;code&gt;validateCacheKey()&lt;/code&gt;, &lt;code&gt;validatePattern()&lt;/code&gt;, and &lt;code&gt;validateTag()&lt;/code&gt; functions used by the runtime are now called in the CLI before any Redis operation runs.&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;// cli.ts — now applied before every Redis op&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;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pattern&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;validateCliInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;validatePattern&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tag&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;validateCliInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&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="nx"&gt;validateTag&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;validateCliInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;validateCacheKey&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The runtime had this hardened back in v1.2.x. The CLI just... never got the memo.&lt;/p&gt;




&lt;h2&gt;
  
  
  VULN-4 (MEDIUM): &lt;code&gt;invalidate&lt;/code&gt; could wipe the entire cache with no confirmation
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The bug&lt;/strong&gt;: Running &lt;code&gt;npx layercache invalidate&lt;/code&gt; with no &lt;code&gt;--pattern&lt;/code&gt; or &lt;code&gt;--tag&lt;/code&gt; defaults to &lt;code&gt;*&lt;/code&gt; — which matches every key in the cache. There was no confirmation step. One mistyped command in a terminal and your entire production cache is gone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix&lt;/strong&gt;: If you run &lt;code&gt;invalidate&lt;/code&gt; with no targeting flags and there are keys to delete, the CLI now refuses and asks you to pass &lt;code&gt;--force&lt;/code&gt; explicitly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;npx layercache invalidate
&lt;span class="go"&gt;Warning: this operation will invalidate 14,823 keys. Use --force to confirm.

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;npx layercache invalidate &lt;span class="nt"&gt;--force&lt;/span&gt;
&lt;span class="go"&gt;Invalidated 14,823 keys.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This one is embarrassing because I added the CLI &lt;em&gt;for convenience in production&lt;/em&gt;, and then left a footgun that could nuke the entire cache by accident. Glad I caught it before anyone else did.&lt;/p&gt;




&lt;h2&gt;
  
  
  VULN-5 (MEDIUM): &lt;code&gt;TagIndex&lt;/code&gt; pruning was silently broken
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The bug&lt;/strong&gt;: &lt;code&gt;TagIndex&lt;/code&gt; uses a &lt;code&gt;knownKeys&lt;/code&gt; collection to track which keys exist, so prefix and wildcard invalidation can find them. Since v1.2.0, it had a &lt;code&gt;maxKnownKeys&lt;/code&gt; limit to prevent unbounded growth — but it was a &lt;code&gt;Set&amp;lt;string&amp;gt;&lt;/code&gt;, which has no access-recency ordering. The pruning code sorted and evicted by... nothing meaningful. It was effectively random deletion, not LRU eviction. Hot keys were just as likely to get pruned as cold ones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix&lt;/strong&gt;: Changed &lt;code&gt;knownKeys&lt;/code&gt; from &lt;code&gt;Set&amp;lt;string&amp;gt;&lt;/code&gt; to &lt;code&gt;Map&amp;lt;string, number&amp;gt;&lt;/code&gt; where the value is a timestamp updated on every &lt;code&gt;touch()&lt;/code&gt; or &lt;code&gt;track()&lt;/code&gt; call. Now pruning correctly evicts least-recently-used entries.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- private readonly knownKeys = new Set&amp;lt;string&amp;gt;()
&lt;/span&gt;&lt;span class="gi"&gt;+ private readonly knownKeys = new Map&amp;lt;string, number&amp;gt;()  // key → last-touched timestamp
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;  async touch(key: string): Promise&amp;lt;void&amp;gt; {
&lt;span class="gd"&gt;-   this.knownKeys.add(key)
&lt;/span&gt;&lt;span class="gi"&gt;+   this.knownKeys.set(key, Date.now())  // updates on every access
&lt;/span&gt;    this.pruneKnownKeysIfNeeded()
  }
&lt;span class="err"&gt;
&lt;/span&gt;  private pruneKnownKeysIfNeeded(): void {
    if (!this.maxKnownKeys || this.knownKeys.size &amp;lt;= this.maxKnownKeys) return
&lt;span class="gd"&gt;-   // old: iterated a Set with no ordering guarantee
&lt;/span&gt;&lt;span class="gi"&gt;+   const sorted = [...this.knownKeys.entries()].sort((a, b) =&amp;gt; a[1] - b[1])
+   const toDelete = Math.ceil(sorted.length * 0.1)
+   for (let i = 0; i &amp;lt; toDelete; i++) this.knownKeys.delete(sorted[i][0])
&lt;/span&gt;  }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The limit was there since v1.2.0 and &lt;em&gt;looked&lt;/em&gt; like it was working. It wasn't.&lt;/p&gt;




&lt;h2&gt;
  
  
  VULN-6 (MEDIUM): TOCTOU race in snapshot file writes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The bug&lt;/strong&gt;: The snapshot persistence code (&lt;code&gt;persistToFile()&lt;/code&gt;) wrote directly to the target path. If the process crashed mid-write, you'd get a partial or corrupt snapshot file with no recovery path. Worse, if two processes tried to write a snapshot concurrently, they'd clobber each other.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix&lt;/strong&gt;: Centralized all snapshot writes through two new utilities: &lt;code&gt;atomicWriteTempPath()&lt;/code&gt; generates a randomized temp filename, and &lt;code&gt;commitAtomicWrite()&lt;/code&gt; renames the temp file to the target — an atomic operation on all POSIX-compliant filesystems.&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/internal/CacheSnapshotFile.ts&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;atomicWriteTempPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;targetPath&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="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;targetPath&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.tmp-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;randomBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&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;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;commitAtomicWrite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tempPath&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="nx"&gt;targetPath&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="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&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="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;rename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tempPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;targetPath&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;error&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;unlink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tempPath&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;error&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;Write to the temp path, then &lt;code&gt;fs.rename()&lt;/code&gt;. If anything goes wrong before the rename, the original snapshot is untouched. If the rename succeeds, readers see either the old file or the new one — never a partial state.&lt;/p&gt;




&lt;h2&gt;
  
  
  VULN-7 (LOW): Memory leak in &lt;code&gt;layerDegradedUntil&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The bug&lt;/strong&gt;: When a cache layer fails and enters degraded mode, &lt;code&gt;CacheStack&lt;/code&gt; stores &lt;code&gt;layerDegradedUntil.set(layer.name, expiryTimestamp)&lt;/code&gt;. When the degradation period expired, the entry was never removed. In a service where Redis occasionally has brief hiccups, this map accumulates an entry per layer per incident — forever.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix&lt;/strong&gt;: On every read that checks degradation status, if the entry has expired, delete it before returning.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;  const degradedUntil = this.layerDegradedUntil.get(layer.name)
  const skip = shouldSkipDegradedLayer(degradedUntil)
&lt;span class="gi"&gt;+ if (!skip &amp;amp;&amp;amp; degradedUntil !== undefined) {
+   this.layerDegradedUntil.delete(layer.name)  // clean up expired entry
+ }
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One-liner fix, but this would quietly accumulate in any service that ever experiences Redis downtime.&lt;/p&gt;




&lt;h2&gt;
  
  
  VULN-8 (LOW): &lt;code&gt;Math.random()&lt;/code&gt; for TTL jitter
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The bug&lt;/strong&gt;: &lt;code&gt;TtlResolver.applyJitter()&lt;/code&gt; used &lt;code&gt;Math.random()&lt;/code&gt; to spread cache expiration times. &lt;code&gt;Math.random()&lt;/code&gt; is not cryptographically secure — it's seeded from a deterministic internal state. For TTL jitter this is mostly harmless, but using a predictable PRNG to compute expiration windows is bad practice. In theory, an observer who can measure cache miss patterns could infer when keys are about to expire.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix&lt;/strong&gt;: Replaced &lt;code&gt;Math.random()&lt;/code&gt; with a &lt;code&gt;crypto.randomBytes&lt;/code&gt;-based equivalent.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gi"&gt;+ import { randomBytes } from 'node:crypto'
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gi"&gt;+ export const secureRandom = {
+   value(): number {
+     return randomBytes(4).readUInt32BE(0) / 0x100000000
+   }
+ }
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;  applyJitter(ttl: number | undefined, jitter: number | undefined): number | undefined {
    if (!ttl || ttl &amp;lt;= 0 || !jitter || jitter &amp;lt;= 0) return ttl
&lt;span class="gd"&gt;-   const delta = (Math.random() * 2 - 1) * jitter
&lt;/span&gt;&lt;span class="gi"&gt;+   const delta = (secureRandom.value() * 2 - 1) * jitter
&lt;/span&gt;    return Math.max(1, Math.round(ttl + delta))
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;randomBytes(4)&lt;/code&gt; is fast. No measurable performance impact.&lt;/p&gt;




&lt;h2&gt;
  
  
  VULN-9 (LOW): Background refresh failures logged at &lt;code&gt;debug&lt;/code&gt; level
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The bug&lt;/strong&gt;: When a stale-while-revalidate background refresh fails — upstream is down, fetcher throws, timeout — the error was logged at &lt;code&gt;debug&lt;/code&gt; level. In almost every production setup, &lt;code&gt;debug&lt;/code&gt; logs are disabled. So these failures were silently swallowed. You'd see keys serving stale values with no log entry explaining why.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix&lt;/strong&gt;: One-line change.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- this.logger.debug?.('background-refresh-failed', { key, error })
&lt;/span&gt;&lt;span class="gi"&gt;+ this.logger.warn?.('background-refresh-failed', { key, error })
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I genuinely don't know how long this was invisible. If you've been running layercache with &lt;code&gt;staleWhileRevalidate&lt;/code&gt; and wondering why some keys feel permanently stale — this might be why.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I learned from this
&lt;/h2&gt;

&lt;p&gt;A few patterns that caused most of these bugs:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unbounded Maps are silent killers.&lt;/strong&gt; VULN-1, VULN-5, and VULN-7 are all variations of the same mistake: I allocated a &lt;code&gt;Map&lt;/code&gt; or &lt;code&gt;Set&lt;/code&gt;, put the bounds/pruning logic on my TODO list, and shipped without it. In tests, these are invisible. In production they show up in memory graphs after days of uptime.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Internal tools don't inherit production hardening automatically.&lt;/strong&gt; VULN-3 and VULN-4 happened because the CLI was an afterthought. The core library had strict input validation. The CLI that wraps it did not. Every interface — HTTP endpoints, CLIs, admin tools — needs its own hardening pass.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Debug-level logging" is often "no logging" in production.&lt;/strong&gt; VULN-9 was a legitimate design decision that turned out to be wrong in practice. Background refresh failures are operational signals, not debugging details.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TOCTOU bugs hide behind success.&lt;/strong&gt; VULN-6 was only a problem during crashes or concurrent writes — situations that don't happen in unit tests. The atomic write pattern is just the right default, regardless.&lt;/p&gt;




&lt;h2&gt;
  
  
  Upgrade
&lt;/h2&gt;

&lt;p&gt;v1.3.3 is a drop-in upgrade. No API changes, no migration needed.&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;layercache@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Full changelog: &lt;a href="https://github.com/flyingsquirrel0419/layercache/blob/main/CHANGELOG.md" rel="noopener noreferrer"&gt;CHANGELOG.md&lt;/a&gt;&lt;br&gt;
Security PR: &lt;a href="https://github.com/flyingsquirrel0419/layercache/pull/19" rel="noopener noreferrer"&gt;#19&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;If you're already using layercache — please upgrade. If you're not, this might be a decent time to take a look:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🐙 &lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/flyingsquirrel0419/layercache" rel="noopener noreferrer"&gt;github.com/flyingsquirrel0419/layercache&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📦 &lt;strong&gt;npm&lt;/strong&gt;: &lt;a href="https://www.npmjs.com/package/layercache" rel="noopener noreferrer"&gt;npmjs.com/package/layercache&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📖 &lt;strong&gt;Docs&lt;/strong&gt;: &lt;a href="https://github.com/flyingsquirrel0419/layercache/blob/main/docs/api.md" rel="noopener noreferrer"&gt;API Reference&lt;/a&gt; · &lt;a href="https://github.com/flyingsquirrel0419/layercache/blob/main/docs/tutorial.md" rel="noopener noreferrer"&gt;Tutorial&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If this has been useful, a ⭐ on GitHub helps a lot — it's the main signal that helps other developers find the library. Thanks for reading. 🙏&lt;/p&gt;

</description>
      <category>node</category>
      <category>typescript</category>
      <category>redis</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I got tired of wiring the same caching stack every project, so I built LayerCache</title>
      <dc:creator>날다람쥐</dc:creator>
      <pubDate>Sat, 18 Apr 2026 16:40:12 +0000</pubDate>
      <link>https://dev.to/flyingsquirrel0419/i-got-tired-of-wiring-the-same-caching-stack-every-project-so-i-built-layercache-52e2</link>
      <guid>https://dev.to/flyingsquirrel0419/i-got-tired-of-wiring-the-same-caching-stack-every-project-so-i-built-layercache-52e2</guid>
      <description>&lt;p&gt;Every Node.js service I've worked on hits the same caching wall. It always starts the same way.&lt;/p&gt;

&lt;p&gt;You add an in-memory cache. It's fast. Life is good.&lt;/p&gt;

&lt;p&gt;Then you scale to multiple instances. Now each server has its own view of the data. Stale reads start showing up in production. So you add Redis. Now all your instances share the same cache. Problem solved — until you realize every single request is paying a Redis round-trip, even for data that barely changes.&lt;/p&gt;

&lt;p&gt;So you bring back the in-memory layer on top of Redis. Now you have L1 (memory) and L2 (Redis). But what happens when a key expires and 200 requests hit at the same time? They all miss L1, all miss L2, and they all go straight to the database simultaneously. Cache stampede. Your DB is not happy.&lt;/p&gt;

&lt;p&gt;You add stampede protection. Then Redis goes down one day, and your entire cache blows up instead of gracefully falling back. You add circuit breaking. Then you realize your memory caches across instances are now serving different data and you need a pub/sub invalidation bus to keep them in sync...&lt;/p&gt;

&lt;p&gt;It never ends.&lt;/p&gt;

&lt;p&gt;I've wired this stack more than once. It's not that any single piece is hard — it's that getting all of it working together correctly, with proper testing and production-grade reliability, takes real engineering time every time.&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://github.com/flyingsquirrel0419/layercache" rel="noopener noreferrer"&gt;LayerCache&lt;/a&gt; to do it once and stop repeating myself.&lt;/p&gt;




&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;LayerCache stacks multiple cache layers (Memory → Redis → Disk) behind a single &lt;code&gt;get()&lt;/code&gt; call.&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;CacheStack&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;MemoryLayer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;RedisLayer&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;layercache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Redis&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;ioredis&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cache&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;CacheStack&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MemoryLayer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;maxSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RedisLayer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Redis&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="na"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3600&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;user&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;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user:123&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;123&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a cache &lt;strong&gt;hit&lt;/strong&gt;: serves the fastest available layer, then automatically backfills the layers above it. So if L1 is cold but L2 (Redis) has the value, L1 gets filled for the next request.&lt;/p&gt;

&lt;p&gt;On a cache &lt;strong&gt;miss&lt;/strong&gt;: the fetcher function runs &lt;strong&gt;exactly once&lt;/strong&gt;, no matter how many requests are waiting. All concurrent callers get the same promise.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;your request flood
       │
┌──────▼──────┐
│ L1 Memory   │  ~0.005 ms  ← serves from here if warm
│             │
│ L2 Redis    │  ~0.2 ms   ← falls through to here if L1 cold
│             │
│ L3 Disk     │  ~2 ms     ← optional persistent layer
│             │
│ Fetcher()   │             ← runs ONCE even under 100 concurrent requests
└─────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Solving the stampede problem
&lt;/h2&gt;

&lt;p&gt;In a benchmark with 75 concurrent requests hitting an expired key:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Origin fetches&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;No cache&lt;/td&gt;
&lt;td&gt;375&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LayerCache&lt;/td&gt;
&lt;td&gt;5 (one per layer)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The local single-flight is handled by sharing an in-flight promise across concurrent callers. No mutex queue. No serialization.&lt;/p&gt;

&lt;p&gt;For &lt;strong&gt;distributed environments&lt;/strong&gt; — multiple Node.js processes or machines — &lt;code&gt;RedisSingleFlightCoordinator&lt;/code&gt; extends this across instances using distributed locks.&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;RedisSingleFlightCoordinator&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;layercache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cache&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;CacheStack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;layers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;singleFlightCoordinator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RedisSingleFlightCoordinator&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;redis&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;In a test with 60 concurrent requests across multiple instances: &lt;strong&gt;1 origin fetch total&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Keeping L1 caches in sync across instances
&lt;/h2&gt;

&lt;p&gt;The classic problem with in-process memory caches in a multi-instance setup: if you invalidate a key on Server A, Servers B and C still serve the old value from their L1.&lt;/p&gt;

&lt;p&gt;LayerCache solves this with a Redis pub/sub invalidation bus.&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;RedisInvalidationBus&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;layercache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cache&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;CacheStack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;layers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;invalidationBus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RedisInvalidationBus&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;publisher&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;subscriber&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Redis&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="c1"&gt;// separate connection for sub&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// invalidating on one instance flushes L1 on all instances&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user:123&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;h2&gt;
  
  
  When Redis dies
&lt;/h2&gt;

&lt;p&gt;This is where a lot of hand-rolled caching setups break badly. LayerCache has two modes:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strict mode&lt;/strong&gt; (default): if any layer fails, the operation fails. Good when you need strong consistency guarantees.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Graceful degradation&lt;/strong&gt;: failed layers are temporarily skipped. The cache keeps working by going directly to the fetcher.&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cache&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;CacheStack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;layers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;gracefulDegradation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;retryAfterMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="nx"&gt;_000&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;I tested this with 500ms of injected Redis latency (way above the 200ms command timeout):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Strict&lt;/th&gt;
&lt;th&gt;Graceful&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;L1 warm hit&lt;/td&gt;
&lt;td&gt;✅ 0.065 ms&lt;/td&gt;
&lt;td&gt;✅ 0.065 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;L2 hit (Redis slow)&lt;/td&gt;
&lt;td&gt;❌ timeout&lt;/td&gt;
&lt;td&gt;✅ 201 ms (fell back to fetcher)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cold miss (Redis slow)&lt;/td&gt;
&lt;td&gt;❌ timeout&lt;/td&gt;
&lt;td&gt;✅ 200 ms (fell back to fetcher)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;L1 hot hits aren't affected at all since they never touch Redis.&lt;/p&gt;




&lt;h2&gt;
  
  
  Benchmark numbers
&lt;/h2&gt;

&lt;p&gt;Ran on a single-core VM with real Docker-backed Redis.&lt;/p&gt;

&lt;h3&gt;
  
  
  Warm hit latency
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;layered (L1 hit):   0.005 ms avg  (1006x faster than no-cache)
memory only:        0.010 ms avg  ( 503x faster than no-cache)
no-cache:           5.030 ms avg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  HTTP throughput (autocannon, 40 connections, 8 seconds)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/layered:   16,211 req/s  —  1.9 ms avg latency
/memory:    16,031 req/s  —  1.9 ms avg latency
/nocache:      158 req/s  — 253.2 ms avg latency
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Memory pressure
&lt;/h3&gt;

&lt;p&gt;With L1 capped at 25 keys and 180 unique keys inserted (256 KiB each), revisits served &lt;strong&gt;0 origin refetches&lt;/strong&gt; — the layer evicted correctly and Redis backed the misses.&lt;/p&gt;

&lt;p&gt;Full benchmark methodology and raw output: &lt;a href="https://github.com/flyingsquirrel0419/layercache/blob/main/docs/benchmarking.md" rel="noopener noreferrer"&gt;docs/benchmarking.md&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Other things it does
&lt;/h2&gt;

&lt;p&gt;I don't want to just dump a feature list, but a few things worth calling out:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tag invalidation&lt;/strong&gt; — attach tags to keys and invalidate all of them at once:&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;await&lt;/span&gt; &lt;span class="nx"&gt;cache&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;post:42&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;tags&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;posts&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;user:7&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="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invalidateByTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user:7&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// clears all keys tagged with user:7&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Stale-while-revalidate&lt;/strong&gt; — return the cached value immediately, refresh in the background:&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;new&lt;/span&gt; &lt;span class="nc"&gt;MemoryLayer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;staleWhileRevalidate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;300&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;Framework middleware&lt;/strong&gt; — drop-in for Express, Fastify, Hono, tRPC, GraphQL:&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="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&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/users&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nf"&gt;createExpressCacheMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;tags&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;users&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;keyResolver&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`users:&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;url&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;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="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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUsers&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;Admin CLI&lt;/strong&gt; — inspect a live Redis-backed cache without writing code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx layercache stats
npx layercache keys &lt;span class="nt"&gt;--pattern&lt;/span&gt; &lt;span class="s2"&gt;"user:*"&lt;/span&gt;
npx layercache invalidate &lt;span class="nt"&gt;--tag&lt;/span&gt; posts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Getting started
&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;layercache
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Memory-only (no Redis needed):&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cache&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;CacheStack&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MemoryLayer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&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;data&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;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;key&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;fetchData&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Full distributed setup:&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;CacheStack&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;MemoryLayer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;RedisLayer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;RedisInvalidationBus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;RedisSingleFlightCoordinator&lt;/span&gt;&lt;span class="p"&gt;,&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;layercache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;redis&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;Redis&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;cache&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;CacheStack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MemoryLayer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;maxSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RedisLayer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;compression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gzip&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="na"&gt;invalidationBus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RedisInvalidationBus&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;publisher&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;subscriber&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Redis&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="na"&gt;singleFlightCoordinator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RedisSingleFlightCoordinator&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;redis&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="na"&gt;gracefulDegradation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;retryAfterMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="nx"&gt;_000&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;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/flyingsquirrel0419/layercache" rel="noopener noreferrer"&gt;https://github.com/flyingsquirrel0419/layercache&lt;/a&gt;
Please star the GitHub repo. It help me much! :&amp;gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;npm&lt;/strong&gt;: &lt;a href="https://www.npmjs.com/package/layercache" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/layercache&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docs&lt;/strong&gt;: &lt;a href="https://github.com/flyingsquirrel0419/layercache/tree/main/docs" rel="noopener noreferrer"&gt;https://github.com/flyingsquirrel0419/layercache/tree/main/docs&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;The part I found most interesting to design was the stampede guard — specifically making sure concurrent callers share a promise rather than queueing through a mutex, and then extending that behavior across processes with Redis. Happy to dig into any of that if you're curious.&lt;/p&gt;

</description>
      <category>node</category>
      <category>typescript</category>
      <category>redis</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
