<?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>Forem: Tommaso Bertocchi</title>
    <description>The latest articles on Forem by Tommaso Bertocchi (@sonotommy).</description>
    <link>https://forem.com/sonotommy</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3399256%2F111d6919-72dc-4992-a6c6-2b20a4ccf85b.jpeg</url>
      <title>Forem: Tommaso Bertocchi</title>
      <link>https://forem.com/sonotommy</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/sonotommy"/>
    <language>en</language>
    <item>
      <title>10 Best Cybersecurity Tools Every Developer Should Know (Including One You've Never Heard Of)</title>
      <dc:creator>Tommaso Bertocchi</dc:creator>
      <pubDate>Fri, 17 Apr 2026 09:50:05 +0000</pubDate>
      <link>https://forem.com/sonotommy/10-best-cybersecurity-tools-every-developer-should-know-including-one-youve-never-heard-of-598b</link>
      <guid>https://forem.com/sonotommy/10-best-cybersecurity-tools-every-developer-should-know-including-one-youve-never-heard-of-598b</guid>
      <description>&lt;p&gt;&lt;em&gt;Cybersecurity isn't just for ops teams anymore. As developers, we ship code that handles user data, processes file uploads, and talks to external services. The tools below will help you build safer applications — from scanning malware in uploaded files to auditing your dependencies.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  1. 🍋 pompelmi — Antivirus Scanning for Node.js
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Website:&lt;/strong&gt; &lt;a href="https://pompelmi.app/" rel="noopener noreferrer"&gt;pompelmi.app&lt;/a&gt; | &lt;strong&gt;npm:&lt;/strong&gt; &lt;code&gt;pompelmi&lt;/code&gt; | &lt;strong&gt;License:&lt;/strong&gt; ISC&lt;/p&gt;

&lt;p&gt;If your Node.js application accepts file uploads, &lt;strong&gt;pompelmi&lt;/strong&gt; is the tool you didn't know you needed.&lt;/p&gt;

&lt;p&gt;It's a minimal, zero-dependency wrapper around &lt;a href="https://www.clamav.net/" rel="noopener noreferrer"&gt;ClamAV&lt;/a&gt; that lets you scan any file path and get back a clean, typed result — no daemons, no cloud calls, no native bindings.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;scan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Verdict&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pompelmi&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;scanUpload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filePath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;scan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filePath&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;result&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;Verdict&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Malicious&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;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;Upload rejected: malware detected&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;return&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;// Verdict.Clean | Verdict.Malicious | Verdict.ScanError&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What makes it special:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One function&lt;/strong&gt; — just call &lt;code&gt;scan(path)&lt;/code&gt; and await a typed &lt;code&gt;Verdict&lt;/code&gt; Symbol&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero runtime dependencies&lt;/strong&gt; — uses Node's built-in &lt;code&gt;child_process&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-platform&lt;/strong&gt; — works on macOS, Linux, and Windows&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No daemon required&lt;/strong&gt; — invokes &lt;code&gt;clamscan&lt;/code&gt; directly, no background process to manage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exit-code mapped&lt;/strong&gt; — no stdout parsing, no brittle regex&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Install it in seconds:&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;pompelmi
&lt;span class="c"&gt;# macOS&lt;/span&gt;
brew &lt;span class="nb"&gt;install &lt;/span&gt;clamav &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; freshclam
&lt;span class="c"&gt;# Linux&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; clamav clamav-daemon &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;freshclam
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're building an API that accepts PDFs, images, or documents from untrusted users, pompelmi is a one-liner that can save you from a catastrophic malware incident.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🔗 Full docs, Docker guide, and API reference at &lt;a href="https://pompelmi.app/" rel="noopener noreferrer"&gt;pompelmi.app&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  2. 🔍 Snyk — Find and Fix Vulnerabilities in Your Code
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Website:&lt;/strong&gt; &lt;a href="https://snyk.io/" rel="noopener noreferrer"&gt;snyk.io&lt;/a&gt; | &lt;strong&gt;Free tier:&lt;/strong&gt; Yes&lt;/p&gt;

&lt;p&gt;Snyk is one of the most developer-friendly security tools available. It scans your project's dependencies (npm, pip, Maven, etc.), your container images, and even your Infrastructure-as-Code files for known vulnerabilities.&lt;/p&gt;

&lt;p&gt;What stands out is the &lt;strong&gt;fix PRs&lt;/strong&gt; feature — Snyk can automatically open a pull request to upgrade a vulnerable dependency. It integrates directly into GitHub, GitLab, and VS Code.&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; snyk
snyk auth
snyk &lt;span class="nb"&gt;test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Continuous dependency vulnerability scanning in CI/CD pipelines.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. 🛡️ OWASP ZAP — The Classic Web App Scanner
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Website:&lt;/strong&gt; &lt;a href="https://www.zaproxy.org/" rel="noopener noreferrer"&gt;zaproxy.org&lt;/a&gt; | &lt;strong&gt;License:&lt;/strong&gt; Apache 2.0&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;OWASP Zed Attack Proxy (ZAP)&lt;/strong&gt; is a battle-tested, open-source web application security scanner maintained by OWASP. It can find XSS, SQL injection, CSRF, and dozens of other vulnerabilities by actively probing your running app.&lt;/p&gt;

&lt;p&gt;It comes with both a GUI and a headless/CLI mode, making it easy to embed into your CI pipeline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Integration and regression security testing of web apps before deployment.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. 🔐 Vault by HashiCorp — Secrets Management
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Website:&lt;/strong&gt; &lt;a href="https://www.vaultproject.io/" rel="noopener noreferrer"&gt;vaultproject.io&lt;/a&gt; | &lt;strong&gt;License:&lt;/strong&gt; BSL 1.1 / open-source editions available&lt;/p&gt;

&lt;p&gt;Hardcoded secrets in your codebase are a ticking time bomb. &lt;strong&gt;HashiCorp Vault&lt;/strong&gt; gives you a centralized, auditable secrets store with fine-grained access control.&lt;/p&gt;

&lt;p&gt;It supports dynamic secrets (credentials that are generated on demand and auto-expire), encryption as a service, and integrations with AWS, Kubernetes, and more.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vault kv put secret/myapp &lt;span class="nv"&gt;db_password&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"supersecret"&lt;/span&gt;
vault kv get secret/myapp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Teams managing API keys, database credentials, and certificates across multiple services.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. 🧱 Helmet.js — HTTP Security Headers for Express
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Website:&lt;/strong&gt; &lt;a href="https://helmetjs.github.io/" rel="noopener noreferrer"&gt;helmetjs.github.io&lt;/a&gt; | &lt;strong&gt;npm:&lt;/strong&gt; &lt;code&gt;helmet&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;A surprising number of web vulnerabilities are mitigated simply by setting the right HTTP response headers. &lt;strong&gt;Helmet&lt;/strong&gt; is a tiny Express middleware that sets headers like &lt;code&gt;Content-Security-Policy&lt;/code&gt;, &lt;code&gt;X-Frame-Options&lt;/code&gt;, &lt;code&gt;Strict-Transport-Security&lt;/code&gt;, and more.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&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;helmet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;helmet&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;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;helmet&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One line of code. Massive security improvement. No excuses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Any Node.js/Express application serving a frontend.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. 🕵️ Trivy — Container &amp;amp; IaC Vulnerability Scanner
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Website:&lt;/strong&gt; &lt;a href="https://trivy.dev/" rel="noopener noreferrer"&gt;trivy.dev&lt;/a&gt; | &lt;strong&gt;License:&lt;/strong&gt; Apache 2.0&lt;/p&gt;

&lt;p&gt;If you ship Docker containers, &lt;strong&gt;Trivy&lt;/strong&gt; by Aqua Security is the go-to scanner. It checks OS packages, language-specific packages, IaC misconfigurations, and even secrets accidentally baked into your image layers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;trivy image node:18-alpine
trivy fs ./myproject
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's fast, has zero configuration for basic use, and integrates cleanly with GitHub Actions and other CI systems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Scanning container images and repositories before pushing to production.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. 🔎 Semgrep — Static Analysis That Doesn't Suck
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Website:&lt;/strong&gt; &lt;a href="https://semgrep.dev/" rel="noopener noreferrer"&gt;semgrep.dev&lt;/a&gt; | &lt;strong&gt;Free tier:&lt;/strong&gt; Yes&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Semgrep&lt;/strong&gt; is a fast, lightweight static analysis tool that lets you write rules in a syntax that closely mirrors the code you're analyzing. It supports 30+ languages and comes with thousands of community rules for catching common security bugs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;semgrep &lt;span class="nt"&gt;--config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;p/security-audit ./src
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unlike traditional SAST tools, Semgrep rules are readable and easy to customize — you can write your own rule in minutes to enforce team-specific security patterns.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Catching security bugs and anti-patterns during code review and CI.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. 🌐 Burp Suite Community Edition — Manual Pen Testing
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Website:&lt;/strong&gt; &lt;a href="https://portswigger.net/burp" rel="noopener noreferrer"&gt;portswigger.net/burp&lt;/a&gt; | &lt;strong&gt;Free tier:&lt;/strong&gt; Community Edition&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Burp Suite&lt;/strong&gt; is the industry standard for manual web application penetration testing. It acts as a proxy between your browser and the server, letting you intercept, inspect, and modify HTTP/S requests in real time.&lt;/p&gt;

&lt;p&gt;The Community Edition is free and includes the intercepting proxy, repeater, decoder, and intruder tools — everything you need to manually probe your own app for logic flaws and injection points.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Deep-dive manual security testing and bug bounty research.&lt;/p&gt;




&lt;h2&gt;
  
  
  9. 🔑 age — Modern File Encryption
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Website:&lt;/strong&gt; &lt;a href="https://age-encryption.org/" rel="noopener noreferrer"&gt;age-encryption.org&lt;/a&gt; | &lt;strong&gt;License:&lt;/strong&gt; BSD 3-Clause&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;age&lt;/strong&gt; (pronounced like the word) is a modern, simple file encryption tool and Go library. It's designed as a safer replacement for GPG, with a much simpler interface and no key management footguns.&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;# Encrypt a file&lt;/span&gt;
age &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="nv"&gt;$RECIPIENT_PUBLIC_KEY&lt;/span&gt; secret.txt &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; secret.txt.age

&lt;span class="c"&gt;# Decrypt it&lt;/span&gt;
age &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; ~/.ssh/id_ed25519 secret.txt.age &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; secret.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Encrypting secrets files, backups, and config files before storing or transmitting them.&lt;/p&gt;




&lt;h2&gt;
  
  
  10. 📋 npm audit — The One You're Already Ignoring
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Built into npm&lt;/strong&gt; | &lt;strong&gt;Free&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Okay, this one's a reminder, not a discovery. &lt;code&gt;npm audit&lt;/code&gt; is built into every npm installation and checks your dependency tree against the GitHub Advisory Database for known CVEs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm audit
npm audit fix
npm audit fix &lt;span class="nt"&gt;--force&lt;/span&gt;  &lt;span class="c"&gt;# upgrades breaking changes too&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The dirty secret is that most developers run it once and then ignore the warnings. Make it part of your CI pipeline with &lt;code&gt;npm audit --audit-level=high&lt;/code&gt; to fail builds on high-severity issues. No excuses — it's already installed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Every single Node.js project, every single time.&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary Table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Language/Platform&lt;/th&gt;
&lt;th&gt;Free?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;pompelmi&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Malware scanning&lt;/td&gt;
&lt;td&gt;Node.js&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Snyk&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Dependency scanning&lt;/td&gt;
&lt;td&gt;Polyglot&lt;/td&gt;
&lt;td&gt;✅ (tier)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;OWASP ZAP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Web app scanning&lt;/td&gt;
&lt;td&gt;Any&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;HashiCorp Vault&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Secrets management&lt;/td&gt;
&lt;td&gt;Any&lt;/td&gt;
&lt;td&gt;✅ (OSS)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Helmet.js&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;HTTP headers&lt;/td&gt;
&lt;td&gt;Node.js/Express&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Trivy&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Container scanning&lt;/td&gt;
&lt;td&gt;Docker/IaC&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Semgrep&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Static analysis&lt;/td&gt;
&lt;td&gt;30+ languages&lt;/td&gt;
&lt;td&gt;✅ (tier)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Burp Suite&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Pen testing&lt;/td&gt;
&lt;td&gt;Web&lt;/td&gt;
&lt;td&gt;✅ (CE)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;age&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;File encryption&lt;/td&gt;
&lt;td&gt;Any&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;npm audit&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Dependency audit&lt;/td&gt;
&lt;td&gt;Node.js&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




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

&lt;p&gt;Security doesn't have to be overwhelming. These ten tools cover the most common attack surfaces a typical application exposes: &lt;strong&gt;vulnerable dependencies, insecure headers, unscanned uploads, leaked secrets, and misconfigured containers&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Start with the ones closest to your stack. If you're building a Node.js API that accepts file uploads, &lt;strong&gt;pompelmi&lt;/strong&gt; is the first tool you should add today — it's a &lt;code&gt;npm install&lt;/code&gt; away and could be the difference between a safe app and a headline.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Found this useful? Drop a ❤️ and share it with your team. Security is everyone's job.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>node</category>
      <category>webdev</category>
      <category>devops</category>
    </item>
    <item>
      <title>10 npm packages you will use in 2026</title>
      <dc:creator>Tommaso Bertocchi</dc:creator>
      <pubDate>Thu, 16 Apr 2026 06:59:58 +0000</pubDate>
      <link>https://forem.com/sonotommy/10-npm-packages-you-will-use-in-2026-796</link>
      <guid>https://forem.com/sonotommy/10-npm-packages-you-will-use-in-2026-796</guid>
      <description>&lt;p&gt;The npm ecosystem moves fast.&lt;/p&gt;

&lt;p&gt;Libraries you'd never heard of six months ago are now in half the starters on GitHub.&lt;br&gt;
Some are genuinely better. Some are hype.&lt;/p&gt;

&lt;p&gt;Here are 10 that I think are actually worth your time in 2026.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Hono, Drizzle, Zod, Vitest, Effect, pompelmi, ofetch, unstorage, tsx, and oslo — one for almost every layer of your stack.&lt;/p&gt;
&lt;/blockquote&gt;


&lt;h2&gt;
  
  
  1. Hono — the web framework that actually stays out of your way
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What it is:&lt;/strong&gt;&lt;br&gt;
A ultrafast, edge-first web framework. Runs on Cloudflare Workers, Deno, Bun, Node.js — all with the same API.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt;&lt;br&gt;
Express is still fine, but it was designed before edge runtimes existed. Hono was built for the current landscape. The router is faster than Express in every benchmark I've seen, and the middleware system is dead simple.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Hono&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;hono&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;app&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;Hono&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;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;/hello&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&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;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;works everywhere&lt;/span&gt;&lt;span class="dl"&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;default&lt;/span&gt; &lt;span class="nx"&gt;app&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;Links:&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;npm install hono&lt;/code&gt; — &lt;a href="https://hono.dev" rel="noopener noreferrer"&gt;honojs.dev&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  2. Drizzle ORM — SQL without losing your mind
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What it is:&lt;/strong&gt;&lt;br&gt;
A TypeScript ORM that writes SQL you'd actually recognize. Schema defined in TypeScript, queries look like SQL, types flow through automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt;&lt;br&gt;
Prisma is great but generates types at build time via a separate CLI step. Drizzle infers everything from your schema file at the TypeScript level — no codegen, no extra process, faster feedback loop.&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="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;select&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="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&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;Links:&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;npm install drizzle-orm&lt;/code&gt; — &lt;a href="https://orm.drizzle.team" rel="noopener noreferrer"&gt;orm.drizzle.team&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  3. Zod — runtime validation that TypeScript actually trusts
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What it is:&lt;/strong&gt;&lt;br&gt;
Schema declaration and validation library. Define a shape once, get both a TypeScript type and a runtime validator.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt;&lt;br&gt;
TypeScript only protects you at compile time. The moment data comes from a form, an API, or a query string — you're on your own. Zod bridges that gap cleanly.&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;z&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zod&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;UserSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;age&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;18&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="nx"&gt;UserSchema&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;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// throws if invalid&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;If you're building APIs in 2026 without Zod (or something like it), you're writing validation by hand and lying to yourself about it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Links:&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;npm install zod&lt;/code&gt; — &lt;a href="https://zod.dev" rel="noopener noreferrer"&gt;zod.dev&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  4. Vitest — tests that don't slow you down
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What it is:&lt;/strong&gt;&lt;br&gt;
A Vite-native test runner. Jest-compatible API, but with native ESM support, instant watch mode, and built-in TypeScript support.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt;&lt;br&gt;
Jest's startup time is noticeable. Vitest starts in under a second. If you're using Vite already (and you probably are), switching costs almost nothing.&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;describe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;it&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expect&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;vitest&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;add&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="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;returns correct sum&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="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="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;Links:&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;npm install -D vitest&lt;/code&gt; — &lt;a href="https://vitest.dev" rel="noopener noreferrer"&gt;vitest.dev&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  5. Effect — error handling the way it should work
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What it is:&lt;/strong&gt;&lt;br&gt;
A TypeScript library for writing reliable, type-safe, composable programs. Errors, async, dependencies — all tracked in the type system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;try/catch&lt;/code&gt; doesn't tell you what can fail or why. Effect makes errors first-class citizens. Every function signature tells you exactly what it can return and what it can fail with.&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;Effect&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;effect&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;program&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Effect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tryPromise&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;try&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;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;/api/data&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="na"&gt;catch&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;NetworkError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Steep learning curve. Worth it for anything production-critical.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Links:&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;npm install effect&lt;/code&gt; — &lt;a href="https://effect.website" rel="noopener noreferrer"&gt;effect.website&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  6. pompelmi — antivirus scanning for Node.js that actually works
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What it is:&lt;/strong&gt;&lt;br&gt;
A minimal, zero-dependency wrapper around ClamAV. Scans any file and returns a typed &lt;code&gt;Verdict&lt;/code&gt; symbol — &lt;code&gt;Verdict.Clean&lt;/code&gt;, &lt;code&gt;Verdict.Malicious&lt;/code&gt;, or &lt;code&gt;Verdict.ScanError&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt;&lt;br&gt;
Most Node.js file upload handlers have no malware scanning at all.&lt;br&gt;
The ones that do usually rely on fragile stdout parsing with regex.&lt;br&gt;
pompelmi maps ClamAV exit codes directly — the stable interface — so it can't silently break between ClamAV versions.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;scan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Verdict&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pompelmi&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;scan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&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;result&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;Verdict&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Malicious&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;File rejected.&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;No daemon needed by default. No runtime dependencies. No regex. Cross-platform.&lt;br&gt;
If ClamAV runs in a container, pass &lt;code&gt;host&lt;/code&gt; and &lt;code&gt;port&lt;/code&gt; and the API stays identical.&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/pompelmi" rel="noopener noreferrer"&gt;
        pompelmi
      &lt;/a&gt; / &lt;a href="https://github.com/pompelmi/pompelmi" rel="noopener noreferrer"&gt;
        pompelmi
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Minimal Node.js wrapper around ClamAV — scan any file and get Clean, Malicious, or ScanError. Handles installation and database updates automatically.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;p&gt;
  &lt;a rel="noopener noreferrer" href="https://github.com/pompelmi/pompelmi/./src/grapefruit.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%2Fpompelmi%2Fpompelmi%2FHEAD%2F.%2Fsrc%2Fgrapefruit.png" width="96" alt="pompelmi logo"&gt;&lt;/a&gt;
&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;pompelmi&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;ClamAV for humans&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;
  &lt;a href="https://www.npmjs.com/package/pompelmi" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/afa9095da8c286a9d2f798ae9d02cfcf6db0ae0efc625cdf7ee757b8af5e9924/68747470733a2f2f696d672e736869656c64732e696f2f6e706d2f762f706f6d70656c6d692e737667" alt="npm version"&gt;&lt;/a&gt;
  &lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/95c61c397ca3825757ec835268e50886b2c10ddc4f0676e1222b19037610927f/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4953432d626c75652e737667"&gt;&lt;img src="https://camo.githubusercontent.com/95c61c397ca3825757ec835268e50886b2c10ddc4f0676e1222b19037610927f/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4953432d626c75652e737667" alt="license"&gt;&lt;/a&gt;
  &lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/489444e15856929c362ce966520a248149a338daec3ec32dec3f83554d46caca/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f706c6174666f726d2d6d61634f532532302537432532304c696e757825323025374325323057696e646f77732d6c69676874677265792e737667"&gt;&lt;img src="https://camo.githubusercontent.com/489444e15856929c362ce966520a248149a338daec3ec32dec3f83554d46caca/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f706c6174666f726d2d6d61634f532532302537432532304c696e757825323025374325323057696e646f77732d6c69676874677265792e737667" alt="platform"&gt;&lt;/a&gt;
  &lt;a href="https://www.npmjs.com/package/pompelmi?activeTab=dependencies" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/aae95fbaa83bc6a3f4597f3a75da45ea46ec236fc324617f0e5a2f15e07fe750/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f646570656e64656e636965732d302d627269676874677265656e" alt="zero dependencies"&gt;&lt;/a&gt;
&lt;/p&gt;




&lt;p&gt;A minimal Node.js wrapper around &lt;a href="https://www.clamav.net/" rel="nofollow noopener noreferrer"&gt;ClamAV&lt;/a&gt; that scans any file and returns a typed &lt;code&gt;Verdict&lt;/code&gt; Symbol: &lt;code&gt;Verdict.Clean&lt;/code&gt;, &lt;code&gt;Verdict.Malicious&lt;/code&gt;, or &lt;code&gt;Verdict.ScanError&lt;/code&gt;. No daemons. No cloud. No native bindings. Zero runtime dependencies.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Table of contents&lt;/h2&gt;
&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/pompelmi/pompelmi#quickstart" rel="noopener noreferrer"&gt;Quickstart&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/pompelmi/pompelmi#how-it-works" rel="noopener noreferrer"&gt;How it works&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/pompelmi/pompelmi#api" rel="noopener noreferrer"&gt;API&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/pompelmi/pompelmi#pompelmiscanfilepath-options" rel="noopener noreferrer"&gt;pompelmi.scan()&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;&lt;a href="https://github.com/pompelmi/pompelmi#docker--remote-scanning" rel="noopener noreferrer"&gt;Docker / remote scanning&lt;/a&gt;&lt;/li&gt;

&lt;li&gt;

&lt;a href="https://github.com/pompelmi/pompelmi#internal-utilities" rel="noopener noreferrer"&gt;Internal utilities&lt;/a&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/pompelmi/pompelmi#clamavinstaller" rel="noopener noreferrer"&gt;ClamAVInstaller()&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/pompelmi/pompelmi#updateclamavdatabase" rel="noopener noreferrer"&gt;updateClamAVDatabase()&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;&lt;a href="https://github.com/pompelmi/pompelmi#supported-platforms" rel="noopener noreferrer"&gt;Supported platforms&lt;/a&gt;&lt;/li&gt;

&lt;li&gt;&lt;a href="https://github.com/pompelmi/pompelmi#installing-clamav-manually" rel="noopener noreferrer"&gt;Installing ClamAV manually&lt;/a&gt;&lt;/li&gt;

&lt;li&gt;&lt;a href="https://github.com/pompelmi/pompelmi#testing" rel="noopener noreferrer"&gt;Testing&lt;/a&gt;&lt;/li&gt;

&lt;li&gt;&lt;a href="https://github.com/pompelmi/pompelmi#contributing" rel="noopener noreferrer"&gt;Contributing&lt;/a&gt;&lt;/li&gt;

&lt;li&gt;&lt;a href="https://github.com/pompelmi/pompelmi#security" rel="noopener noreferrer"&gt;Security&lt;/a&gt;&lt;/li&gt;

&lt;li&gt;&lt;a href="https://github.com/pompelmi/pompelmi#license" rel="noopener noreferrer"&gt;License&lt;/a&gt;&lt;/li&gt;

&lt;/ul&gt;
&lt;br&gt;


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

&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;npm install pompelmi&lt;/pre&gt;

&lt;/div&gt;

&lt;div class="highlight highlight-source-js notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt; scan&lt;span class="pl-kos"&gt;,&lt;/span&gt; Verdict &lt;span class="pl-kos"&gt;}&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-en"&gt;require&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'pompelmi'&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;

&lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;result&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-k"&gt;await&lt;/span&gt; &lt;span class="pl-en"&gt;scan&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'/path/to/file.zip'&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;

&lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;result&lt;/span&gt; &lt;span class="pl-c1"&gt;===&lt;/span&gt; &lt;span class="pl-v"&gt;Verdict&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;Malicious&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
  &lt;span class="pl-k"&gt;throw&lt;/span&gt; &lt;span class="pl-k"&gt;new&lt;/span&gt; &lt;span class="pl-v"&gt;Error&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'File rejected: malware detected'&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;How it works&lt;/h2&gt;

&lt;/div&gt;


&lt;ol&gt;

&lt;li&gt;

&lt;strong&gt;Validate&lt;/strong&gt; — pompelmi checks that the argument is a string and that the file exists before spawning anything.&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Scan&lt;/strong&gt; — pompelmi spawns &lt;code&gt;clamscan --no-summary &amp;lt;filePath&amp;gt;&lt;/code&gt; as a child process and reads the exit code.&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Map&lt;/strong&gt; — the exit code…&lt;/li&gt;

&lt;/ol&gt;
&lt;/div&gt;
&lt;br&gt;
  &lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/pompelmi/pompelmi" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;p&gt;&lt;strong&gt;Links:&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;npm install pompelmi&lt;/code&gt; — &lt;a href="https://pompelmi.app" rel="noopener noreferrer"&gt;pompelmi.app&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  7. ofetch — fetch that behaves like you expect
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What it is:&lt;/strong&gt;&lt;br&gt;
A better &lt;code&gt;fetch&lt;/code&gt; wrapper from the UnJS team. Auto-parses JSON, smart retries, proper error throwing, and works in Node, browsers, and edge runtimes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt;&lt;br&gt;
Native &lt;code&gt;fetch&lt;/code&gt; is fine until you need error handling, and then you realize you have to check &lt;code&gt;res.ok&lt;/code&gt; yourself every single time. ofetch throws on non-2xx responses by default, handles JSON automatically, and adds retry logic with a single option.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ofetch&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;ofetch&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;data&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;ofetch&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="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&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&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Tommy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;retry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&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;Links:&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;npm install ofetch&lt;/code&gt; — &lt;a href="https://github.com/unjs/ofetch" rel="noopener noreferrer"&gt;github.com/unjs/ofetch&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  8. unstorage — one API for every storage backend
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What it is:&lt;/strong&gt;&lt;br&gt;
A universal key-value storage layer. Same API whether you're writing to memory, Redis, Cloudflare KV, localStorage, the filesystem, or a database.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt;&lt;br&gt;
You want to swap Redis for Cloudflare KV when you move to the edge — you shouldn't have to rewrite your caching layer. unstorage makes that a config change, not a refactor.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createStorage&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;unstorage&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="nx"&gt;redisDriver&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;unstorage/drivers/redis&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;storage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createStorage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;redisDriver&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;REDIS_URL&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;session:abc&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;userId&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;session&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;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;session:abc&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;Links:&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;npm install unstorage&lt;/code&gt; — &lt;a href="https://unstorage.unjs.io" rel="noopener noreferrer"&gt;unstorage.unjs.io&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  9. tsx — run TypeScript without a build step
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What it is:&lt;/strong&gt;&lt;br&gt;
A CLI tool that runs &lt;code&gt;.ts&lt;/code&gt; files directly in Node.js. Drop-in replacement for &lt;code&gt;node&lt;/code&gt;, powered by esbuild.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;ts-node&lt;/code&gt; is slow. &lt;code&gt;tsx&lt;/code&gt; is fast. Starts instantly, handles modern ESM TypeScript, and requires zero configuration.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx tsx src/index.ts
&lt;span class="c"&gt;# or&lt;/span&gt;
tsx watch src/index.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace this in your &lt;code&gt;package.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"scripts"&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;"dev"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tsx watch src/index.ts"&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;&lt;strong&gt;Links:&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;npm install -D tsx&lt;/code&gt; — &lt;a href="https://tsx.is" rel="noopener noreferrer"&gt;tsx.is&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  10. oslo — auth primitives without the framework lock-in
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What it is:&lt;/strong&gt;&lt;br&gt;
A collection of small, focused auth utilities: CSRF tokens, OTP generation, cookie handling, encoding, timing-safe comparison. From the Lucia auth team.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt;&lt;br&gt;
Full auth libraries (NextAuth, Lucia) make assumptions about your framework and database. oslo gives you the low-level primitives — the pieces auth is built from — so you can compose exactly what you need.&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;generateRandomString&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;alphabet&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;oslo/crypto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;TOTP&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;oslo/otp&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;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;generateRandomString&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;alphabet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0-9&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;totp&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;TOTP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SHA-1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isValid&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;totp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userInput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secret&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;Links:&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;npm install oslo&lt;/code&gt; — &lt;a href="https://oslo.js.org" rel="noopener noreferrer"&gt;oslo.js.org&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The list at a glance
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Package&lt;/th&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;One-liner&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hono&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Framework&lt;/td&gt;
&lt;td&gt;Edge-native, framework-agnostic web server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;drizzle-orm&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;Type-safe ORM, no codegen&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;zod&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Validation&lt;/td&gt;
&lt;td&gt;Schema → TypeScript type + runtime validation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;vitest&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Testing&lt;/td&gt;
&lt;td&gt;Fast Jest-compatible test runner&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;effect&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Reliability&lt;/td&gt;
&lt;td&gt;Type-safe errors and async composition&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pompelmi&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Security&lt;/td&gt;
&lt;td&gt;File antivirus scanning via ClamAV&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ofetch&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;HTTP&lt;/td&gt;
&lt;td&gt;fetch that handles errors and JSON automatically&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;unstorage&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Storage&lt;/td&gt;
&lt;td&gt;Universal key-value storage across backends&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tsx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;DX&lt;/td&gt;
&lt;td&gt;Run TypeScript files instantly, no config&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;oslo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Auth&lt;/td&gt;
&lt;td&gt;Low-level auth primitives, no framework assumptions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;p&gt;Which of these are already in your stack — and which ones are you trying next?&lt;/p&gt;

&lt;p&gt;I'm especially curious if anyone has a better alternative to &lt;code&gt;effect&lt;/code&gt; for typed error handling without the learning curve.&lt;/p&gt;

</description>
      <category>node</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I tried every Node.js antivirus library. Here's what I found.</title>
      <dc:creator>Tommaso Bertocchi</dc:creator>
      <pubDate>Wed, 15 Apr 2026 17:58:49 +0000</pubDate>
      <link>https://forem.com/sonotommy/i-tried-every-nodejs-antivirus-library-heres-what-i-found-122j</link>
      <guid>https://forem.com/sonotommy/i-tried-every-nodejs-antivirus-library-heres-what-i-found-122j</guid>
      <description>&lt;p&gt;Most Node.js file upload handlers look something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/upload&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;upload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;single&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;file&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="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="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;No scan. No check. Just trust.&lt;/p&gt;

&lt;p&gt;That's fine until someone uploads a malware-laced PDF to your SaaS and your enterprise customer's IT team starts asking questions.&lt;/p&gt;

&lt;p&gt;I went looking for a proper antivirus solution for Node.js. Here's everything I found — and why most of it is worse than it looks.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Most Node.js antivirus libraries are either abandoned, overly complex, or rely on fragile stdout parsing. &lt;a href="https://pompelmi.app" rel="noopener noreferrer"&gt;pompelmi&lt;/a&gt; is the one that actually does it right — zero dependencies, typed verdicts, and direct ClamAV exit code mapping with no regex.&lt;/p&gt;
&lt;/blockquote&gt;


&lt;h2&gt;
  
  
  What I was actually looking for
&lt;/h2&gt;

&lt;p&gt;Before I started, I wrote down my requirements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Works in a real production Node.js app (not a script)&lt;/li&gt;
&lt;li&gt;Doesn't require me to run a background daemon 24/7 for single-scan use cases&lt;/li&gt;
&lt;li&gt;Returns typed results I can actually &lt;code&gt;switch&lt;/code&gt; on&lt;/li&gt;
&lt;li&gt;Actively maintained&lt;/li&gt;
&lt;li&gt;Zero or minimal runtime dependencies&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Spoiler: most libraries fail at least two of these.&lt;/p&gt;


&lt;h2&gt;
  
  
  The landscape (as of 2025)
&lt;/h2&gt;
&lt;h3&gt;
  
  
  1. &lt;code&gt;clamscan&lt;/code&gt; (npm)
&lt;/h3&gt;

&lt;p&gt;The oldest and most-starred option. Wraps ClamAV and has been around since 2014.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it does:&lt;/strong&gt; Spawns &lt;code&gt;clamscan&lt;/code&gt; or connects to &lt;code&gt;clamd&lt;/code&gt; daemon. Returns a result object.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The problem:&lt;/strong&gt; It parses stdout with regex patterns. That's a brittle contract — ClamAV output format changes between versions, and your regex silently breaks. I've been burned by this in production. The library also has a handful of long-standing open issues around false negatives on certain output formats.&lt;/p&gt;

&lt;p&gt;It works. But you're trusting regex to stand between you and malware.&lt;/p&gt;


&lt;h3&gt;
  
  
  2. &lt;code&gt;clamdjs&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Connects to a running &lt;code&gt;clamd&lt;/code&gt; daemon via TCP.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The problem:&lt;/strong&gt; You have to run a daemon. For many setups — serverless functions, ephemeral containers, low-traffic apps — spinning up a persistent background service just to occasionally scan a file is overkill. Also, &lt;code&gt;clamdjs&lt;/code&gt; hasn't seen meaningful updates in years.&lt;/p&gt;


&lt;h3&gt;
  
  
  3. Random VirusTotal API wrappers
&lt;/h3&gt;

&lt;p&gt;These exist. They send your files to VirusTotal's API and return scan results.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The problem:&lt;/strong&gt; You're uploading potentially sensitive user files to a third-party service. For anything with PII, healthcare data, or enterprise contracts, that's a non-starter. Also rate limits, latency, and cost.&lt;/p&gt;


&lt;h3&gt;
  
  
  4. Cloud-native options (AWS Malware Protection, GCP Security Command Center)
&lt;/h3&gt;

&lt;p&gt;If you're already deep in AWS or GCP, these exist and work well.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The problem:&lt;/strong&gt; Vendor lock-in, cost, and setup complexity. Sometimes you just want to &lt;code&gt;await scan(file)&lt;/code&gt; and move on.&lt;/p&gt;


&lt;h2&gt;
  
  
  pompelmi: the one I actually kept
&lt;/h2&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/pompelmi" rel="noopener noreferrer"&gt;
        pompelmi
      &lt;/a&gt; / &lt;a href="https://github.com/pompelmi/pompelmi" rel="noopener noreferrer"&gt;
        pompelmi
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Minimal Node.js wrapper around ClamAV — scan any file and get Clean, Malicious, or ScanError. Handles installation and database updates automatically.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;p&gt;
  &lt;a rel="noopener noreferrer" href="https://github.com/pompelmi/pompelmi/./src/grapefruit.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%2Fpompelmi%2Fpompelmi%2FHEAD%2F.%2Fsrc%2Fgrapefruit.png" width="96" alt="pompelmi logo"&gt;&lt;/a&gt;
&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;pompelmi&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;ClamAV for humans&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;
  &lt;a href="https://www.npmjs.com/package/pompelmi" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/afa9095da8c286a9d2f798ae9d02cfcf6db0ae0efc625cdf7ee757b8af5e9924/68747470733a2f2f696d672e736869656c64732e696f2f6e706d2f762f706f6d70656c6d692e737667" alt="npm version"&gt;&lt;/a&gt;
  &lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/95c61c397ca3825757ec835268e50886b2c10ddc4f0676e1222b19037610927f/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4953432d626c75652e737667"&gt;&lt;img src="https://camo.githubusercontent.com/95c61c397ca3825757ec835268e50886b2c10ddc4f0676e1222b19037610927f/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4953432d626c75652e737667" alt="license"&gt;&lt;/a&gt;
  &lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/489444e15856929c362ce966520a248149a338daec3ec32dec3f83554d46caca/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f706c6174666f726d2d6d61634f532532302537432532304c696e757825323025374325323057696e646f77732d6c69676874677265792e737667"&gt;&lt;img src="https://camo.githubusercontent.com/489444e15856929c362ce966520a248149a338daec3ec32dec3f83554d46caca/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f706c6174666f726d2d6d61634f532532302537432532304c696e757825323025374325323057696e646f77732d6c69676874677265792e737667" alt="platform"&gt;&lt;/a&gt;
  &lt;a href="https://www.npmjs.com/package/pompelmi?activeTab=dependencies" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/aae95fbaa83bc6a3f4597f3a75da45ea46ec236fc324617f0e5a2f15e07fe750/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f646570656e64656e636965732d302d627269676874677265656e" alt="zero dependencies"&gt;&lt;/a&gt;
&lt;/p&gt;




&lt;p&gt;A minimal Node.js wrapper around &lt;a href="https://www.clamav.net/" rel="nofollow noopener noreferrer"&gt;ClamAV&lt;/a&gt; that scans any file and returns a typed &lt;code&gt;Verdict&lt;/code&gt; Symbol: &lt;code&gt;Verdict.Clean&lt;/code&gt;, &lt;code&gt;Verdict.Malicious&lt;/code&gt;, or &lt;code&gt;Verdict.ScanError&lt;/code&gt;. No daemons. No cloud. No native bindings. Zero runtime dependencies.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Table of contents&lt;/h2&gt;
&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/pompelmi/pompelmi#quickstart" rel="noopener noreferrer"&gt;Quickstart&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/pompelmi/pompelmi#how-it-works" rel="noopener noreferrer"&gt;How it works&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/pompelmi/pompelmi#api" rel="noopener noreferrer"&gt;API&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/pompelmi/pompelmi#pompelmiscanfilepath-options" rel="noopener noreferrer"&gt;pompelmi.scan()&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;&lt;a href="https://github.com/pompelmi/pompelmi#docker--remote-scanning" rel="noopener noreferrer"&gt;Docker / remote scanning&lt;/a&gt;&lt;/li&gt;

&lt;li&gt;

&lt;a href="https://github.com/pompelmi/pompelmi#internal-utilities" rel="noopener noreferrer"&gt;Internal utilities&lt;/a&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/pompelmi/pompelmi#clamavinstaller" rel="noopener noreferrer"&gt;ClamAVInstaller()&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/pompelmi/pompelmi#updateclamavdatabase" rel="noopener noreferrer"&gt;updateClamAVDatabase()&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;&lt;a href="https://github.com/pompelmi/pompelmi#supported-platforms" rel="noopener noreferrer"&gt;Supported platforms&lt;/a&gt;&lt;/li&gt;

&lt;li&gt;&lt;a href="https://github.com/pompelmi/pompelmi#installing-clamav-manually" rel="noopener noreferrer"&gt;Installing ClamAV manually&lt;/a&gt;&lt;/li&gt;

&lt;li&gt;&lt;a href="https://github.com/pompelmi/pompelmi#testing" rel="noopener noreferrer"&gt;Testing&lt;/a&gt;&lt;/li&gt;

&lt;li&gt;&lt;a href="https://github.com/pompelmi/pompelmi#contributing" rel="noopener noreferrer"&gt;Contributing&lt;/a&gt;&lt;/li&gt;

&lt;li&gt;&lt;a href="https://github.com/pompelmi/pompelmi#security" rel="noopener noreferrer"&gt;Security&lt;/a&gt;&lt;/li&gt;

&lt;li&gt;&lt;a href="https://github.com/pompelmi/pompelmi#license" rel="noopener noreferrer"&gt;License&lt;/a&gt;&lt;/li&gt;

&lt;/ul&gt;
&lt;br&gt;


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

&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;npm install pompelmi&lt;/pre&gt;

&lt;/div&gt;

&lt;div class="highlight highlight-source-js notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt; scan&lt;span class="pl-kos"&gt;,&lt;/span&gt; Verdict &lt;span class="pl-kos"&gt;}&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-en"&gt;require&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'pompelmi'&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;

&lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;result&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-k"&gt;await&lt;/span&gt; &lt;span class="pl-en"&gt;scan&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'/path/to/file.zip'&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;

&lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;result&lt;/span&gt; &lt;span class="pl-c1"&gt;===&lt;/span&gt; &lt;span class="pl-v"&gt;Verdict&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;Malicious&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
  &lt;span class="pl-k"&gt;throw&lt;/span&gt; &lt;span class="pl-k"&gt;new&lt;/span&gt; &lt;span class="pl-v"&gt;Error&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'File rejected: malware detected'&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;How it works&lt;/h2&gt;

&lt;/div&gt;


&lt;ol&gt;

&lt;li&gt;

&lt;strong&gt;Validate&lt;/strong&gt; — pompelmi checks that the argument is a string and that the file exists before spawning anything.&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Scan&lt;/strong&gt; — pompelmi spawns &lt;code&gt;clamscan --no-summary &amp;lt;filePath&amp;gt;&lt;/code&gt; as a child process and reads the exit code.&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Map&lt;/strong&gt; — the exit code…&lt;/li&gt;

&lt;/ol&gt;
&lt;/div&gt;
&lt;br&gt;
  &lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/pompelmi/pompelmi" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;p&gt;pompelmi is a Node.js antivirus library built around one idea: &lt;strong&gt;don't parse stdout, map exit codes directly&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;ClamAV already communicates via exit codes. &lt;code&gt;0&lt;/code&gt; = clean. &lt;code&gt;1&lt;/code&gt; = malware found. &lt;code&gt;2&lt;/code&gt; = error. pompelmi maps these directly to typed verdict symbols. No regex. No stdout parsing. No silent failures.&lt;/p&gt;

&lt;h3&gt;
  
  
  Install
&lt;/h3&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; @pompelmi/probe
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ClamAV needs to be present on the host (one-time setup):&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;# macOS&lt;/span&gt;
brew &lt;span class="nb"&gt;install &lt;/span&gt;clamav &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; freshclam

&lt;span class="c"&gt;# Ubuntu/Debian&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; clamav &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;freshclam

&lt;span class="c"&gt;# Windows&lt;/span&gt;
choco &lt;span class="nb"&gt;install &lt;/span&gt;clamav &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;pompelmi automatically detects whether your virus definitions are up-to-date and skips &lt;code&gt;freshclam&lt;/code&gt; if they already are. No redundant downloads.&lt;/p&gt;

&lt;h3&gt;
  
  
  Usage
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;scan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Verdict&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;@pompelmi/probe&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;scan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/path/to/uploaded-file.pdf&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nx"&gt;Verdict&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Clean&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;// Safe to process&lt;/span&gt;
    &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nx"&gt;Verdict&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Malicious&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;// Reject and alert&lt;/span&gt;
    &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nx"&gt;Verdict&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ScanError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;// Handle gracefully&lt;/span&gt;
    &lt;span class="k"&gt;break&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;That's it. Typed. Exhaustive. No strings to compare, no regex, no &lt;code&gt;result.isInfected&lt;/code&gt; booleans with unclear semantics.&lt;/p&gt;

&lt;h3&gt;
  
  
  What makes it different
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;No stdout parsing.&lt;/strong&gt;&lt;br&gt;
Every other library I tested parses ClamAV's human-readable output. pompelmi uses exit codes exclusively — the stable, documented interface ClamAV actually exposes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No daemon required.&lt;/strong&gt;&lt;br&gt;
pompelmi calls &lt;code&gt;clamscan&lt;/code&gt; directly via Node's built-in &lt;code&gt;child_process&lt;/code&gt;. No background process to manage. Works in Lambda, in Docker, in a plain old Express app.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Zero runtime dependencies.&lt;/strong&gt;&lt;br&gt;
The entire library relies on Node.js built-ins. Nothing to audit, nothing to patch, nothing that breaks on a minor version bump.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cross-platform.&lt;/strong&gt;&lt;br&gt;
macOS, Linux, and Windows all work with the same code. The ClamAV install step varies per OS; the Node.js API doesn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Typed verdicts.&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;Verdict.Clean&lt;/code&gt;, &lt;code&gt;Verdict.Malicious&lt;/code&gt;, &lt;code&gt;Verdict.ScanError&lt;/code&gt; — symbols, not strings. TypeScript narrows correctly. You can't typo &lt;code&gt;"malicous"&lt;/code&gt; at 2am and ship a broken check.&lt;/p&gt;




&lt;h2&gt;
  
  
  A realistic Express upload handler
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&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;express&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="nx"&gt;multer&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;multer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;scan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Verdict&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;@pompelmi/probe&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="nx"&gt;path&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;path&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="nx"&gt;fs&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;fs/promises&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;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&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;upload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;multer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;dest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/tmp/uploads&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/upload&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;upload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;single&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;file&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;filePath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;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;scan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filePath&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;result&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;Verdict&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Malicious&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;fs&lt;/span&gt;&lt;span class="p"&gt;.&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;filePath&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;File rejected: malware detected.&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;Verdict&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ScanError&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;fs&lt;/span&gt;&lt;span class="p"&gt;.&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;filePath&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Scan failed. Please retry.&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="c1"&gt;// Verdict.Clean — proceed with processing&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/var/uploads&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;originalname&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;fs&lt;/span&gt;&lt;span class="p"&gt;.&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;filePath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dest&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;dest&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unlink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filePath&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="p"&gt;{});&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Unexpected error.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clean, safe, and explicit about every possible outcome.&lt;/p&gt;




&lt;h2&gt;
  
  
  The verdict (pun intended)
&lt;/h2&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;Maintained&lt;/th&gt;
&lt;th&gt;No daemon needed&lt;/th&gt;
&lt;th&gt;Typed results&lt;/th&gt;
&lt;th&gt;No stdout parsing&lt;/th&gt;
&lt;th&gt;Zero deps&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;clamscan&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Partially&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;clamdjs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VirusTotal wrappers&lt;/td&gt;
&lt;td&gt;Varies&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Varies&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;pompelmi&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you're building a Node.js app that handles file uploads in 2025, you should be scanning them.&lt;/p&gt;

&lt;p&gt;pompelmi makes it as easy as it should be.&lt;/p&gt;




&lt;p&gt;Are you scanning user uploads in your app? What's your current setup — and what's the biggest friction point you've hit?&lt;/p&gt;

&lt;p&gt;Drop it in the comments. I'm curious whether anyone has a pattern I haven't seen.&lt;/p&gt;

</description>
      <category>node</category>
      <category>security</category>
      <category>javascript</category>
      <category>opensource</category>
    </item>
    <item>
      <title>it will be for all 2026</title>
      <dc:creator>Tommaso Bertocchi</dc:creator>
      <pubDate>Sun, 12 Apr 2026 17:35:58 +0000</pubDate>
      <link>https://forem.com/sonotommy/it-will-be-for-all-2026-4bm5</link>
      <guid>https://forem.com/sonotommy/it-will-be-for-all-2026-4bm5</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/sonotommy/10-open-source-projects-youll-actually-use-in-2026-2ig9" class="crayons-story__hidden-navigation-link"&gt;10 Open-Source Projects You’ll Actually Use in 2026&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/sonotommy" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3399256%2F111d6919-72dc-4992-a6c6-2b20a4ccf85b.jpeg" alt="sonotommy profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/sonotommy" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Tommaso Bertocchi
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Tommaso Bertocchi
                
              
              &lt;div id="story-author-preview-content-3350792" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/sonotommy" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3399256%2F111d6919-72dc-4992-a6c6-2b20a4ccf85b.jpeg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Tommaso Bertocchi&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/sonotommy/10-open-source-projects-youll-actually-use-in-2026-2ig9" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Mar 14&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/sonotommy/10-open-source-projects-youll-actually-use-in-2026-2ig9" id="article-link-3350792"&gt;
          10 Open-Source Projects You’ll Actually Use in 2026
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ai"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ai&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/webdev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;webdev&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/programming"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;programming&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/opensource"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;opensource&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/sonotommy/10-open-source-projects-youll-actually-use-in-2026-2ig9" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/exploding-head-daceb38d627e6ae9b730f36a1e390fca556a4289d5a41abb2c35068ad3e2c4b5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;20&lt;span class="hidden s:inline"&gt; reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/sonotommy/10-open-source-projects-youll-actually-use-in-2026-2ig9#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              Comments


              1&lt;span class="hidden s:inline"&gt; comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            7 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
    </item>
    <item>
      <title>read this</title>
      <dc:creator>Tommaso Bertocchi</dc:creator>
      <pubDate>Sun, 12 Apr 2026 17:27:09 +0000</pubDate>
      <link>https://forem.com/sonotommy/read-this-2oah</link>
      <guid>https://forem.com/sonotommy/read-this-2oah</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/sonotommy/i-spent-3-hours-adding-antivirus-to-my-express-app-then-i-reduced-it-to-3-lines-2dm7" class="crayons-story__hidden-navigation-link"&gt;I Spent 3 Hours Adding Antivirus to My Express App. Then I Reduced It to 3 Lines.&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/sonotommy" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3399256%2F111d6919-72dc-4992-a6c6-2b20a4ccf85b.jpeg" alt="sonotommy profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/sonotommy" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Tommaso Bertocchi
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Tommaso Bertocchi
                
              
              &lt;div id="story-author-preview-content-3489863" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/sonotommy" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3399256%2F111d6919-72dc-4992-a6c6-2b20a4ccf85b.jpeg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Tommaso Bertocchi&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/sonotommy/i-spent-3-hours-adding-antivirus-to-my-express-app-then-i-reduced-it-to-3-lines-2dm7" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Apr 12&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/sonotommy/i-spent-3-hours-adding-antivirus-to-my-express-app-then-i-reduced-it-to-3-lines-2dm7" id="article-link-3489863"&gt;
          I Spent 3 Hours Adding Antivirus to My Express App. Then I Reduced It to 3 Lines.
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/javascript"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;javascript&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/node"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;node&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/security"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;security&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/beginners"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;beginners&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/sonotommy/i-spent-3-hours-adding-antivirus-to-my-express-app-then-i-reduced-it-to-3-lines-2dm7" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/exploding-head-daceb38d627e6ae9b730f36a1e390fca556a4289d5a41abb2c35068ad3e2c4b5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;20&lt;span class="hidden s:inline"&gt; reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/sonotommy/i-spent-3-hours-adding-antivirus-to-my-express-app-then-i-reduced-it-to-3-lines-2dm7#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              Comments


              2&lt;span class="hidden s:inline"&gt; comments&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            3 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
    </item>
    <item>
      <title>I Spent 3 Hours Adding Antivirus to My Express App. Then I Reduced It to 3 Lines.</title>
      <dc:creator>Tommaso Bertocchi</dc:creator>
      <pubDate>Sun, 12 Apr 2026 09:51:23 +0000</pubDate>
      <link>https://forem.com/sonotommy/i-spent-3-hours-adding-antivirus-to-my-express-app-then-i-reduced-it-to-3-lines-2dm7</link>
      <guid>https://forem.com/sonotommy/i-spent-3-hours-adding-antivirus-to-my-express-app-then-i-reduced-it-to-3-lines-2dm7</guid>
      <description>&lt;p&gt;Two years ago, I shipped my first production app with file uploads. A week later, my mentor asked: "Are you scanning those files for malware?"&lt;/p&gt;

&lt;p&gt;I wasn't.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Google Rabbit Hole
&lt;/h2&gt;

&lt;p&gt;Like any developer, I Googled "node js antivirus file upload." Here's what I found:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ClamAV&lt;/strong&gt; — the open-source standard. Great! Let's use it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;clamscan npm package&lt;/strong&gt; — 47 configuration options.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;clamav.js&lt;/strong&gt; — requires running &lt;code&gt;clamd&lt;/code&gt; daemon.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Various tutorials&lt;/strong&gt; — "First, configure your socket connection..."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I just wanted to scan a file. Why did I need to understand Unix sockets?&lt;/p&gt;

&lt;h2&gt;
  
  
  My First Attempt
&lt;/h2&gt;

&lt;p&gt;After an hour of reading docs, I had something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;NodeClam&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;clamscan&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;clamscan&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;NodeClam&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;removeInfected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;quarantineInfected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;scanLog&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;debugMode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;fileList&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;scanRecursively&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;clamscan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/usr/bin/clamscan&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;scanArchives&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;active&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;clamdscan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/var/run/clamav/clamd.ctl&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;localFallback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/usr/bin/clamdscan&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;configFile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;multiscan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;reloadDb&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;active&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;bypassTest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;preference&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;clamdscan&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// ... 30 more lines to actually scan a file&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It didn't work. The socket path was wrong on my Mac. I spent another hour debugging.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Daemon Problem
&lt;/h2&gt;

&lt;p&gt;Most ClamAV wrappers assume you're running &lt;code&gt;clamd&lt;/code&gt; — a background daemon that keeps virus definitions in memory for faster scanning.&lt;/p&gt;

&lt;p&gt;This makes sense for high-throughput enterprise systems. But I was building a side project. I didn't want to manage a daemon. I didn't want to configure systemd. I didn't want to think about socket permissions.&lt;/p&gt;

&lt;p&gt;I just wanted to answer one question: &lt;strong&gt;is this file safe?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Actually Needed
&lt;/h2&gt;

&lt;p&gt;After three hours of frustration, I wrote down what I actually wanted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;scan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Verdict&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pompelmi&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;scan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/path/to/upload.zip&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// Verdict.Clean | Verdict.Malicious | Verdict.ScanError — that's it&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No daemon. No configuration object. No socket paths. Just a function that takes a file and returns an answer.&lt;/p&gt;

&lt;p&gt;So I built it.&lt;/p&gt;

&lt;h2&gt;
  
  
  pompelmi: ClamAV for Humans
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;scan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Verdict&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pompelmi&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="nx"&gt;pompelmi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/path/to/file.zip&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;Verdict&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Malicious&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;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;File rejected: malware detected&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;That's the entire API. One function. Three possible results — typed as Symbols, so no typos, no accidental string mismatches.&lt;/p&gt;

&lt;p&gt;Here's the same Express middleware, before and after:&lt;/p&gt;

&lt;h3&gt;
  
  
  Before (47 lines)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;NodeClam&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;clamscan&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;multer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;multer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// ... 35 lines of configuration ...&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/upload&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;upload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;single&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;file&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;clam&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;clamscan&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isInfected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;viruses&lt;/span&gt;&lt;span class="p"&gt;}&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;clam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isInfected&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&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;isInfected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unlinkSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Malware detected&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;viruses&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Is this a scan error or a config error? Who knows!&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Scan failed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  After (12 lines)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;scan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Verdict&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pompelmi&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;multer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;multer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/upload&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;upload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;single&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;file&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;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;scan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&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;result&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;Verdict&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Malicious&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unlinkSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Malware detected&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;Verdict&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ScanError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Scan couldn't complete — treat as untrusted&lt;/span&gt;
    &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unlinkSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;File rejected&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;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="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How It Works (The Boring Way)
&lt;/h2&gt;

&lt;p&gt;pompelmi doesn't do anything clever:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check that the file exists&lt;/li&gt;
&lt;li&gt;Spawn &lt;code&gt;clamscan --no-summary &amp;lt;path&amp;gt;&lt;/code&gt; using Node's native &lt;code&gt;child_process&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Read the exit code&lt;/li&gt;
&lt;li&gt;Map it to a Symbol&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No stdout parsing. No regex. No daemon. No socket. No third-party spawn library. Just a child process and an exit code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Exit 0 → Verdict.Clean
Exit 1 → Verdict.Malicious
Exit 2 → Verdict.ScanError
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ClamAV has been doing this reliably for 20 years. I just wrapped it in a function.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Symbols, Not Strings?
&lt;/h2&gt;

&lt;p&gt;Since v1.2.0, scan results are &lt;code&gt;Symbol&lt;/code&gt;s, not plain strings. This means:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ✅ This works&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;result&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;Verdict&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Malicious&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;// ❌ This will never match — intentionally&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;result&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Malicious&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It prevents a whole class of bugs where you typo a string comparison (&lt;code&gt;'malicious'&lt;/code&gt; vs &lt;code&gt;'Malicious'&lt;/code&gt;) and your security check silently does nothing. The compiler catches it for you.&lt;/p&gt;

&lt;p&gt;If you need the verdict as a string for logging, each Symbol has a &lt;code&gt;.description&lt;/code&gt; property:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;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;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// "Clean", "Malicious", or "ScanError"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Upgrading from v1.1.x?&lt;/strong&gt; Replace any &lt;code&gt;result === 'Clean'&lt;/code&gt; checks with &lt;code&gt;result === Verdict.Clean&lt;/code&gt; (and same for &lt;code&gt;Malicious&lt;/code&gt; and &lt;code&gt;ScanError&lt;/code&gt;). Don't forget to import &lt;code&gt;Verdict&lt;/code&gt; alongside &lt;code&gt;scan&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  "But What About Performance?"
&lt;/h2&gt;

&lt;p&gt;Yes, spawning &lt;code&gt;clamscan&lt;/code&gt; for every file is slower than using the daemon. Each scan loads the virus database from disk (~300MB).&lt;/p&gt;

&lt;p&gt;For a side project handling 100 uploads/day? Doesn't matter.&lt;/p&gt;

&lt;p&gt;For a startup processing 10,000 files/hour? You probably have a dedicated security engineer who knows how to configure &lt;code&gt;clamd&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;pompelmi is for the 99% of developers who just need &lt;em&gt;something&lt;/em&gt; — and "slow but working" beats "fast but misconfigured."&lt;/p&gt;

&lt;h2&gt;
  
  
  The Point
&lt;/h2&gt;

&lt;p&gt;Developer tools should meet you where you are.&lt;/p&gt;

&lt;p&gt;When I was a junior developer, I didn't need 47 configuration options. I needed one function that worked. I needed to ship something on Friday and not think about Unix sockets.&lt;/p&gt;

&lt;p&gt;If you're building something bigger, you'll outgrow pompelmi. That's fine. By then, you'll understand &lt;em&gt;why&lt;/em&gt; you need the daemon, and configuring it won't feel like dark magic.&lt;/p&gt;

&lt;p&gt;But if you're building your first app with file uploads, and you just want to scan for malware without a 3-hour detour — &lt;code&gt;npm install pompelmi&lt;/code&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;pompelmi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/pompelmi/pompelmi" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://www.npmjs.com/package/pompelmi" rel="noopener noreferrer"&gt;npm&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;pompelmi is Italian for "grapefruits." I named it that because I was eating one when I finally got the code working at 2am. Sometimes that's all the reason you need.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>node</category>
      <category>security</category>
      <category>beginners</category>
    </item>
    <item>
      <title>10 Tools That Will Instantly Make Your Coding Projects Better</title>
      <dc:creator>Tommaso Bertocchi</dc:creator>
      <pubDate>Fri, 03 Apr 2026 09:03:50 +0000</pubDate>
      <link>https://forem.com/sonotommy/10-tools-that-will-instantly-make-your-coding-projects-better-1eef</link>
      <guid>https://forem.com/sonotommy/10-tools-that-will-instantly-make-your-coding-projects-better-1eef</guid>
      <description>&lt;p&gt;Most coding projects do not fail because the idea was bad.&lt;/p&gt;

&lt;p&gt;They fail because everything around the code is weak.&lt;/p&gt;

&lt;p&gt;The deploy process is manual.&lt;/p&gt;

&lt;p&gt;The API contract lives in someone's head.&lt;/p&gt;

&lt;p&gt;Errors show up first in production.&lt;/p&gt;

&lt;p&gt;Dependencies age quietly until they become a problem.&lt;/p&gt;

&lt;p&gt;Uploads get trusted way too easily.&lt;/p&gt;

&lt;p&gt;Nobody has a clean picture of how the system actually works.&lt;/p&gt;

&lt;p&gt;That is the part a lot of teams underestimate.&lt;/p&gt;

&lt;p&gt;Good projects are not just built with good code.&lt;/p&gt;

&lt;p&gt;They are built with good systems around the code.&lt;/p&gt;

&lt;p&gt;So this is not a random list of apps.&lt;/p&gt;

&lt;p&gt;These are 10 tools that make coding projects feel more professional, more reliable, more secure, and much easier to ship.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;TL;DR: Better projects usually come from stronger workflow, testing, observability, documentation, dependency hygiene, and security, not just smarter features.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fytknrcugcpceeb9l9wbj.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fytknrcugcpceeb9l9wbj.gif" alt="South Park developers staring at computer screens" width="384" height="270"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The moment you realize the feature works, but the project still feels one bad deploy away from chaos. Source: &lt;a href="https://giphy.com/gifs/southparkgifs-l3vRd3vZPrApPqzjq" rel="noopener noreferrer"&gt;South Park on Giphy&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;GitHub Actions&lt;/li&gt;
&lt;li&gt;Postman&lt;/li&gt;
&lt;li&gt;Docker&lt;/li&gt;
&lt;li&gt;Sentry&lt;/li&gt;
&lt;li&gt;Playwright&lt;/li&gt;
&lt;li&gt;Swagger / OpenAPI&lt;/li&gt;
&lt;li&gt;pompelmi&lt;/li&gt;
&lt;li&gt;Dependabot&lt;/li&gt;
&lt;li&gt;Excalidraw&lt;/li&gt;
&lt;li&gt;Raycast&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  1) GitHub Actions — Stop shipping manually
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What it is:&lt;/strong&gt; GitHub's built-in automation layer for CI, CD, releases, scheduled jobs, and repo workflows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it improves your project:&lt;/strong&gt; It turns quality checks from a vague team habit into a repeatable system. Linting, tests, builds, previews, and deploy gates stop depending on who remembered what.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; pull request checks, automated releases, scheduled maintenance jobs, deployment pipelines, repo housekeeping.&lt;/p&gt;

&lt;p&gt;The fastest way to make a repo feel serious is to make every change prove itself before it lands. Even a small workflow that runs install, lint, test, and build on every PR removes a huge amount of avoidable drama.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Links:&lt;/strong&gt; &lt;a href="https://docs.github.com/en/actions" rel="noopener noreferrer"&gt;Official&lt;/a&gt; · &lt;a href="https://github.com/actions/starter-workflows" rel="noopener noreferrer"&gt;Representative repo&lt;/a&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/actions" rel="noopener noreferrer"&gt;
        actions
      &lt;/a&gt; / &lt;a href="https://github.com/actions/starter-workflows" rel="noopener noreferrer"&gt;
        starter-workflows
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Accelerating new GitHub Actions workflows 
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;p&gt;
  &lt;a rel="noopener noreferrer nofollow" href="https://avatars0.githubusercontent.com/u/44036562?s=100&amp;amp;v=4"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Favatars0.githubusercontent.com%2Fu%2F44036562%3Fs%3D100%26v%3D4"&gt;&lt;/a&gt;
&lt;/p&gt;

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

&lt;p&gt;These are the workflow files for helping people get started with GitHub Actions.  They're presented whenever you start to create a new GitHub Actions workflow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you want to get started with GitHub Actions, you can use these starter workflows by clicking the "Actions" tab in the repository where you want to create a workflow.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/5510c8300d37413d04ca9102a8f1038208a3818fd8d94b26339256a0855d6924/68747470733a2f2f64337676366c703535716a6171632e636c6f756466726f6e742e6e65742f6974656d732f3335334133703359327833633274324e306330312f496d616765253230323031392d30382d32372532306174253230332e32352e3037253230504d2e706e67"&gt;&lt;img src="https://camo.githubusercontent.com/5510c8300d37413d04ca9102a8f1038208a3818fd8d94b26339256a0855d6924/68747470733a2f2f64337676366c703535716a6171632e636c6f756466726f6e742e6e65742f6974656d732f3335334133703359327833633274324e306330312f496d616765253230323031392d30382d32372532306174253230332e32352e3037253230504d2e706e67"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Note&lt;/h3&gt;
&lt;/div&gt;

&lt;p&gt;Thank you for your interest in this GitHub repo, however, right now we are not taking contributions.&lt;/p&gt;

&lt;p&gt;We continue to focus our resources on strategic areas that help our customers be successful while making developers' lives easier. While GitHub Actions remains a key part of this vision, we are allocating resources towards other areas of Actions and are not taking contributions to this repository at this time. The GitHub public roadmap is the best place to follow along for any updates on features we’re working on and what stage they’re in.&lt;/p&gt;

&lt;p&gt;We…&lt;/p&gt;
&lt;/div&gt;


&lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/actions/starter-workflows" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;br&gt;&lt;br&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;ci&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;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&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;verify&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;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&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;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;22&lt;/span&gt;
          &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm&lt;/span&gt;
      &lt;span class="pi"&gt;-&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;npm ci&lt;/span&gt;
      &lt;span class="pi"&gt;-&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;npm run lint&lt;/span&gt;
      &lt;span class="pi"&gt;-&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;npm test&lt;/span&gt;
      &lt;span class="pi"&gt;-&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;npm run build&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyxknr34f6np28h96a1kx.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyxknr34f6np28h96a1kx.gif" alt="Family Guy staring at a computer" width="480" height="270"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;When the team discovers that "our deploy checklist" was actually just one person's memory. Source: &lt;a href="https://giphy.com/gifs/fox-computer-family-guy-5nvQ7fBWhPVXXOcfRI" rel="noopener noreferrer"&gt;Family Guy on Giphy&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  2) Postman — Make your API usable by more than its author
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What it is:&lt;/strong&gt; An API platform for sending requests, organizing collections, sharing environments, and testing real responses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it improves your project:&lt;/strong&gt; Good APIs are not just endpoints that return JSON. They are endpoints people can inspect, replay, debug, and hand to teammates without a 20-minute Slack explanation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; backend teams, QA workflows, auth-heavy APIs, onboarding, partner integrations, manual verification.&lt;/p&gt;

&lt;p&gt;Postman is useful because it turns one person's debugging setup into shared project knowledge. The moment auth headers, refresh flows, sample payloads, and odd edge-case requests are saved in a collection, your API stops feeling mysterious. And once those collections become something your team can rerun and share consistently, the project gets easier to trust.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Links:&lt;/strong&gt; &lt;a href="https://www.postman.com/" rel="noopener noreferrer"&gt;Official&lt;/a&gt; · &lt;a href="https://github.com/postmanlabs/newman" rel="noopener noreferrer"&gt;Representative repo&lt;/a&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/postmanlabs" rel="noopener noreferrer"&gt;
        postmanlabs
      &lt;/a&gt; / &lt;a href="https://github.com/postmanlabs/newman" rel="noopener noreferrer"&gt;
        newman
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Newman is a command-line collection runner for Postman
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;p&gt;&lt;a href="https://www.postman.com/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/f790fa9f1bf3a99575b0fcc7f0c7a632c2454ca04c846293ee4f0858e52d6d82/68747470733a2f2f6173736574732e676574706f73746d616e2e636f6d2f636f6d6d6f6e2d73686172652f706f73746d616e2d6c6f676f2d686f72697a6f6e74616c2d333230783133322e706e67"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Manage all of your organization's APIs in Postman, with the industry's most complete API development environment.&lt;/em&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;newman &lt;em&gt;the cli companion for postman&lt;/em&gt; &lt;a href="https://github.com/postmanlabs/newman/actions/workflows/ci.yml" rel="noopener noreferrer"&gt;&lt;img src="https://github.com/postmanlabs/newman/actions/workflows/ci.yml/badge.svg?branch=develop" alt="Build Status"&gt;&lt;/a&gt; &lt;a href="https://codecov.io/gh/postmanlabs/newman" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/3a4478ca4d8675017c1a3f9289d9d9fbadbc95d4be320ccc7e59bf79c69fb369/68747470733a2f2f636f6465636f762e696f2f67682f706f73746d616e6c6162732f6e65776d616e2f6272616e63682f646576656c6f702f67726170682f62616467652e737667" alt="codecov"&gt;&lt;/a&gt;
&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;Newman is a command-line collection runner for Postman. It allows you to effortlessly run and test a Postman collection directly from the command-line. It is built with extensibility in mind so that you can easily integrate it with your continuous integration servers and build systems.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Table of contents&lt;/h2&gt;
&lt;/div&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://github.com/postmanlabs/newman#getting-started" rel="noopener noreferrer"&gt;Getting Started&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/postmanlabs/newman#usage" rel="noopener noreferrer"&gt;Usage&lt;/a&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href="https://github.com/postmanlabs/newman#using-newman-cli" rel="noopener noreferrer"&gt;Using Newman CLI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/postmanlabs/newman#using-newman-as-a-library" rel="noopener noreferrer"&gt;Using Newman as a Library&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/postmanlabs/newman#using-reporters-with-newman" rel="noopener noreferrer"&gt;Using Reporters with Newman&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;a href="https://github.com/postmanlabs/newman#command-line-options" rel="noopener noreferrer"&gt;Command Line Options&lt;/a&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://github.com/postmanlabs/newman#newman-options" rel="noopener noreferrer"&gt;newman-options&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/postmanlabs/newman#newman-run-collection-file-source-options" rel="noopener noreferrer"&gt;newman-run&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/postmanlabs/newman#ssl" rel="noopener noreferrer"&gt;SSL&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/postmanlabs/newman#configuring-proxy" rel="noopener noreferrer"&gt;Configuring Proxy&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;a href="https://github.com/postmanlabs/newman#api-reference" rel="noopener noreferrer"&gt;API Reference&lt;/a&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://github.com/postmanlabs/newman#newmanrunoptions-object--callback-function--run-eventemitter" rel="noopener noreferrer"&gt;newman run&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/postmanlabs/newman#newmanruncallbackerror-object--summary-object" rel="noopener noreferrer"&gt;Run summary object&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/postmanlabs/newman#newmanrunevents" rel="noopener noreferrer"&gt;Events emitted during a collection run&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;a href="https://github.com/postmanlabs/newman#reporters" rel="noopener noreferrer"&gt;Reporters&lt;/a&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://github.com/postmanlabs/newman#configuring-reporters" rel="noopener noreferrer"&gt;Configuring Reporters&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/postmanlabs/newman#cli-reporter" rel="noopener noreferrer"&gt;CLI Reporter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/postmanlabs/newman#json-reporter" rel="noopener noreferrer"&gt;JSON Reporter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/postmanlabs/newman#junitxml-reporter" rel="noopener noreferrer"&gt;JUnit Reporter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/postmanlabs/newman#html-reporter" rel="noopener noreferrer"&gt;HTML Reporter&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;a href="https://github.com/postmanlabs/newman#external-reporters" rel="noopener noreferrer"&gt;External Reporters&lt;/a&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://github.com/postmanlabs/newman#using-external-reporters" rel="noopener noreferrer"&gt;Using External Reporters&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/postmanlabs/newman#creating-your-own-reporter" rel="noopener noreferrer"&gt;Creating Your Own Reporter&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;


&lt;/li&gt;

&lt;li&gt;&lt;a href="https://github.com/postmanlabs/newman#file-uploads" rel="noopener noreferrer"&gt;File Uploads&lt;/a&gt;&lt;/li&gt;

&lt;li&gt;&lt;a href="https://github.com/postmanlabs/newman#using-newman-with-the-postman-api" rel="noopener noreferrer"&gt;Using Newman with the Postman API&lt;/a&gt;&lt;/li&gt;

&lt;li&gt;&lt;a href="https://github.com/postmanlabs/newman#using-newman-in-docker" rel="noopener noreferrer"&gt;Using Newman in Docker&lt;/a&gt;&lt;/li&gt;

&lt;li&gt;&lt;a href="https://github.com/postmanlabs/newman#using-socks-proxy" rel="noopener noreferrer"&gt;Using Socks Proxy&lt;/a&gt;&lt;/li&gt;

&lt;li&gt;&lt;a href="https://github.com/postmanlabs/newman#migration-guide" rel="noopener noreferrer"&gt;Migration Guide&lt;/a&gt;&lt;/li&gt;

&lt;li&gt;&lt;a href="https://github.com/postmanlabs/newman#compatibility" rel="noopener noreferrer"&gt;Compatibility&lt;/a&gt;&lt;/li&gt;

&lt;li&gt;&lt;a href="https://github.com/postmanlabs/newman#contributing" rel="noopener noreferrer"&gt;Contributing&lt;/a&gt;&lt;/li&gt;

&lt;li&gt;&lt;a href="https://github.com/postmanlabs/newman#community-support" rel="noopener noreferrer"&gt;Community Support&lt;/a&gt;&lt;/li&gt;

&lt;li&gt;&lt;a href="https://github.com/postmanlabs/newman#license" rel="noopener noreferrer"&gt;License&lt;/a&gt;&lt;/li&gt;

&lt;/ol&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Getting started&lt;/h2&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/postmanlabs/newman" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;



&lt;h2&gt;
  
  
  3) Docker — Kill environment roulette
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What it is:&lt;/strong&gt; A container platform for packaging your app and its dependencies into reproducible environments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it improves your project:&lt;/strong&gt; It reduces the classic "works on my machine" tax by making local, CI, staging, and production setups much more predictable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; apps with multiple services, onboarding, backend systems, consistent local environments, reproducible deployments.&lt;/p&gt;

&lt;p&gt;Docker will not magically fix bad architecture, but it will stop the weekly ritual where one machine has the right runtime, another machine has the wrong system packages, and production is somehow "close enough." That alone makes a project calmer to ship.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Links:&lt;/strong&gt; &lt;a href="https://www.docker.com/" rel="noopener noreferrer"&gt;Official&lt;/a&gt; · &lt;a href="https://github.com/docker/compose" rel="noopener noreferrer"&gt;Representative repo&lt;/a&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/docker" rel="noopener noreferrer"&gt;
        docker
      &lt;/a&gt; / &lt;a href="https://github.com/docker/compose" rel="noopener noreferrer"&gt;
        compose
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Define and run multi-container applications with Docker
    &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;Table of Contents&lt;/h1&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/docker/compose#docker-compose" rel="noopener noreferrer"&gt;Docker Compose&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/docker/compose#where-to-get-docker-compose" rel="noopener noreferrer"&gt;Where to get Docker Compose&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/docker/compose#windows-and-macos" rel="noopener noreferrer"&gt;Windows and macOS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/docker/compose#linux" rel="noopener noreferrer"&gt;Linux&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/docker/compose#quick-start" rel="noopener noreferrer"&gt;Quick Start&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/docker/compose#contributing" rel="noopener noreferrer"&gt;Contributing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/docker/compose#legacy" rel="noopener noreferrer"&gt;Legacy&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Docker Compose&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a href="https://github.com/docker/compose/releases/latest" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/098ceda2418a39891f8231ac86922b9a80a56edefe4f5fe469ff1dd56748d3ff/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f762f72656c656173652f646f636b65722f636f6d706f73652e7376673f7374796c653d666c61742d737175617265" alt="GitHub release"&gt;&lt;/a&gt;
&lt;a href="https://pkg.go.dev/github.com/docker/compose/v5" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/b92bd49f755879826fbf8a9032e9966a79f3048d9305efd266584ea5ef6db264/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f676f2e6465762d646f63732d3030376439633f7374796c653d666c61742d737175617265266c6f676f3d676f266c6f676f436f6c6f723d7768697465" alt="PkgGoDev"&gt;&lt;/a&gt;
&lt;a href="https://github.com/docker/compose/actions?query=workflow%3Aci" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/7d65ed1750bbdbbd8f7fdbf3f5e146cd5d6f96afbe74e7effbf7e1b8409ad40c/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f646f636b65722f636f6d706f73652f63692e796d6c3f6c6162656c3d6369266c6f676f3d676974687562267374796c653d666c61742d737175617265" alt="Build Status"&gt;&lt;/a&gt;
&lt;a href="https://goreportcard.com/report/github.com/docker/compose/v5" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/5b314dd464f6cc1578e7fedd460b0821be018f39a915bc739d3f57bd48fe4dc5/68747470733a2f2f676f7265706f7274636172642e636f6d2f62616467652f6769746875622e636f6d2f646f636b65722f636f6d706f73652f76353f7374796c653d666c61742d737175617265" alt="Go Report Card"&gt;&lt;/a&gt;
&lt;a href="https://codecov.io/gh/docker/compose" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/1efc6e6c06d2cf4ec8c2db8e3584cc771fff354a9238521feec7085832425827/68747470733a2f2f636f6465636f762e696f2f67682f646f636b65722f636f6d706f73652f6272616e63682f6d61696e2f67726170682f62616467652e7376673f746f6b656e3d4850334b345934637475" alt="Codecov"&gt;&lt;/a&gt;
&lt;a href="https://api.securityscorecards.dev/projects/github.com/docker/compose" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/cddf57aa119a0b420ad0f6dafc87b29be800eb15675327f25de7bf8b77b4a0ee/68747470733a2f2f6170692e736563757269747973636f726563617264732e6465762f70726f6a656374732f6769746875622e636f6d2f646f636b65722f636f6d706f73652f6261646765" alt="OpenSSF Scorecard"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer" href="https://github.com/docker/compose/logo.png?raw=true"&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%2Fdocker%2Fcompose%2FHEAD%2Flogo.png%3Fraw%3Dtrue" alt="Docker Compose" title="Docker Compose Logo"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Docker Compose is a tool for running multi-container applications on Docker
defined using the &lt;a href="https://compose-spec.io" rel="nofollow noopener noreferrer"&gt;Compose file format&lt;/a&gt;
A Compose file is used to define how one or more containers that make up
your application are configured.
Once you have a Compose file, you can create and start your application with a
single command: &lt;code&gt;docker compose up&lt;/code&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: About Docker Swarm
Docker Swarm used to rely on the legacy compose file format but did not adopt the compose specification
so is missing some of the recent enhancements in the compose syntax. After
&lt;a href="https://www.mirantis.com/software/swarm/" rel="nofollow noopener noreferrer"&gt;acquisition by Mirantis&lt;/a&gt; swarm isn't maintained by Docker Inc, and
as such some Docker Compose features aren't accessible to swarm users.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Where to get Docker Compose&lt;/h1&gt;

&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Windows and macOS&lt;/h3&gt;

&lt;/div&gt;
&lt;p&gt;Docker Compose is included in
&lt;a href="https://www.docker.com/products/docker-desktop/" rel="nofollow noopener noreferrer"&gt;Docker Desktop&lt;/a&gt;…&lt;/p&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/docker/compose" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3000:3000"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;NODE_ENV&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;production&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;

  &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn1v0so18u2gs530tznf2.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn1v0so18u2gs530tznf2.gif" alt="The Simpsons road trip GIF" width="480" height="270"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Trying to explain why local, staging, and production are "basically the same" when they absolutely are not. Source: &lt;a href="https://giphy.com/gifs/AnimationOnFOX-the-simpsons-road-to-cincinnati-season-32-ep-8-MsNaqI4G2rT1luIgve" rel="noopener noreferrer"&gt;The Simpsons on Giphy&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  4) Sentry — Catch the breakage before users do
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What it is:&lt;/strong&gt; Error tracking and performance monitoring for apps, APIs, and background jobs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it improves your project:&lt;/strong&gt; It gives failures context instead of vibes. You get stack traces, release correlation, breadcrumbs, request metadata, and performance signals that help you fix the right thing faster.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; production apps, APIs, workers, frontend error tracking, release monitoring, performance debugging.&lt;/p&gt;

&lt;p&gt;Projects get better the minute production stops feeling like a blindfold. Sentry shortens the distance between "something is broken" and "here is the exact release, route, and error path that caused it."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Links:&lt;/strong&gt; &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Official&lt;/a&gt; · &lt;a href="https://github.com/getsentry/sentry" rel="noopener noreferrer"&gt;GitHub&lt;/a&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/getsentry" rel="noopener noreferrer"&gt;
        getsentry
      &lt;/a&gt; / &lt;a href="https://github.com/getsentry/sentry" rel="noopener noreferrer"&gt;
        sentry
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Developer-first error tracking and performance monitoring
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;p&gt;
    &lt;a href="https://sentry.io/?utm_source=github&amp;amp;utm_medium=logo" rel="nofollow noopener noreferrer"&gt;
      &lt;img src="https://camo.githubusercontent.com/63d7d9ecd366a58bdf965da3824d392a4ef345b8097753faa12ae07738ac8431/68747470733a2f2f73656e7472792d6272616e642e73746f726167652e676f6f676c65617069732e636f6d2f73656e7472792d776f72646d61726b2d6461726b2d3238307838342e706e67" alt="Sentry" width="280" height="84"&gt;
    &lt;/a&gt;
  &lt;/p&gt;
  &lt;p&gt;
    Users and logs provide clues. Sentry provides answers.
  &lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;What's Sentry?&lt;/h1&gt;

&lt;/div&gt;

&lt;p&gt;Sentry is the debugging platform that helps every developer detect, trace, and fix issues. Code breaks, fix it faster.&lt;/p&gt;

&lt;p&gt;
  &lt;a rel="noopener noreferrer" href="https://github.com/getsentry/sentry/raw/master/.github/screenshots/issue-details.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fgetsentry%2Fsentry%2Fraw%2Fmaster%2F.github%2Fscreenshots%2Fissue-details.png" width="270"&gt;&lt;/a&gt;
  &lt;a rel="noopener noreferrer" href="https://github.com/getsentry/sentry/raw/master/.github/screenshots/seer.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fgetsentry%2Fsentry%2Fraw%2Fmaster%2F.github%2Fscreenshots%2Fseer.png" width="270"&gt;&lt;/a&gt;
  &lt;a rel="noopener noreferrer" href="https://github.com/getsentry/sentry/raw/master/.github/screenshots/insights.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fgetsentry%2Fsentry%2Fraw%2Fmaster%2F.github%2Fscreenshots%2Finsights.png" width="270"&gt;&lt;/a&gt;
  &lt;a rel="noopener noreferrer" href="https://github.com/getsentry/sentry/raw/master/.github/screenshots/traces.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fgetsentry%2Fsentry%2Fraw%2Fmaster%2F.github%2Fscreenshots%2Ftraces.png" width="270"&gt;&lt;/a&gt;
  &lt;a rel="noopener noreferrer" href="https://github.com/getsentry/sentry/raw/master/.github/screenshots/trace-explorer.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fgetsentry%2Fsentry%2Fraw%2Fmaster%2F.github%2Fscreenshots%2Ftrace-explorer.png" width="270"&gt;&lt;/a&gt;
  &lt;a rel="noopener noreferrer" href="https://github.com/getsentry/sentry/raw/master/.github/screenshots/replays.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fgetsentry%2Fsentry%2Fraw%2Fmaster%2F.github%2Fscreenshots%2Freplays.png" width="270"&gt;&lt;/a&gt;
  &lt;a rel="noopener noreferrer" href="https://github.com/getsentry/sentry/raw/master/.github/screenshots/insights.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fgetsentry%2Fsentry%2Fraw%2Fmaster%2F.github%2Fscreenshots%2Finsights.png" width="270"&gt;&lt;/a&gt;
  &lt;a rel="noopener noreferrer" href="https://github.com/getsentry/sentry/raw/master/.github/screenshots/logs.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fgetsentry%2Fsentry%2Fraw%2Fmaster%2F.github%2Fscreenshots%2Flogs.png" width="270"&gt;&lt;/a&gt;
  &lt;a rel="noopener noreferrer" href="https://github.com/getsentry/sentry/raw/master/.github/screenshots/uptime.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fgetsentry%2Fsentry%2Fraw%2Fmaster%2F.github%2Fscreenshots%2Fuptime.png" width="270"&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Official Sentry SDKs&lt;/h2&gt;

&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/getsentry/sentry-javascript" rel="noopener noreferrer"&gt;JavaScript&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/getsentry/sentry-electron/" rel="noopener noreferrer"&gt;Electron&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/getsentry/sentry-react-native" rel="noopener noreferrer"&gt;React-Native&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/getsentry/sentry-python" rel="noopener noreferrer"&gt;Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/getsentry/sentry-ruby" rel="noopener noreferrer"&gt;Ruby&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/getsentry/sentry-php" rel="noopener noreferrer"&gt;PHP&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/getsentry/sentry-laravel" rel="noopener noreferrer"&gt;Laravel&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/getsentry/sentry-go" rel="noopener noreferrer"&gt;Go&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/getsentry/sentry-rust" rel="noopener noreferrer"&gt;Rust&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/getsentry/sentry-java" rel="noopener noreferrer"&gt;Java/Kotlin&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/getsentry/sentry-cocoa" rel="noopener noreferrer"&gt;Objective-C/Swift&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/getsentry/sentry-dotnet" rel="noopener noreferrer"&gt;C#/F#&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/getsentry/sentry-native" rel="noopener noreferrer"&gt;C/C++&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/getsentry/sentry-dart" rel="noopener noreferrer"&gt;Dart/Flutter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/getsentry/perl-raven" rel="noopener noreferrer"&gt;Perl&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/getsentry/sentry-clj/" rel="noopener noreferrer"&gt;Clojure&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/getsentry/sentry-elixir" rel="noopener noreferrer"&gt;Elixir&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/getsentry/sentry-unity" rel="noopener noreferrer"&gt;Unity&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/getsentry/sentry-unreal" rel="noopener noreferrer"&gt;Unreal Engine&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/getsentry/sentry-godot" rel="noopener noreferrer"&gt;Godot Engine&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/getsentry/sentry-powershell" rel="noopener noreferrer"&gt;PowerShell&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Resources&lt;/h1&gt;

&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.sentry.io/" rel="nofollow noopener noreferrer"&gt;Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/getsentry/sentry/discussions" rel="noopener noreferrer"&gt;Discussions&lt;/a&gt; (Bugs, feature requests,
general questions)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://discord.gg/PXa5Apfe7K" rel="nofollow noopener noreferrer"&gt;Discord&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.sentry.io/internal/contributing/" rel="nofollow noopener noreferrer"&gt;Contributing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/getsentry/sentry/issues" rel="noopener noreferrer"&gt;Bug Tracker&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/getsentry/sentry" rel="noopener noreferrer"&gt;Code&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://explore.transifex.com/getsentry/sentry/" rel="nofollow noopener noreferrer"&gt;Transifex&lt;/a&gt; (Translate
Sentry!)&lt;/li&gt;
&lt;/ul&gt;

&lt;/div&gt;
&lt;br&gt;
&lt;br&gt;
  &lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/getsentry/sentry" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;br&gt;&lt;br&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="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Sentry&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;@sentry/node&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;Sentry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;dsn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SENTRY_DSN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NODE_ENV&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;tracesSampleRate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.2&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;
  
  
  5) Playwright — Test the flows that actually make or break products
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What it is:&lt;/strong&gt; A browser automation and end-to-end testing framework for real user journeys.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it improves your project:&lt;/strong&gt; Unit tests are great for logic. Playwright is how you protect the stuff people actually click, type, submit, and depend on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; login flows, checkout paths, onboarding, regression prevention, cross-browser testing, critical UI journeys.&lt;/p&gt;

&lt;p&gt;A lot of products feel stable right up until somebody runs the real flow. One solid Playwright test for auth, payments, or signup often prevents the kind of regression that makes a team lose confidence in every release.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Links:&lt;/strong&gt; &lt;a href="https://playwright.dev/" rel="noopener noreferrer"&gt;Official&lt;/a&gt; · &lt;a href="https://github.com/microsoft/playwright" rel="noopener noreferrer"&gt;GitHub&lt;/a&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/microsoft" rel="noopener noreferrer"&gt;
        microsoft
      &lt;/a&gt; / &lt;a href="https://github.com/microsoft/playwright" rel="noopener noreferrer"&gt;
        playwright
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Playwright is a framework for Web Testing and Automation. It allows testing Chromium, Firefox and WebKit with a single API. 
    &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;🎭 Playwright&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;&lt;a href="https://www.npmjs.com/package/playwright" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/0c0b88b731baae15c1fea7318541bac85741ecce8999effb24c9ef0424c90a9c/68747470733a2f2f696d672e736869656c64732e696f2f6e706d2f762f706c61797772696768742e737667" alt="npm version"&gt;&lt;/a&gt; &lt;a href="https://www.chromium.org/Home" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/3bc87c4989cdbc340aee9d64f0afee6e0fea831220dd2832b2b1f09e3d33f6b0/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6368726f6d69756d2d3134372e302e373732372e34392d626c75652e7376673f6c6f676f3d676f6f676c652d6368726f6d65" alt="Chromium version"&gt;&lt;/a&gt; &lt;a href="https://www.mozilla.org/en-US/firefox/new/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/7a08142d1731126e65ef4aba60edf37271ad1cec4c693ead952639015ac968cd/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f66697265666f782d3134382e302e322d626c75652e7376673f6c6f676f3d66697265666f7862726f77736572" alt="Firefox version"&gt;&lt;/a&gt; &lt;a href="https://webkit.org/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/5c900e8803765a59d9436251138d1ffc21f224465c01ba5a22cf5ebca96ea992/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7765626b69742d32362e342d626c75652e7376673f6c6f676f3d736166617269" alt="WebKit version"&gt;&lt;/a&gt; &lt;a href="https://aka.ms/playwright/discord" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/a0f278c5bb9bf1c04af523df174c2526d259f8441df73f3378f686863c9ffde0/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6a6f696e2d646973636f72642d696e666f726d6174696f6e616c" alt="Join Discord"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;
&lt;a href="https://playwright.dev" rel="nofollow noopener noreferrer"&gt;Documentation&lt;/a&gt; | &lt;a href="https://playwright.dev/docs/api/class-playwright" rel="nofollow noopener noreferrer"&gt;API reference&lt;/a&gt;
&lt;/h2&gt;
&lt;/div&gt;

&lt;p&gt;Playwright is a framework for web automation and testing. It drives Chromium, Firefox, and WebKit with a single API — in your tests, in your scripts, and as a tool for AI agents.&lt;/p&gt;

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

&lt;p&gt;Choose the path that fits your workflow:&lt;/p&gt;

&lt;p&gt;&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;br&gt;
&lt;thead&gt;
&lt;br&gt;
&lt;tr&gt;


&lt;th&gt;Best for&lt;/th&gt;
&lt;br&gt;
&lt;th&gt;Install&lt;/th&gt;
&lt;br&gt;
&lt;/tr&gt;
&lt;br&gt;
&lt;/thead&gt;
&lt;br&gt;
&lt;tbody&gt;
&lt;br&gt;
&lt;tr&gt;
&lt;br&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href="https://github.com/microsoft/playwright#playwright-test" rel="noopener noreferrer"&gt;Playwright Test&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;br&gt;
&lt;td&gt;End-to-end testing&lt;/td&gt;
&lt;br&gt;
&lt;td&gt;&lt;code&gt;npm init playwright@latest&lt;/code&gt;&lt;/td&gt;
&lt;br&gt;
&lt;/tr&gt;
&lt;br&gt;
&lt;tr&gt;
&lt;br&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href="https://github.com/microsoft/playwright#playwright-cli" rel="noopener noreferrer"&gt;Playwright CLI&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;br&gt;
&lt;td&gt;Coding agents (Claude Code, Copilot)&lt;/td&gt;
&lt;br&gt;
&lt;td&gt;&lt;code&gt;npm i -g @playwright/cli@latest&lt;/code&gt;&lt;/td&gt;
&lt;br&gt;
&lt;/tr&gt;
&lt;br&gt;
&lt;tr&gt;
&lt;br&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href="https://github.com/microsoft/playwright#playwright-mcp" rel="noopener noreferrer"&gt;Playwright MCP&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;br&gt;
&lt;td&gt;AI agents and LLM-driven automation&lt;/td&gt;
&lt;br&gt;
&lt;td&gt;&lt;code&gt;npx @playwright/mcp@latest&lt;/code&gt;&lt;/td&gt;
&lt;br&gt;
&lt;/tr&gt;
&lt;br&gt;
&lt;tr&gt;
&lt;br&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href="https://github.com/microsoft/playwright#playwright-library" rel="noopener noreferrer"&gt;Playwright Library&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;br&gt;
&lt;td&gt;Browser automation scripts&lt;/td&gt;
&lt;br&gt;
&lt;td&gt;&lt;code&gt;npm i playwright&lt;/code&gt;&lt;/td&gt;
&lt;br&gt;
&lt;/tr&gt;
&lt;br&gt;
&lt;tr&gt;
&lt;br&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href="https://github.com/microsoft/playwright#vs-code-extension" rel="noopener noreferrer"&gt;VS Code Extension&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;br&gt;
&lt;td&gt;Test authoring and debugging in VS Code&lt;/td&gt;
&lt;br&gt;
&lt;td&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=ms-playwright.playwright" rel="nofollow noopener noreferrer"&gt;Install from Marketplace&lt;/a&gt;&lt;/td&gt;
&lt;br&gt;
&lt;/tr&gt;
&lt;br&gt;
&lt;/tbody&gt;
&lt;br&gt;
&lt;/table&gt;&lt;/div&gt;&lt;br&gt;
&lt;/p&gt;


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

&lt;/div&gt;

&lt;p&gt;Playwright Test is a full-featured test runner built for end-to-end testing. It runs tests across Chromium, Firefox, and WebKit with full browser isolation, auto-waiting, and web-first assertions.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Install&lt;/h3&gt;

&lt;/div&gt;

&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;npm init playwright@latest&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Or add manually:&lt;/p&gt;

&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;npm i -D @playwright/test
npx playwright install&lt;/pre&gt;

&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Write a test&lt;/h3&gt;

&lt;/div&gt;

&lt;div class="highlight highlight-source-ts notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt; &lt;span class="pl-s1"&gt;test&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s1"&gt;expect&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/microsoft/playwright" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&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;test&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expect&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;@playwright/test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;checkout completes successfully&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;page&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;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/checkout&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;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByLabel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dev@example.com&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;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Pay now&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;click&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;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Payment confirmed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&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;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fytknrcugcpceeb9l9wbj.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fytknrcugcpceeb9l9wbj.gif" alt="South Park developers staring at screens" width="384" height="270"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The exact moment your one "probably fine" user flow turns out to be the one thing that definitely needed an end-to-end test. Source: &lt;a href="https://giphy.com/gifs/southparkgifs-l3vRd3vZPrApPqzjq" rel="noopener noreferrer"&gt;South Park on Giphy&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  6) Swagger / OpenAPI — Turn your API into a real contract
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What it is:&lt;/strong&gt; A specification-driven way to describe routes, payloads, auth requirements, and responses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it improves your project:&lt;/strong&gt; It makes your API explicit. That means better docs, fewer frontend/backend misunderstandings, easier integrations, and less behavior hidden in memory or tribal knowledge.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; internal APIs, public APIs, SDK generation, backend teams, frontend handoff, partner integrations.&lt;/p&gt;

&lt;p&gt;When the contract is written down, fewer things depend on guessing. That helps new teammates onboard faster, frontend work unblock earlier, and backend changes stop surprising everybody else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Links:&lt;/strong&gt; &lt;a href="https://www.openapis.org/" rel="noopener noreferrer"&gt;Official spec&lt;/a&gt; · &lt;a href="https://swagger.io/swagger-ui/" rel="noopener noreferrer"&gt;Swagger UI&lt;/a&gt; · &lt;a href="https://github.com/swagger-api/swagger-ui" rel="noopener noreferrer"&gt;Representative repo&lt;/a&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/swagger-api" rel="noopener noreferrer"&gt;
        swagger-api
      &lt;/a&gt; / &lt;a href="https://github.com/swagger-api/swagger-ui" rel="noopener noreferrer"&gt;
        swagger-ui
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Swagger UI is a collection of HTML, JavaScript, and CSS assets that dynamically generate beautiful documentation from a Swagger-compliant API.
    &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;&lt;a rel="noopener noreferrer nofollow" href="https://raw.githubusercontent.com/swagger-api/swagger.io/wordpress/images/assets/SWU-logo-clr.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%2Fswagger-api%2Fswagger.io%2Fwordpress%2Fimages%2Fassets%2FSWU-logo-clr.png" width="300"&gt;&lt;/a&gt;&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;&lt;a href="http://badge.fury.io/js/swagger-ui" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/cfe28814e1caf20cbfa822bdd725f8ba712d06c6fb0255158ef957e1a85d0300/68747470733a2f2f62616467652e667572792e696f2f6a732f737761676765722d75692e737667" alt="NPM version"&gt;&lt;/a&gt;
&lt;a href="https://jenkins.swagger.io/view/OSS%20-%20JavaScript/job/oss-swagger-ui-master/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/20fa9ff4b88dd6e8ee2952e4736b530ecb6eb97a09aa74e7f7730f0c15e2a820/68747470733a2f2f6a656e6b696e732e737761676765722e696f2f766965772f4f53532532302d2532304a6176615363726970742f6a6f622f6f73732d737761676765722d75692d6d61737465722f62616467652f69636f6e3f7375626a6563743d6a656e6b696e732532306275696c64" alt="Build Status"&gt;&lt;/a&gt;
&lt;a href="https://jenkins.swagger.io/job/oss-swagger-ui-security-audit/lastBuild/console" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/e323b81c6ca5c63dd835bb6095f8f2a46669c4d1194ee9768a525a32d69e5ef0/68747470733a2f2f6a656e6b696e732e737761676765722e696f2f6275696c645374617475732f69636f6e3f6a6f623d6f73732d737761676765722d75692d73656375726974792d6175646974267375626a6563743d6e706d2532306175646974" alt="npm audit"&gt;&lt;/a&gt;
&lt;a href="https://github.com/swagger-api/swagger-ui/graphs/contributors" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/cdde3d9c89070c961c862fbbc92cfa497ada3a3a9a427279326b7cc2af316c20/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f636f6e7472696275746f72732d616e6f6e2f737761676765722d6170692f737761676765722d75692e737667" alt="total GitHub contributors"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.npmjs.com/package/swagger-ui" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/a74195c108b9895617dab4ffc4c3cc4f75aaa00dbd9ad24c2f68777c6616a912/68747470733a2f2f696d672e736869656c64732e696f2f6e706d2f646d2f737761676765722d75692e7376673f6c6162656c3d6e706d253230646f776e6c6f616473" alt="monthly npm installs"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/93a4b0cdd6b10e98d88f41fc4bd5583dafc95616232039f049238dfbc654439b/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f646f636b65722d646f636b65722e737761676765722e696f25324673776167676572617069253246737761676765722d2d75692d626c7565"&gt;&lt;img src="https://camo.githubusercontent.com/93a4b0cdd6b10e98d88f41fc4bd5583dafc95616232039f049238dfbc654439b/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f646f636b65722d646f636b65722e737761676765722e696f25324673776167676572617069253246737761676765722d2d75692d626c7565" alt="docker registry"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/19d9515165e5ccfc77c6f003b6f55732d408a1933cbadc95b11f79d0239152c3/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f646d2f737761676765722d6170692f737761676765722d75692e7376673f6c6162656c3d7061636b6167697374253230696e7374616c6c73"&gt;&lt;img src="https://camo.githubusercontent.com/19d9515165e5ccfc77c6f003b6f55732d408a1933cbadc95b11f79d0239152c3/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f646d2f737761676765722d6170692f737761676765722d75692e7376673f6c6162656c3d7061636b6167697374253230696e7374616c6c73" alt="monthly packagist installs"&gt;&lt;/a&gt;
&lt;a href="https://bundlephobia.com/package/swagger-ui" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/7ee0491b66dddf6f7b7676ce1a50c68a236319b4079f63fc88493ad37087300e/68747470733a2f2f696d672e736869656c64732e696f2f62756e646c6570686f6269612f6d696e7a69702f737761676765722d75692e7376673f6c6162656c3d677a697025323073697a65" alt="gzip size"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Introduction&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a href="https://swagger.io/tools/swagger-ui/" rel="nofollow noopener noreferrer"&gt;Swagger UI&lt;/a&gt; allows anyone — be it your development team or your end consumers — to visualize and interact with the API’s resources without having any of the implementation logic in place. It’s automatically generated from your OpenAPI (formerly known as Swagger) Specification, with the visual documentation making it easy for back end implementation and client side consumption.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;General&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;👉🏼 Want to score an easy open-source contribution?&lt;/strong&gt; Check out our &lt;a href="https://github.com/swagger-api/swagger-ui/issues?q=is%3Aissue+is%3Aopen+label%3A%22Good+first+issue%22" rel="noopener noreferrer"&gt;Good first issue&lt;/a&gt; label.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;🕰️ Looking for the older version of Swagger UI?&lt;/strong&gt; Refer to the &lt;a href="https://github.com/swagger-api/swagger-ui/tree/2.x" rel="noopener noreferrer"&gt;&lt;em&gt;2.x&lt;/em&gt; branch&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This repository publishes three different NPM modules:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.npmjs.com/package/swagger-ui" rel="nofollow noopener noreferrer"&gt;swagger-ui&lt;/a&gt; is a traditional npm module intended for use in single-page applications that are capable of resolving dependencies (via Webpack, Browserify, etc.).&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.npmjs.com/package/swagger-ui-dist" rel="nofollow noopener noreferrer"&gt;swagger-ui-dist&lt;/a&gt; is a dependency-free module that includes everything you need to serve Swagger UI in a server-side project, or a single-page application that can't resolve npm module dependencies.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.npmjs.com/package/swagger-ui-react" rel="nofollow noopener noreferrer"&gt;swagger-ui-react&lt;/a&gt; is Swagger…&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/swagger-api/swagger-ui" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;openapi&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;3.1.0&lt;/span&gt;
&lt;span class="na"&gt;info&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Example&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;API"&lt;/span&gt;
  &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1.0.0&lt;/span&gt;
&lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="s"&gt;/users/{id}&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Fetch a user&lt;/span&gt;
      &lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;in&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;path&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;id&lt;/span&gt;
          &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
          &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
      &lt;span class="na"&gt;responses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;200"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;returned&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;successfully"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn1v0so18u2gs530tznf2.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn1v0so18u2gs530tznf2.gif" alt="The Simpsons road trip GIF" width="480" height="270"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Frontend, backend, and QA when the API shape is finally written down in one place instead of floating around in chat history. Source: &lt;a href="https://giphy.com/gifs/AnimationOnFOX-the-simpsons-road-to-cincinnati-season-32-ep-8-MsNaqI4G2rT1luIgve" rel="noopener noreferrer"&gt;The Simpsons on Giphy&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  7) pompelmi — Secure the upload edge before it bites you
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What it is:&lt;/strong&gt; A Node.js file upload security tool that scans uploads in-process before you accept them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it improves your project:&lt;/strong&gt; Upload handling is one of the most underestimated attack surfaces in web apps. Extensions lie, MIME types can be spoofed, archives can be risky, and "we'll validate later" is how tiny assumptions become expensive incidents.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Express, Fastify, Koa, NestJS, Next.js, privacy-sensitive products, public upload flows, moderation-heavy apps.&lt;/p&gt;

&lt;p&gt;Most teams spend more time styling the upload button than defending the upload pipeline. That is backwards. A tool like &lt;a href="https://github.com/pompelmi/pompelmi" rel="noopener noreferrer"&gt;pompelmi&lt;/a&gt; helps you make verdict-based decisions before a file becomes your problem, which is exactly the kind of boring guardrail mature projects quietly rely on.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/pompelmi/pompelmi" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;br&gt;&lt;br&gt;
Site: &lt;a href="https://pompelmi.github.io/pompelmi/" rel="noopener noreferrer"&gt;Project site&lt;/a&gt;&lt;br&gt;&lt;br&gt;
Demo: &lt;a href="https://pompelmi.github.io/pompelmi/demo/?ref=producthunt" rel="noopener noreferrer"&gt;Live demo&lt;/a&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;scanBytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;STRICT_PUBLIC_UPLOAD&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;pompelmi&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;report&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;scanBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;originalname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;mimeType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mimetype&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;STRICT_PUBLIC_UPLOAD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;failClosed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;verdict&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;clean&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;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;422&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Upload blocked&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;verdict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;verdict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;reasons&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reasons&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;If you handle user uploads in Node.js, this is the kind of layer that makes your project feel much more intentional. It is not just about malware. It is about treating MIME spoofing, risky archives, and uncertain files as security decisions instead of wishful thinking.&lt;/p&gt;
&lt;h3&gt;
  
  
  Repository preview
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftpta32fr1ijb956g7es6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftpta32fr1ijb956g7es6.png" alt="pompelmi repository banner" width="800" height="420"&gt;&lt;/a&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/pompelmi" rel="noopener noreferrer"&gt;
        pompelmi
      &lt;/a&gt; / &lt;a href="https://github.com/pompelmi/pompelmi" rel="noopener noreferrer"&gt;
        pompelmi
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Open-source file upload security for Node.js. Scan files before storage to detect malware, MIME spoofing, and risky archives.
    &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;Pompelmi&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;In-process file upload security for Node.js. Inspect untrusted files before storage so your application can reject, quarantine, or accept with context.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.npmjs.com/package/pompelmi" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/e0d42981d2ee0a7922681b51f292837bf13d14622e6e08fdb9ab6a10c0e01c8b/68747470733a2f2f696d672e736869656c64732e696f2f6e706d2f762f706f6d70656c6d69" alt="npm version"&gt;&lt;/a&gt;
&lt;a href="https://github.com/pompelmi/pompelmi/actions/workflows/ci.yml" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/88fcc5ec40e69b2f99fc5b711e2d11e121c009bbd7e34320be41a01ec6df9023/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f706f6d70656c6d692f706f6d70656c6d692f63692e796d6c3f6c6162656c3d6369" alt="CI"&gt;&lt;/a&gt;
&lt;a href="https://github.com/pompelmi/pompelmi/stargazers" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/0c7b5fc13dd2baf14b42b2c762318188faec8b92a0f0e39b3d5f937cc1072532/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f73746172732f706f6d70656c6d692f706f6d70656c6d69" alt="GitHub stars"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Pompelmi helps reduce:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;MIME / extension spoofing and magic-byte mismatches&lt;/li&gt;
&lt;li&gt;risky archive structures such as ZIP bombs, traversal, and deep nesting&lt;/li&gt;
&lt;li&gt;risky document and binary patterns such as PDF actions, Office macro hints, PE signatures, and polyglot files&lt;/li&gt;
&lt;li&gt;store-first upload flows that need a clean / suspicious / malicious verdict before persistence&lt;/li&gt;
&lt;li&gt;known malicious matches when you plug in YARA or another scanner&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Install: &lt;code&gt;npm install pompelmi&lt;/code&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Quick Start&lt;/h2&gt;
&lt;/div&gt;
&lt;div class="highlight highlight-source-ts notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt; &lt;span class="pl-s1"&gt;scanBytes&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-c1"&gt;STRICT_PUBLIC_UPLOAD&lt;/span&gt; &lt;span class="pl-kos"&gt;}&lt;/span&gt; &lt;span class="pl-k"&gt;from&lt;/span&gt; &lt;span class="pl-s"&gt;'pompelmi'&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
&lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;report&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-k"&gt;await&lt;/span&gt; &lt;span class="pl-en"&gt;scanBytes&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;file&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;buffer&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
  &lt;span class="pl-c1"&gt;filename&lt;/span&gt;: &lt;span class="pl-s1"&gt;file&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;originalname&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
  &lt;span class="pl-c1"&gt;mimeType&lt;/span&gt;: &lt;span class="pl-s1"&gt;file&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;mimetype&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
  &lt;span class="pl-c1"&gt;policy&lt;/span&gt;: &lt;span class="pl-c1"&gt;STRICT_PUBLIC_UPLOAD&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
  &lt;span class="pl-c1"&gt;failClosed&lt;/span&gt;: &lt;span class="pl-c1"&gt;true&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;

&lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;report&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;verdict&lt;/span&gt; &lt;span class="pl-c1"&gt;!==&lt;/span&gt; &lt;span class="pl-s"&gt;'clean'&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
  &lt;span class="pl-k"&gt;return&lt;/span&gt; &lt;span class="pl-s1"&gt;res&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;status&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-c1"&gt;422&lt;/span&gt;&lt;span class="pl-kos"&gt;)&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/pompelmi/pompelmi" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flb1s195cp236p7ihx18a.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flb1s195cp236p7ihx18a.gif" alt="pompelmi malware detection demo" width="600" height="501"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Upload security usually feels optional right until it really, really is not.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  8) Dependabot — Keep dependency drift from turning into real risk
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What it is:&lt;/strong&gt; GitHub's automated dependency update system for packages, containers, and workflow versions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it improves your project:&lt;/strong&gt; It keeps security and maintenance work moving in small increments instead of one painful cleanup sprint after months of neglect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; npm repos, Docker images, GitHub Actions workflows, libraries, apps with busy dependency trees.&lt;/p&gt;

&lt;p&gt;Most dependency risk is boring right until it is suddenly not. Dependabot makes updates reviewable, visible, and routine, which is much better than discovering six months of drift during an incident or release crunch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Links:&lt;/strong&gt; &lt;a href="https://docs.github.com/github/administering-a-repository/about-github-dependabot" rel="noopener noreferrer"&gt;Docs&lt;/a&gt; · &lt;a href="https://github.com/dependabot/dependabot-core" rel="noopener noreferrer"&gt;GitHub&lt;/a&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/dependabot" rel="noopener noreferrer"&gt;
        dependabot
      &lt;/a&gt; / &lt;a href="https://github.com/dependabot/dependabot-core" rel="noopener noreferrer"&gt;
        dependabot-core
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      🤖 Dependabot's core logic for creating update PRs.
    &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;
    
        
        
        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fuser-images.githubusercontent.com%2F7659%2F174594540-5e29e523-396a-465b-9a6e-6cab5b15a568.svg" alt="Dependabot" width="336"&gt;
    
&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;Welcome to the public home of Dependabot &lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.githubassets.com%2Fimages%2Ficons%2Femoji%2Fdependabot.png" class="article-body-image-wrapper"&gt;&lt;img class="emoji" title=":dependabot:" alt=":dependabot:" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.githubassets.com%2Fimages%2Ficons%2Femoji%2Fdependabot.png" height="20" width="20"&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Table of Contents&lt;/h1&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/dependabot/dependabot-core#what-is-dependabot-core" rel="noopener noreferrer"&gt;What is Dependabot-Core?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/dependabot/dependabot-core#how-to-run-dependabot" rel="noopener noreferrer"&gt;How to run Dependabot&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/dependabot/dependabot-core#contributing-to-dependabot" rel="noopener noreferrer"&gt;Contributing to Dependabot&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/dependabot/dependabot-core#reporting-issues-and-feature-requests" rel="noopener noreferrer"&gt;Reporting Issues and Feature Requests&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/dependabot/dependabot-core#submitting-pull-requests" rel="noopener noreferrer"&gt;Submitting Pull Requests&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/dependabot/dependabot-core#new-ecosystems" rel="noopener noreferrer"&gt;New Ecosystems&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/dependabot/dependabot-core#development-guide" rel="noopener noreferrer"&gt;Development Guide&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/dependabot/dependabot-core#getting-a-development-environment-running" rel="noopener noreferrer"&gt;Getting a Development Environment Running&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/dependabot/dependabot-core#debugging-problems" rel="noopener noreferrer"&gt;Debugging Problems&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/dependabot/dependabot-core#running-tests" rel="noopener noreferrer"&gt;Running Tests&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/dependabot/dependabot-core#profiling" rel="noopener noreferrer"&gt;Profiling&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/dependabot/dependabot-core#architecture-and-code-layout" rel="noopener noreferrer"&gt;Architecture and Code Layout&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/dependabot/dependabot-core#trademarks" rel="noopener noreferrer"&gt;Trademarks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/dependabot/dependabot-core#notes-for-project-maintainers" rel="noopener noreferrer"&gt;Notes for Project Maintainers&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;What is Dependabot-Core?&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;Dependabot-Core is the library at the heart of &lt;a href="https://docs.github.com/en/code-security/dependabot" rel="noopener noreferrer"&gt;Dependabot&lt;/a&gt; security / version updates.&lt;/p&gt;
&lt;p&gt;Use it to generate automated pull requests updating dependencies for projects written in Ruby, JavaScript, Python,
PHP, Dart, Elixir, Elm, Go, Rust, Java, Julia, and .NET. It can also update git submodules, Docker files, Opentofu, Terraform files and Pre-Commit hooks.
Features include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Check for the latest version of a dependency &lt;em&gt;that's resolvable given a project's other dependencies&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Generate updated manifest and lockfiles for a new dependency version&lt;/li&gt;
&lt;li&gt;Generate PR descriptions that include the updated dependency's changelogs, release notes, and commits&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;How to&lt;/h1&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/dependabot/dependabot-core" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
&lt;span class="na"&gt;updates&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;package-ecosystem&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm&lt;/span&gt;
    &lt;span class="na"&gt;directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/&lt;/span&gt;
    &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;weekly&lt;/span&gt;
    &lt;span class="na"&gt;open-pull-requests-limit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;package-ecosystem&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github-actions&lt;/span&gt;
    &lt;span class="na"&gt;directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/&lt;/span&gt;
    &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;weekly&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyxknr34f6np28h96a1kx.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyxknr34f6np28h96a1kx.gif" alt="Family Guy shocked at the computer" width="480" height="270"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The face you make when a stale dependency turns into tonight's emergency patch window. Source: &lt;a href="https://giphy.com/gifs/fox-computer-family-guy-5nvQ7fBWhPVXXOcfRI" rel="noopener noreferrer"&gt;Family Guy on Giphy&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  9) Excalidraw — Think clearly before you build expensively
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What it is:&lt;/strong&gt; A fast, low-friction whiteboard for architecture sketches, flows, states, and system diagrams.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it improves your project:&lt;/strong&gt; It helps teams see the system before they commit to the system. That reduces misalignment, hand-wavy assumptions, and long explanations nobody remembers later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; architecture reviews, sequence diagrams, onboarding docs, RFCs, async collaboration, state-flow discussions.&lt;/p&gt;

&lt;p&gt;A five-minute sketch can save hours of wrong implementation. Excalidraw is especially good when a project keeps getting explained verbally but never gets turned into something the whole team can point at and improve together.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Links:&lt;/strong&gt; &lt;a href="https://excalidraw.com/" rel="noopener noreferrer"&gt;Official&lt;/a&gt; · &lt;a href="https://github.com/excalidraw/excalidraw" rel="noopener noreferrer"&gt;GitHub&lt;/a&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/excalidraw" rel="noopener noreferrer"&gt;
        excalidraw
      &lt;/a&gt; / &lt;a href="https://github.com/excalidraw/excalidraw" rel="noopener noreferrer"&gt;
        excalidraw
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Virtual whiteboard for sketching hand-drawn like diagrams
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;a href="https://excalidraw.com/" rel="nofollow noopener noreferrer"&gt;
  
    
    &lt;img alt="Excalidraw" src="https://camo.githubusercontent.com/998e67de729ddc7a96857f3ad11075481ee1ea59e6c5e79c6b13886013cce97b/68747470733a2f2f657863616c69647261772e6e7963332e63646e2e6469676974616c6f6365616e7370616365732e636f6d2f6769746875622f657863616c69647261775f6769746875625f636f7665725f322e706e67"&gt;
  
&lt;/a&gt;
&lt;div class="markdown-heading"&gt;
&lt;h4 class="heading-element"&gt;
  &lt;a href="https://excalidraw.com" rel="nofollow noopener noreferrer"&gt;Excalidraw Editor&lt;/a&gt; |
  &lt;a href="https://plus.excalidraw.com/blog" rel="nofollow noopener noreferrer"&gt;Blog&lt;/a&gt; |
  &lt;a href="https://docs.excalidraw.com" rel="nofollow noopener noreferrer"&gt;Documentation&lt;/a&gt; |
  &lt;a href="https://plus.excalidraw.com" rel="nofollow noopener noreferrer"&gt;Excalidraw+&lt;/a&gt;
&lt;/h4&gt;
&lt;/div&gt;

&lt;div&gt;
  &lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;
    An open source virtual hand-drawn style whiteboard. &lt;br&gt;
    Collaborative and end-to-end encrypted. &lt;br&gt;
  &lt;br&gt;
  &lt;/h2&gt;
&lt;/div&gt;


&lt;/div&gt;
&lt;br&gt;
&lt;br&gt;

&lt;p&gt;
  &lt;a href="https://github.com/excalidraw/excalidraw/blob/master/LICENSE" rel="noopener noreferrer"&gt;
    &lt;img alt="Excalidraw is released under the MIT license." src="https://camo.githubusercontent.com/7013272bd27ece47364536a221edb554cd69683b68a46fc0ee96881174c4214c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d626c75652e737667"&gt;&lt;/a&gt;
  &lt;a href="https://www.npmjs.com/package/@excalidraw/excalidraw" rel="nofollow noopener noreferrer"&gt;
    &lt;img alt="npm downloads/month" src="https://camo.githubusercontent.com/b888038a3f43d195f22378d9e805a780cf8ae92b3a21170cf3b9a6d49b646c90/68747470733a2f2f696d672e736869656c64732e696f2f6e706d2f646d2f40657863616c69647261772f657863616c6964726177"&gt;&lt;/a&gt;
  &lt;a href="https://docs.excalidraw.com/docs/introduction/contributing" rel="nofollow noopener noreferrer"&gt;
    &lt;img alt="PRs welcome!" src="https://camo.githubusercontent.com/7d9ed3c8f22eceb1711573169b1390cc0b1194467340dc815205060c162b5309/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5052732d77656c636f6d652d627269676874677265656e2e7376673f7374796c653d666c6174"&gt;&lt;/a&gt;
  &lt;a href="https://discord.gg/UexuTaE" rel="nofollow noopener noreferrer"&gt;
    &lt;img alt="Chat on Discord" src="https://camo.githubusercontent.com/bd5521032bc04de0884f38454e796fc9841b675ed8b713960f3ad8fa1912ffc9/68747470733a2f2f696d672e736869656c64732e696f2f646973636f72642f3732333637323433303734343137343638323f636f6c6f723d373338616436266c6162656c3d436861742532306f6e253230446973636f7264266c6f676f3d646973636f7264266c6f676f436f6c6f723d666666666666267769646765743d66616c7365"&gt;&lt;/a&gt;
  &lt;a href="https://deepwiki.com/excalidraw/excalidraw" rel="nofollow noopener noreferrer"&gt;
    &lt;img alt="Ask DeepWiki" src="https://camo.githubusercontent.com/0f5ae213ac378635adeb5d7f13cef055ad2f7d9a47b36de7b1c67dbe09f609ca/68747470733a2f2f6465657077696b692e636f6d2f62616467652e737667"&gt;&lt;/a&gt;
  &lt;a href="https://twitter.com/excalidraw" rel="nofollow noopener noreferrer"&gt;
    &lt;img alt="Follow Excalidraw on Twitter" src="https://camo.githubusercontent.com/601234ddc1c94ae90113c962cbae8f570910de0a8706705d1a27897cd74e47ad/68747470733a2f2f696d672e736869656c64732e696f2f747769747465722f666f6c6c6f772f657863616c69647261772e7376673f6c6162656c3d666f6c6c6f772b40657863616c6964726177267374796c653d736f6369616c266c6f676f3d74776974746572"&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;div&gt;
    &lt;a href="https://excalidraw.com" rel="nofollow noopener noreferrer"&gt;
      &lt;img src="https://camo.githubusercontent.com/1be1106db66a9fbbe2a19cc027266ff647ecb7269d542e0712ff36d4a25c294a/68747470733a2f2f657863616c69647261772e6e7963332e63646e2e6469676974616c6f6365616e7370616365732e636f6d2f67697468756225324670726f647563745f73686f77636173652e706e67" alt="Product showcase"&gt;
    &lt;/a&gt;
    
      &lt;p&gt;
        Create beautiful hand-drawn like diagrams, wireframes, or whatever you like.
      &lt;/p&gt;
    
  
&lt;/div&gt;

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

&lt;/div&gt;

&lt;p&gt;The Excalidraw editor (npm package) supports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;💯 Free &amp;amp; open-source.&lt;/li&gt;
&lt;li&gt;🎨 Infinite, canvas-based whiteboard.&lt;/li&gt;
&lt;li&gt;✍️ Hand-drawn like style.&lt;/li&gt;
&lt;li&gt;🌓 Dark mode.&lt;/li&gt;
&lt;li&gt;🏗️ Customizable.&lt;/li&gt;
&lt;li&gt;📷 Image support.&lt;/li&gt;
&lt;li&gt;😀 Shape libraries support.&lt;/li&gt;
&lt;li&gt;🌐 Localization (i18n) support.&lt;/li&gt;
&lt;li&gt;🖼️ Export to PNG, SVG &amp;amp; clipboard.&lt;/li&gt;
&lt;li&gt;💾 Open format - export drawings as an &lt;code&gt;.excalidraw&lt;/code&gt; json file.&lt;/li&gt;
&lt;li&gt;⚒️ Wide range of tools - rectangle, circle, diamond, arrow, line, free-draw, eraser...&lt;/li&gt;
&lt;li&gt;➡️ Arrow-binding &amp;amp; labeled arrows.&lt;/li&gt;
&lt;li&gt;🔙 Undo / Redo.&lt;/li&gt;
&lt;li&gt;🔍 Zoom and panning support.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Excalidraw.com&lt;/h2&gt;

&lt;/div&gt;

&lt;p&gt;The app hosted at &lt;a href="https://excalidraw.com" rel="nofollow noopener noreferrer"&gt;excalidraw.com&lt;/a&gt; is a minimal showcase of what you can build with Excalidraw. Its &lt;a href="https://github.com/excalidraw/excalidraw/tree/master/excalidraw-app" rel="noopener noreferrer"&gt;source code&lt;/a&gt; is part of this repository as well, and the app features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;📡 PWA support (works offline).&lt;/li&gt;
&lt;li&gt;🤼 Real-time collaboration.&lt;/li&gt;
&lt;li&gt;🔒 End-to-end…&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/excalidraw/excalidraw" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;

&lt;h2&gt;
  
  
  10) Raycast — Remove friction from the machine itself
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What it is:&lt;/strong&gt; A keyboard-first launcher and automation layer for local developer workflows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it improves your project:&lt;/strong&gt; Not every project improvement lives in the repo. Faster local execution means less context-switching, fewer tiny interruptions, and more energy left for the work that actually matters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; macOS-based developers, snippets, quick scripts, search, clipboard workflows, window control, micro-automation.&lt;/p&gt;

&lt;p&gt;Raycast is one of those tools that quietly improves everything around your coding day. Opening the right folders, jumping to tickets, searching docs, running snippets, and triggering scripts faster sounds small until you notice how much time you stop wasting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Links:&lt;/strong&gt; &lt;a href="https://raycast.com/" rel="noopener noreferrer"&gt;Official&lt;/a&gt; · &lt;a href="https://github.com/raycast/extensions" rel="noopener noreferrer"&gt;GitHub&lt;/a&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/raycast" rel="noopener noreferrer"&gt;
        raycast
      &lt;/a&gt; / &lt;a href="https://github.com/raycast/extensions" rel="noopener noreferrer"&gt;
        extensions
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Everything you need to extend Raycast.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;p&gt;
  &lt;a rel="noopener noreferrer" href="https://github.com/raycast/extensions/images/store-logo.webp"&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%2Fraycast%2Fextensions%2FHEAD%2Fimages%2Fstore-logo.webp" height="128"&gt;&lt;/a&gt;
  &lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Raycast Extensions&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;
  &lt;a href="https://x.com/raycast" rel="nofollow noopener noreferrer"&gt;
    &lt;img alt="" src="https://camo.githubusercontent.com/c504b2ad6f5cac0a3da902b380b8b837a9c277b95d9a78717e6ae4c9466531b0/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f466f6c6c6f7725323040726179636173742d626c61636b2e7376673f7374796c653d666f722d7468652d6261646765266c6f676f3d58"&gt;
  &lt;/a&gt;
  &lt;a href="https://raycast.com/community" rel="nofollow noopener noreferrer"&gt;
    &lt;img alt="" src="https://camo.githubusercontent.com/4c6a01363e9db745bfd386e358718acc9ad706ce2511e16ea6328db7228aa58a/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4a6f696e253230746865253230636f6d6d756e6974792d626c61636b2e7376673f7374796c653d666f722d7468652d6261646765266c6f676f3d52617963617374266c6f676f436f6c6f723d666666"&gt;
  &lt;/a&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://raycast.com/" rel="nofollow noopener noreferrer"&gt;Raycast&lt;/a&gt; lets you control your tools with a few keystrokes. This repository contains all extensions that are available in the &lt;a href="https://raycast.com/store" rel="nofollow noopener noreferrer"&gt;Raycast Store&lt;/a&gt;. It also includes documentation and examples of how to extend Raycast using React.&lt;/p&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/raycast/extensions/images/header.webp"&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%2Fraycast%2Fextensions%2FHEAD%2Fimages%2Fheader.webp" alt="Header"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Getting Started&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;Visit &lt;a href="https://developers.raycast.com" rel="nofollow noopener noreferrer"&gt;https://developers.raycast.com&lt;/a&gt; to get started with our API. If you want to discover and install extensions, check out &lt;a href="https://raycast.com/store" rel="nofollow noopener noreferrer"&gt;our Store&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Be sure to read and follow our &lt;a href="https://manual.raycast.com/community-guidelines" rel="nofollow noopener noreferrer"&gt;Community&lt;/a&gt; and &lt;a href="https://manual.raycast.com/extensions" rel="nofollow noopener noreferrer"&gt;Extension&lt;/a&gt; guidelines and &lt;a href="https://www.raycast.com/aup" rel="nofollow noopener noreferrer"&gt;Acceptable Use Policy&lt;/a&gt; when submitting your extension and interacting with other folks in this repository.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Feedback&lt;/h2&gt;

&lt;/div&gt;
&lt;p&gt;Raycast wouldn't be where it is without the feedback from our community, so we would be happy to hear what you think of the API / DevX and how we can improve. Please use &lt;a href="https://github.com/raycast/extensions/issues/new/choose" rel="noopener noreferrer"&gt;GitHub issues&lt;/a&gt; for everything API related (bugs, improvements suggestions, developer experience, docs, etc). We have a few &lt;a href="https://developers.raycast.com/examples" rel="nofollow noopener noreferrer"&gt;templates&lt;/a&gt; that should help you get started.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Community&lt;/h2&gt;

&lt;/div&gt;
&lt;p&gt;Join our…&lt;/p&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/raycast/extensions" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fytknrcugcpceeb9l9wbj.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fytknrcugcpceeb9l9wbj.gif" alt="South Park developers staring at computer screens" width="384" height="270"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;What your laptop sees after you finally replace ten tiny daily interruptions with one keyboard-first workflow. Source: &lt;a href="https://giphy.com/gifs/southparkgifs-l3vRd3vZPrApPqzjq" rel="noopener noreferrer"&gt;South Park on Giphy&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;The projects people trust are rarely the ones with the flashiest feature list.&lt;/p&gt;

&lt;p&gt;They are the ones where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;deploys are repeatable&lt;/li&gt;
&lt;li&gt;APIs are easy to test&lt;/li&gt;
&lt;li&gt;user flows are protected&lt;/li&gt;
&lt;li&gt;production failures are visible&lt;/li&gt;
&lt;li&gt;docs are explicit&lt;/li&gt;
&lt;li&gt;uploads are treated like a real security boundary&lt;/li&gt;
&lt;li&gt;dependencies do not rot quietly&lt;/li&gt;
&lt;li&gt;ideas are clarified before code gets expensive&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is why good tooling matters so much.&lt;/p&gt;

&lt;p&gt;It does not just make developers faster.&lt;br&gt;
It makes projects better.&lt;/p&gt;

&lt;p&gt;If you could add only one of these to your stack this week, which one would make the biggest difference?&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>productivity</category>
      <category>devops</category>
    </item>
    <item>
      <title>7 Real Workflows That Actually Save Developers Hours</title>
      <dc:creator>Tommaso Bertocchi</dc:creator>
      <pubDate>Thu, 02 Apr 2026 13:50:09 +0000</pubDate>
      <link>https://forem.com/sonotommy/7-real-workflows-that-actually-save-developers-hours-5550</link>
      <guid>https://forem.com/sonotommy/7-real-workflows-that-actually-save-developers-hours-5550</guid>
      <description>&lt;p&gt;Most developers are still using AI like a novelty keyboard shortcut.&lt;/p&gt;

&lt;p&gt;They ask it for regexes, toy apps, or random refactors, then wonder why the payoff feels small.&lt;/p&gt;

&lt;p&gt;That is the wrong mental model.&lt;/p&gt;

&lt;p&gt;The real value is not "write code for me." The real value is "remove the friction around engineering work."&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmm44wl288uhglzor382a.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmm44wl288uhglzor382a.gif" alt="Developer typing hard at a home workstation" width="480" height="270"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;GIF source: &lt;a href="https://giphy.com/gifs/salesforce-bear-computer-work-from-home-1GEATImIxEXVR79Dhk" rel="noopener noreferrer"&gt;Salesforce on Giphy&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The code itself is often not the slow part. The slow part is turning messy human input into something the codebase can actually absorb.&lt;/p&gt;

&lt;p&gt;Bug reports are vague. Product requests are fuzzy. PRs hide edge cases. Logs are noisy. Meetings are chaos. Docs get written last. Migrations start with "should be fine" and end with regret.&lt;/p&gt;

&lt;p&gt;That is where AI earns its keep.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Stop using AI for one-off tricks and start using it for repeatable engineering workflows.&lt;/li&gt;
&lt;li&gt;The best workflows sit before coding or after coding, not just inside the editor.&lt;/li&gt;
&lt;li&gt;AI is great at turning messy input into structured output.&lt;/li&gt;
&lt;li&gt;Good outputs include test cases, checklists, review notes, implementation plans, and docs.&lt;/li&gt;
&lt;li&gt;You still own the judgment.&lt;/li&gt;
&lt;li&gt;You let the model handle the formatting, sorting, summarizing, and first-pass synthesis.&lt;/li&gt;
&lt;li&gt;AI stops feeling like a toy when it becomes part of your process.&lt;/li&gt;
&lt;/ul&gt;


&lt;div class="crayons-card c-embed"&gt;

  &lt;br&gt;
AI stops being impressive when you demo it.

&lt;p&gt;It starts being useful when it handles the annoying parts of engineering.&lt;br&gt;

&lt;/p&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  1. Turn rough bug reports into reproducible test cases
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The problem:&lt;/strong&gt; most bug reports are written like crime scene poetry.&lt;/p&gt;

&lt;p&gt;"Checkout is broken on mobile sometimes" is not a useful input. Neither is "I clicked around and it failed."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How AI helps:&lt;/strong&gt; give it the report, any stack trace, and a bit of context. Ask it to turn that mess into a reproducible path, a test matrix, and candidate assertions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Practical example:&lt;/strong&gt; a PM drops this into Slack:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;User says password reset fails after they open the email on iPhone and go back to the app.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is not ready for engineering. AI can turn it into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;likely repro conditions&lt;/li&gt;
&lt;li&gt;assumptions to verify&lt;/li&gt;
&lt;li&gt;exact steps to test&lt;/li&gt;
&lt;li&gt;likely layers involved&lt;/li&gt;
&lt;li&gt;candidate integration or E2E test cases
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Turn this bug report into a reproducible test plan.

Output:
- assumptions
- exact repro steps
- likely affected layers
- edge cases to test
- one candidate automated test

Bug report:
"User says password reset fails after they open the email on iPhone and go back to the app."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why it saves time:&lt;/strong&gt; you stop spending the first 30 minutes translating vague human language into engineer language.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Generate first-draft docs right after shipping a feature
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The problem:&lt;/strong&gt; docs are always "we'll do it after this PR lands." Then nobody does it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How AI helps:&lt;/strong&gt; after the feature ships, feed it the PR description, commit summary, config changes, and a couple of examples. Let it draft internal docs, release notes, or onboarding notes while the context is still fresh.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Practical example:&lt;/strong&gt; you shipped a webhook retry system. You already have the raw ingredients:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PR summary&lt;/li&gt;
&lt;li&gt;env vars added&lt;/li&gt;
&lt;li&gt;retry rules&lt;/li&gt;
&lt;li&gt;failure behavior&lt;/li&gt;
&lt;li&gt;sample payloads&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Turn that into a first draft immediately.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Turn these shipping notes into internal docs.

Output:
- what changed
- why it exists
- new config or env vars
- failure behavior
- examples
- what support/devs should know

Notes:
[paste PR description, commit summary, examples]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why it saves time:&lt;/strong&gt; blank-page writing is slow. Editing a decent first draft is fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Turn giant log dumps into structured debugging notes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The problem:&lt;/strong&gt; logs are full of repetition, noise, and timing confusion. You end up scrolling forever and still do not have a clean theory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How AI helps:&lt;/strong&gt; ask it to cluster repeated errors, reconstruct the timeline, and separate symptoms from likely root causes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Practical example:&lt;/strong&gt; you paste in request logs, app errors, and one queue worker trace. Instead of "look through this," you ask for structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Analyze these logs and turn them into debugging notes.

Output:
- timeline of events
- repeated errors grouped together
- likely root causes
- signals that may be red herrings
- next 5 checks to run

Logs:
[paste logs]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why it saves time:&lt;/strong&gt; the model is doing triage, not fixing production. That is exactly where it is useful.&lt;/p&gt;

&lt;p&gt;
  Optional deeper example
  &lt;br&gt;
Instead of rereading 400 lines of logs, ask for output in this shape:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Timeline&lt;/code&gt;: request received, cache miss, DB timeout, retry, downstream 502&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Recurring patterns&lt;/code&gt;: same tenant, same endpoint, same timeout window&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Most likely causes&lt;/code&gt;: connection pool exhaustion, retry storm, slow downstream dependency&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Checks to run now&lt;/code&gt;: inspect pool metrics, compare healthy tenant timings, sample failed request IDs, verify retry backoff, look for deploy correlation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That gives you a debugging worksheet instead of a wall of text.&lt;br&gt;
&lt;/p&gt;

&lt;br&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Extract action items from messy meetings or issue threads
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The problem:&lt;/strong&gt; a single GitHub issue or Slack thread can contain decisions, half-decisions, contradictions, and silent assumptions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How AI helps:&lt;/strong&gt; make it extract owners, blockers, decisions, and unanswered questions in a format your team can actually act on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Practical example:&lt;/strong&gt; after a product meeting, you have rough notes plus a noisy thread with frontend, backend, and design comments mixed together. Ask AI to convert it into a clean action list.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Read this meeting transcript and issue thread.

Extract:
- decisions made
- open questions
- blockers
- owners if identifiable
- next actions

Format it as a concise engineering handoff.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why it saves time:&lt;/strong&gt; you stop rewriting the same conversation into tickets by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Create migration checklists before touching code
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The problem:&lt;/strong&gt; upgrades fail because teams start with code changes instead of scope control.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How AI helps:&lt;/strong&gt; before you touch the repo, use AI to generate a migration checklist that covers dependency risks, config changes, affected app areas, test plan, rollout risks, and rollback points.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flfdgo4f9q9nxq3drughr.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flfdgo4f9q9nxq3drughr.gif" alt="Clippy-style debugging animation" width="480" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;GIF source: &lt;a href="https://giphy.com/gifs/MicrosoftCloud-msbuild-microsoft-build-IZ7R7Mok8NXzyGmlvE" rel="noopener noreferrer"&gt;Microsoft Cloud on Giphy&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Practical example:&lt;/strong&gt; you need to upgrade a framework, SDK, or ORM. Do not begin with "let's bump the version and see what breaks."&lt;/p&gt;

&lt;p&gt;Start here:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Create a migration checklist for this upgrade.

Context:
- current version: X
- target version: Y
- package list
- build/test scripts
- app patterns in use

Output:
- risky areas
- code patterns to audit
- config changes
- test plan
- rollout plan
- rollback plan
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why it saves time:&lt;/strong&gt; migrations get expensive when you discover risk too late. A checklist is cheaper than a surprise.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Review PRs for edge cases and missing tests
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The problem:&lt;/strong&gt; most AI PR reviews are too polite and too shallow. "Looks good" is useless.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How AI helps:&lt;/strong&gt; give it a diff and ask it to act like a skeptical reviewer. Not a cheerleader. Not a linter. A reviewer looking for behavioral gaps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Practical example:&lt;/strong&gt; a PR claims to "fix duplicate invoice sending." AI can inspect the patch and ask the questions a rushed human reviewer might miss:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what happens on retries?&lt;/li&gt;
&lt;li&gt;what if the process crashes after write but before send?&lt;/li&gt;
&lt;li&gt;what if two workers race?&lt;/li&gt;
&lt;li&gt;where is the regression test?
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Review this PR diff like a skeptical staff engineer.

Focus on:
- edge cases
- behavior changes not mentioned in the title
- missing tests
- rollback risk
- race conditions
- failure states

Diff:
[paste diff or summary]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why it saves time:&lt;/strong&gt; it gives you a second set of eyes on the logic, not just the syntax.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Turn vague product requests into implementation plans
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The problem:&lt;/strong&gt; product asks are often written at the wrong altitude. Too vague to estimate. Too specific to question. Dangerous combination.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How AI helps:&lt;/strong&gt; use it to turn the request into an engineering plan with assumptions, scope boundaries, data changes, states, risks, and open questions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Practical example:&lt;/strong&gt; "We need users to pause subscriptions temporarily."&lt;/p&gt;

&lt;p&gt;That sounds simple until you ask the obvious follow-ups:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;billing implications?&lt;/li&gt;
&lt;li&gt;proration?&lt;/li&gt;
&lt;li&gt;expiration?&lt;/li&gt;
&lt;li&gt;admin override?&lt;/li&gt;
&lt;li&gt;analytics?&lt;/li&gt;
&lt;li&gt;email flows?&lt;/li&gt;
&lt;li&gt;API contract?&lt;/li&gt;
&lt;li&gt;mobile states?&lt;/li&gt;
&lt;li&gt;edge case when payment fails during resume?
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Turn this product request into an implementation plan.

Output:
- assumptions
- open questions
- API/data model changes
- frontend states
- edge cases
- telemetry
- smallest shippable version
- risks

Request:
"We need users to pause subscriptions temporarily."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why it saves time:&lt;/strong&gt; you walk into planning with something sharper than vibes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Toy Use vs Real Workflow
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Toy use&lt;/th&gt;
&lt;th&gt;Real workflow&lt;/th&gt;
&lt;th&gt;Why the workflow wins&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;"Build me a Snake game"&lt;/td&gt;
&lt;td&gt;Turn a product request into an implementation plan&lt;/td&gt;
&lt;td&gt;It reduces ambiguity before code starts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Write a regex for me"&lt;/td&gt;
&lt;td&gt;Turn a vague bug report into a test case plan&lt;/td&gt;
&lt;td&gt;It helps you reproduce and verify real bugs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Summarize this file"&lt;/td&gt;
&lt;td&gt;Review a PR for missing tests and edge cases&lt;/td&gt;
&lt;td&gt;It finds risk, not just words&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Explain Docker like I'm five"&lt;/td&gt;
&lt;td&gt;Create a migration checklist before an upgrade&lt;/td&gt;
&lt;td&gt;It prevents expensive mistakes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"What does this stack trace mean?"&lt;/td&gt;
&lt;td&gt;Convert logs into structured debugging notes&lt;/td&gt;
&lt;td&gt;It gives you a path to investigate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Write release notes"&lt;/td&gt;
&lt;td&gt;Generate docs right after shipping from real changes&lt;/td&gt;
&lt;td&gt;It captures context before it disappears&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Brainstorm app ideas"&lt;/td&gt;
&lt;td&gt;Extract action items from meetings and issue threads&lt;/td&gt;
&lt;td&gt;It turns conversation into execution&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What Changed for Me
&lt;/h2&gt;

&lt;p&gt;I stopped asking AI for brilliance.&lt;/p&gt;

&lt;p&gt;I started asking it for leverage.&lt;/p&gt;

&lt;p&gt;That changed everything.&lt;/p&gt;

&lt;p&gt;I use it less like a genius intern and more like a formatting engine for engineering work. It is great at turning messy input into clean output:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;vague report into test plan&lt;/li&gt;
&lt;li&gt;raw logs into debugging notes&lt;/li&gt;
&lt;li&gt;shipped code into docs&lt;/li&gt;
&lt;li&gt;discussion into actions&lt;/li&gt;
&lt;li&gt;request into scope&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the pattern.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI is not most useful in the middle of coding. It is often most useful at the handoff points around coding.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is this just ChatGPT with better prompts?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not really. The important part is not the prompt text. It is the workflow shape. You are feeding AI messy inputs that already exist in your day and asking for structured outputs you actually use.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Should AI write production code too?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Sometimes, yes. But that is not the biggest unlocked value for most teams. Code generation gets attention. Workflow acceleration saves time more reliably.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What about privacy and security?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Use your brain. Do not paste secrets, private customer data, or sensitive production material into tools that are not approved for it. Redact first. Use the right environment. Treat AI like any other external system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Won't this create more cleanup work?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Only if you ask for finished answers when you really need first drafts. The trick is to use AI for scaffolding, synthesis, and structure. You do the final judgment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;A lot of developers are disappointed by AI because they are using it for low-stakes parlor tricks.&lt;/p&gt;

&lt;p&gt;The better use is boring on purpose.&lt;/p&gt;

&lt;p&gt;It helps with the stuff that slows real work down: clarifying, organizing, checking, summarizing, planning, and documenting.&lt;/p&gt;

&lt;p&gt;That is how it stops being a toy.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/new" class="crayons-btn crayons-btn--primary"&gt;What workflow saves you the most time? Share it in the comments.&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;If you already have a workflow like this, I want to hear it. And if you think one of these is overrated, even better. Those are usually the best comment sections.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>programming</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Most file upload security in Node.js is still just extension checks. That’s not enough. Pompelmi scans uploads before storage for MIME spoofing, risky archives, suspicious structures, and optional YARA. OSS, MIT. GitHub https://github.com/pompelmi/pompelmi</title>
      <dc:creator>Tommaso Bertocchi</dc:creator>
      <pubDate>Thu, 02 Apr 2026 07:54:59 +0000</pubDate>
      <link>https://forem.com/sonotommy/most-file-upload-security-in-nodejs-is-still-just-extension-checks-thats-not-enough-pompelmi-l9d</link>
      <guid>https://forem.com/sonotommy/most-file-upload-security-in-nodejs-is-still-just-extension-checks-thats-not-enough-pompelmi-l9d</guid>
      <description>&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://github.com/pompelmi/pompelmi" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Frepository-images.githubusercontent.com%2F1026124833%2Fc593196f-8d6f-401a-87cd-8387d5fd0c14" height="640" class="m-0" width="1280"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://github.com/pompelmi/pompelmi" rel="noopener noreferrer" class="c-link"&gt;
            GitHub - pompelmi/pompelmi: Open-source file upload security for Node.js. Scan files before storage to detect malware, MIME spoofing, and risky archives. · GitHub
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Open-source file upload security for Node.js. Scan files before storage to detect malware, MIME spoofing, and risky archives. - pompelmi/pompelmi
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.githubassets.com%2Ffavicons%2Ffavicon.svg" width="32" height="32"&gt;
          github.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


</description>
      <category>javascript</category>
      <category>node</category>
      <category>opensource</category>
      <category>security</category>
    </item>
    <item>
      <title>How I promoted my open-source security repo to 575 GitHub stars by treating it like a real product</title>
      <dc:creator>Tommaso Bertocchi</dc:creator>
      <pubDate>Mon, 30 Mar 2026 10:37:49 +0000</pubDate>
      <link>https://forem.com/sonotommy/how-i-promoted-my-open-source-security-repo-to-575-github-stars-by-treating-it-like-a-real-product-6b9</link>
      <guid>https://forem.com/sonotommy/how-i-promoted-my-open-source-security-repo-to-575-github-stars-by-treating-it-like-a-real-product-6b9</guid>
      <description>&lt;p&gt;When people talk about growing an open-source project, the advice usually sounds vague:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;post on social media&lt;/li&gt;
&lt;li&gt;share it on Reddit&lt;/li&gt;
&lt;li&gt;write blog posts&lt;/li&gt;
&lt;li&gt;keep shipping&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That advice is not wrong, but it is incomplete.&lt;/p&gt;

&lt;p&gt;What worked for me with &lt;a href="https://github.com/pompelmi/pompelmi" rel="noopener noreferrer"&gt;Pompelmi&lt;/a&gt; was not “being everywhere.”&lt;br&gt;
It was making the project easy to understand, easy to trust, and easy to discover in places where the right developers were already looking.&lt;/p&gt;

&lt;p&gt;Pompelmi is an open-source file upload security tool for Node.js. It scans files before storage to help detect malware, MIME spoofing, risky archives, and other upload-related problems. It works with frameworks like Express, Next.js, NestJS, Fastify, and Koa.&lt;/p&gt;

&lt;p&gt;At the time of writing, the repo has grown to hundreds of stars and picked up mentions from places like Stack Overflow, Help Net Security, Node Weekly, Detection Engineering Weekly, and Bytes.&lt;/p&gt;

&lt;p&gt;I did not get there with paid ads.&lt;br&gt;
I did not get there with a huge audience.&lt;br&gt;
And I definitely did not get there from one viral post.&lt;/p&gt;

&lt;p&gt;I got there by treating an open-source repo like a product that deserved positioning, packaging, distribution, and trust.&lt;/p&gt;

&lt;p&gt;Here is exactly how I approached it.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. I built around a painful problem that is easy to explain
&lt;/h2&gt;

&lt;p&gt;A lot of open-source projects struggle because they are technically interesting but hard to describe in one sentence.&lt;/p&gt;

&lt;p&gt;Pompelmi was different.&lt;/p&gt;

&lt;p&gt;The core message is simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Your file upload endpoint is part of your attack surface.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is something backend and security developers immediately understand.&lt;br&gt;
A surprising number of apps still treat file uploads as a basic “accept file, save file” workflow, when in reality uploads can introduce malware, MIME spoofing, archive abuse, and downstream processing risks.&lt;/p&gt;

&lt;p&gt;The project became easier to promote once I stopped describing it as a collection of scanning features and started describing it as a clear security layer for file uploads.&lt;/p&gt;

&lt;p&gt;That positioning matters a lot.&lt;br&gt;
If your project solves a problem people already worry about, distribution gets much easier.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. I made the GitHub repo do the selling for me
&lt;/h2&gt;

&lt;p&gt;A lot of traffic is wasted because people land on a repo and still do not understand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what the project does&lt;/li&gt;
&lt;li&gt;who it is for&lt;/li&gt;
&lt;li&gt;why it matters&lt;/li&gt;
&lt;li&gt;how to try it quickly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I tried to make the repository itself act like a landing page.&lt;/p&gt;

&lt;p&gt;That meant focusing on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a clear headline&lt;/li&gt;
&lt;li&gt;a sharp one-line value proposition&lt;/li&gt;
&lt;li&gt;framework-specific examples&lt;/li&gt;
&lt;li&gt;visible badges and trust signals&lt;/li&gt;
&lt;li&gt;a social preview image&lt;/li&gt;
&lt;li&gt;relevant GitHub topics&lt;/li&gt;
&lt;li&gt;mentions from recognizable sources&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In my experience, stars usually come &lt;strong&gt;after comprehension&lt;/strong&gt;.&lt;br&gt;
If someone has to read too much to understand the value, conversion drops.&lt;/p&gt;

&lt;p&gt;A good open-source repo should answer this in seconds:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Why should I care about this right now?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If it cannot, growth gets harder no matter how good the code is.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. I stopped thinking only about “promotion” and focused on distribution channels with intent
&lt;/h2&gt;

&lt;p&gt;Not all traffic is equal.&lt;/p&gt;

&lt;p&gt;A random viral post can bring views.&lt;br&gt;
A highly relevant placement can bring users, stars, issues, contributors, and long-term trust.&lt;/p&gt;

&lt;p&gt;For Pompelmi, the best distribution channels were not generic “look at my repo” posts.&lt;br&gt;
They were places where developers were already thinking about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;file uploads&lt;/li&gt;
&lt;li&gt;backend frameworks&lt;/li&gt;
&lt;li&gt;Node.js security&lt;/li&gt;
&lt;li&gt;web application security&lt;/li&gt;
&lt;li&gt;malware scanning&lt;/li&gt;
&lt;li&gt;upload validation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That changed how I spent my time.&lt;/p&gt;

&lt;p&gt;Instead of chasing broad attention, I focused on channels where Pompelmi was obviously useful.&lt;/p&gt;

&lt;p&gt;That included:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;framework ecosystem pages and docs&lt;/li&gt;
&lt;li&gt;developer newsletters&lt;/li&gt;
&lt;li&gt;practical tutorials&lt;/li&gt;
&lt;li&gt;curated lists&lt;/li&gt;
&lt;li&gt;security-focused publications&lt;/li&gt;
&lt;li&gt;posts that answer real implementation questions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This was slower than hype marketing, but much more durable.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. I wrote for existing ecosystems instead of waiting for people to “find” the project
&lt;/h2&gt;

&lt;p&gt;One of the best things I did was to move closer to where developers already work.&lt;/p&gt;

&lt;p&gt;If someone is building with Fastify, Next.js, Express, Koa, or another Node.js framework, they are much more likely to adopt a tool when they discover it in a context they already trust.&lt;/p&gt;

&lt;p&gt;That is why ecosystem visibility matters so much.&lt;/p&gt;

&lt;p&gt;For an open-source dev tool, this can mean:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;integration docs&lt;/li&gt;
&lt;li&gt;example apps&lt;/li&gt;
&lt;li&gt;framework-specific packages&lt;/li&gt;
&lt;li&gt;PRs to community pages&lt;/li&gt;
&lt;li&gt;entries in ecosystem lists&lt;/li&gt;
&lt;li&gt;guides that show the tool inside real workflows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is more effective than generic promotion because it reduces friction.&lt;br&gt;
The user no longer has to imagine how your project fits into their stack.&lt;br&gt;
You already showed them.&lt;/p&gt;

&lt;p&gt;That has been especially important for Pompelmi, because it is not a standalone product people browse for fun. It is infrastructure. It has to feel practical, easy to slot in, and low-risk to try.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. I treated every mention as leverage, not as a one-day win
&lt;/h2&gt;

&lt;p&gt;One mistake I see often is that a project gets featured somewhere and the maintainer celebrates for 24 hours, then moves on.&lt;/p&gt;

&lt;p&gt;A better approach is to turn every mention into a permanent trust asset.&lt;/p&gt;

&lt;p&gt;Once Pompelmi started getting mentioned by recognizable publications and newsletters, I made sure to surface that credibility.&lt;/p&gt;

&lt;p&gt;Why this matters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it reassures first-time visitors&lt;/li&gt;
&lt;li&gt;it increases perceived legitimacy&lt;/li&gt;
&lt;li&gt;it gives future outreach more credibility&lt;/li&gt;
&lt;li&gt;it compounds over time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When a new reader sees that your project has already been noticed by respected people or publications, they are more likely to take it seriously.&lt;/p&gt;

&lt;p&gt;Trust is one of the biggest bottlenecks in open-source growth, especially in security.&lt;br&gt;
If your project touches something sensitive like file scanning, people want reasons to believe it is real, maintained, and worth trying.&lt;/p&gt;

&lt;p&gt;Mentions help with that.&lt;br&gt;
But only if you actually use them well.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. I wrote articles that create curiosity first, then route people into the repo
&lt;/h2&gt;

&lt;p&gt;Another thing that helped a lot was writing content that was broader than the project itself.&lt;/p&gt;

&lt;p&gt;If every post is “here is my repo,” people tune out.&lt;/p&gt;

&lt;p&gt;But if the post is genuinely useful on its own, it can attract a wider audience and still send qualified traffic back to your work.&lt;/p&gt;

&lt;p&gt;For example, list-style articles can work surprisingly well when done right.&lt;br&gt;
Not because they are magical, but because they have built-in clarity:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;readers know what they are getting&lt;/li&gt;
&lt;li&gt;they are easy to skim&lt;/li&gt;
&lt;li&gt;they create curiosity&lt;/li&gt;
&lt;li&gt;they naturally expose multiple projects and ideas&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is especially useful on developer platforms where attention is competitive.&lt;/p&gt;

&lt;p&gt;For Pompelmi, I found it better to mix different types of content:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;practical tutorials&lt;/li&gt;
&lt;li&gt;list articles&lt;/li&gt;
&lt;li&gt;launch/update posts&lt;/li&gt;
&lt;li&gt;ecosystem-specific examples&lt;/li&gt;
&lt;li&gt;opinionated posts about a real problem&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A tutorial can bring search traffic.&lt;br&gt;
A list article can bring discovery.&lt;br&gt;
An update post can re-activate existing followers.&lt;br&gt;
Together, they create a loop.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. I learned that consistency beats one big launch
&lt;/h2&gt;

&lt;p&gt;A lot of people want one giant post that changes everything.&lt;br&gt;
Sometimes that happens.&lt;br&gt;
Usually it does not.&lt;/p&gt;

&lt;p&gt;What usually works better is repeated exposure across relevant channels.&lt;/p&gt;

&lt;p&gt;For an open-source project, that can look like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;shipping releases consistently&lt;/li&gt;
&lt;li&gt;improving docs regularly&lt;/li&gt;
&lt;li&gt;posting new examples&lt;/li&gt;
&lt;li&gt;publishing tutorials&lt;/li&gt;
&lt;li&gt;sharing integration updates&lt;/li&gt;
&lt;li&gt;reaching out to relevant newsletters or blogs&lt;/li&gt;
&lt;li&gt;submitting to framework/community pages&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each action on its own may look small.&lt;br&gt;
Together, they create the feeling that the project is alive.&lt;/p&gt;

&lt;p&gt;That feeling matters.&lt;br&gt;
Developers do not just star code. They star momentum.&lt;/p&gt;

&lt;p&gt;A maintained repo with fresh releases, better docs, ecosystem integrations, and outside mentions feels safer to trust than a clever repo that looks abandoned.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. I made the project easier to adopt, not just easier to admire
&lt;/h2&gt;

&lt;p&gt;A star is nice, but the things that usually create long-term growth are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;usage&lt;/li&gt;
&lt;li&gt;word of mouth&lt;/li&gt;
&lt;li&gt;references&lt;/li&gt;
&lt;li&gt;integration into real apps&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That means adoption matters more than aesthetics alone.&lt;/p&gt;

&lt;p&gt;So beyond promotion, I tried to reduce practical friction:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;make the README clearer&lt;/li&gt;
&lt;li&gt;provide examples&lt;/li&gt;
&lt;li&gt;support popular Node.js frameworks&lt;/li&gt;
&lt;li&gt;keep the messaging concrete&lt;/li&gt;
&lt;li&gt;show the exact use case quickly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is especially important for technical tools.&lt;/p&gt;

&lt;p&gt;If someone sees a repo and thinks “cool project,” that is nice.&lt;br&gt;
If they think “I can add this to my upload pipeline today,” that is much better.&lt;/p&gt;

&lt;p&gt;The second reaction is what creates compounding growth.&lt;/p&gt;




&lt;h2&gt;
  
  
  9. I leaned into credibility because security projects need it more than most
&lt;/h2&gt;

&lt;p&gt;Security tooling has a higher trust bar than a casual utility library.&lt;/p&gt;

&lt;p&gt;If your project claims to make file uploads safer, people naturally ask themselves:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;is this maintained?&lt;/li&gt;
&lt;li&gt;is this serious?&lt;/li&gt;
&lt;li&gt;is this just marketing?&lt;/li&gt;
&lt;li&gt;who is using it?&lt;/li&gt;
&lt;li&gt;has anyone reputable noticed it?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That means credibility is part of distribution.&lt;br&gt;
Not just code quality.&lt;/p&gt;

&lt;p&gt;For me, credibility came from a mix of things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;consistent releases&lt;/li&gt;
&lt;li&gt;clear scope&lt;/li&gt;
&lt;li&gt;practical documentation&lt;/li&gt;
&lt;li&gt;presence in real developer ecosystems&lt;/li&gt;
&lt;li&gt;recognition from publications and newsletters&lt;/li&gt;
&lt;li&gt;visible maintenance&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The more serious the problem you are solving, the more important this becomes.&lt;/p&gt;




&lt;h2&gt;
  
  
  10. What I would tell someone trying to grow an open-source repo today
&lt;/h2&gt;

&lt;p&gt;If I had to summarize the whole strategy in a few points, it would be this:&lt;/p&gt;

&lt;h3&gt;
  
  
  Build something with a sharp problem statement
&lt;/h3&gt;

&lt;p&gt;If people cannot repeat what your project does in one sentence, growth will be harder.&lt;/p&gt;

&lt;h3&gt;
  
  
  Treat the repo like a product page
&lt;/h3&gt;

&lt;p&gt;Your README, social preview, examples, and topics all matter.&lt;/p&gt;

&lt;h3&gt;
  
  
  Go where intent already exists
&lt;/h3&gt;

&lt;p&gt;Ecosystem pages, newsletters, practical searches, tutorials, and framework communities are better than random exposure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Turn every small win into a trust multiplier
&lt;/h3&gt;

&lt;p&gt;A mention, a PR merge, a tutorial, a release, an integration: each one should strengthen the next.&lt;/p&gt;

&lt;h3&gt;
  
  
  Focus on repeated relevance
&lt;/h3&gt;

&lt;p&gt;One post can help. A system of useful distribution points helps more.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I would do next for Pompelmi
&lt;/h2&gt;

&lt;p&gt;If I were pushing the project even harder from here, I would double down on five things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;More framework-specific tutorials&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Not just “what Pompelmi is,” but “how to secure uploads in Express / Next.js / Fastify.”&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;More ecosystem placements&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Community pages, official docs, plugin directories, curated security lists.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;More comparison-driven content&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
For example: why file type validation is not enough, or why extension checks fail.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;More proof-driven trust&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Mentions, examples, case studies, and implementation patterns.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;More discoverable educational content&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Articles that solve the reader’s problem first, then naturally introduce the repo.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That is the real lesson I learned from promoting an open-source project:&lt;/p&gt;

&lt;p&gt;Growth is rarely about shouting louder.&lt;br&gt;
It is usually about making the project easier to understand, easier to trust, and easier to encounter in the right places.&lt;/p&gt;

&lt;p&gt;And once that starts working, every new article, PR, mention, release, and integration becomes part of the same flywheel.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final thought
&lt;/h2&gt;

&lt;p&gt;A lot of people think open source “speaks for itself.”&lt;br&gt;
Sometimes it does.&lt;br&gt;
Usually it needs help.&lt;/p&gt;

&lt;p&gt;If you built something genuinely useful, promoting it is not fake and it is not selfish.&lt;br&gt;
It is part of the work.&lt;/p&gt;

&lt;p&gt;That was the biggest shift for me.&lt;/p&gt;

&lt;p&gt;I stopped thinking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“How do I get people to notice my repo?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And started thinking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“How do I make this project impossible to ignore for the specific developers who need it?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That mindset made everything else easier.&lt;/p&gt;

&lt;p&gt;If you want to check out Pompelmi, the repo is here:&lt;/p&gt;

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

</description>
      <category>marketing</category>
      <category>opensource</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to Secure File Uploads in Next.js</title>
      <dc:creator>Tommaso Bertocchi</dc:creator>
      <pubDate>Mon, 30 Mar 2026 08:14:09 +0000</pubDate>
      <link>https://forem.com/sonotommy/how-to-secure-file-uploads-in-nextjs-ema</link>
      <guid>https://forem.com/sonotommy/how-to-secure-file-uploads-in-nextjs-ema</guid>
      <description>&lt;p&gt;File uploads are one of those features that look simple until you think about the security side.&lt;/p&gt;

&lt;p&gt;A user uploads a file, your route accepts it, and everything seems fine.&lt;/p&gt;

&lt;p&gt;But what if that file is not what it claims to be?&lt;/p&gt;

&lt;p&gt;A renamed executable can look like a harmless PDF. A ZIP archive can hide traversal tricks or resource-exhaustion problems. A document can carry risky structures you would never catch by checking only the filename extension.&lt;/p&gt;

&lt;p&gt;That is why upload endpoints are part of your attack surface.&lt;/p&gt;

&lt;p&gt;In this article, I will show you how to secure file uploads in a &lt;strong&gt;Next.js App Router&lt;/strong&gt; application by scanning them &lt;strong&gt;before storage&lt;/strong&gt; with &lt;a href="https://github.com/pompelmi/pompelmi" rel="noopener noreferrer"&gt;pompelmi&lt;/a&gt;, an open-source upload security tool for Node.js.&lt;/p&gt;

&lt;p&gt;We will build a minimal example that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;accepts a file from a browser form&lt;/li&gt;
&lt;li&gt;scans it on the server&lt;/li&gt;
&lt;li&gt;returns a verdict&lt;/li&gt;
&lt;li&gt;blocks suspicious or malicious uploads before they reach storage&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why file upload validation is usually too weak
&lt;/h2&gt;

&lt;p&gt;A lot of upload handlers still rely on checks like these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;file.name.endsWith('.pdf')&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;file.type === 'application/pdf'&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;maximum size limits only&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those checks are still useful, but they are not enough.&lt;/p&gt;

&lt;p&gt;The browser-provided MIME type can be wrong or misleading. The filename can be changed by the client. And some dangerous files look harmless until you inspect the actual bytes or the archive structure.&lt;/p&gt;

&lt;p&gt;A safer model is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;receive the upload&lt;/li&gt;
&lt;li&gt;inspect the file in-process&lt;/li&gt;
&lt;li&gt;decide whether to allow, quarantine, or reject it&lt;/li&gt;
&lt;li&gt;store only what passed the gate&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That is exactly the role Pompelmi is designed for.&lt;/p&gt;




&lt;h2&gt;
  
  
  What we are going to build
&lt;/h2&gt;

&lt;p&gt;We will use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Next.js App Router&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;@pompelmi/next-upload&lt;/code&gt;&lt;/strong&gt; for the upload route&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;pompelmi&lt;/code&gt;&lt;/strong&gt; for the scanning logic and policy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At the end, you will have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;app/api/upload/route.ts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;lib/security.ts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;app/page.tsx&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Install the packages
&lt;/h2&gt;

&lt;p&gt;Inside your Next.js project, install the core package and the Next.js adapter:&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;pompelmi @pompelmi/next-upload
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pompelmi requires &lt;strong&gt;Node.js 18+&lt;/strong&gt;, and for Next.js uploads you should use the &lt;strong&gt;Node runtime&lt;/strong&gt;, not the Edge runtime.&lt;/p&gt;




&lt;h2&gt;
  
  
  Create the security config
&lt;/h2&gt;

&lt;p&gt;Create &lt;code&gt;lib/security.ts&lt;/code&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;CommonHeuristicsScanner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;composeScanners&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;createZipBombGuard&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;pompelmi&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;policy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;includeExtensions&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;pdf&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;png&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;jpg&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;jpeg&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;zip&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;txt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;allowedMimeTypes&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;application/pdf&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;image/png&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;image/jpeg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/zip&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;text/plain&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;maxFileSizeBytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;timeoutMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;concurrency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;failClosed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scanner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;composeScanners&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zipGuard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nf"&gt;createZipBombGuard&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;maxEntries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;512&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;maxTotalUncompressedBytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;maxCompressionRatio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;heuristics&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;CommonHeuristicsScanner&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="c1"&gt;// ['yara', YourYaraScanner],&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;parallel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;stopOn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;suspicious&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;timeoutMsPerScanner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;tagSourceName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What this does
&lt;/h3&gt;

&lt;p&gt;This setup gives you a practical upload gate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;includeExtensions&lt;/code&gt; limits which file extensions you accept&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;allowedMimeTypes&lt;/code&gt; defines which MIME types are allowed&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;maxFileSizeBytes&lt;/code&gt; blocks very large files early&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;failClosed: true&lt;/code&gt; makes the route safer if scanning fails or times out&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;createZipBombGuard(...)&lt;/code&gt; adds protection against hostile ZIP archives&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CommonHeuristicsScanner&lt;/code&gt; adds built-in heuristic checks&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;composeScanners(...)&lt;/code&gt; lets you combine multiple scanners into one pipeline&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can keep this small at first and harden it later.&lt;/p&gt;




&lt;h2&gt;
  
  
  Create the upload route
&lt;/h2&gt;

&lt;p&gt;Now create &lt;code&gt;app/api/upload/route.ts&lt;/code&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;createNextUploadHandler&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;@pompelmi/next-upload&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;scanner&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;@/lib/security&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;runtime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;nodejs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dynamic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;force-dynamic&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;POST&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createNextUploadHandler&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;scanner&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;That is the core of the integration.&lt;/p&gt;

&lt;p&gt;The important part is this line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;POST&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createNextUploadHandler&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;scanner&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 creates a ready-to-use upload handler for your App Router route. It applies your policy and scanner before you decide to persist or process the file elsewhere.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why &lt;code&gt;runtime = 'nodejs'&lt;/code&gt; matters
&lt;/h3&gt;

&lt;p&gt;The route must run in the Node.js runtime because file inspection is a server-side concern and the package is built for Node.js upload flows.&lt;/p&gt;




&lt;h2&gt;
  
  
  Create a minimal upload page
&lt;/h2&gt;

&lt;p&gt;Now create &lt;code&gt;app/page.tsx&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;HomePage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setResult&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setLoading&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&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;handleSubmit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FormEvent&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLFormElement&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&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;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentTarget&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;input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;elements&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;namedItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;file&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;HTMLInputElement&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&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;formData&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;FormData&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;file&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

    &lt;span class="nf"&gt;setLoading&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;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;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;/api/upload&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;method&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&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;formData&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;response&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;setResult&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;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="nf"&gt;setResult&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Request failed&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="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;setLoading&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&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="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;maxWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;720&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;40px auto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;fontFamily&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sans-serif&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Secure file upload demo&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Upload a file and let the server inspect it before storage.&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt; &lt;span class="na"&gt;onSubmit&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleSubmit&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"file"&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"file"&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt; &lt;span class="na"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;loading&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;marginLeft&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;loading&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Scanning...&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;Upload&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;pre&lt;/span&gt;
          &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;marginTop&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1px solid #ddd&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;borderRadius&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="na"&gt;overflowX&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;auto&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="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="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="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;pre&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is intentionally simple.&lt;/p&gt;

&lt;p&gt;It sends one file as &lt;code&gt;multipart/form-data&lt;/code&gt; to &lt;code&gt;/api/upload&lt;/code&gt; and prints the JSON response from the server.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the response looks like
&lt;/h2&gt;

&lt;p&gt;The exact shape can vary depending on your policy and scanner setup, but the important part is the &lt;strong&gt;verdict&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Typical verdicts are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;clean&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;suspicious&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;malicious&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, a blocked upload might look conceptually like this:&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;"ok"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Upload blocked"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"verdict"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"suspicious"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reasons"&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="s2"&gt;"MIME mismatch detected"&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;A clean upload may return something like:&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;"ok"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"verdict"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"clean"&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;The key idea is that your application gets a policy decision at the upload boundary instead of blindly trusting the file.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where to store the file
&lt;/h2&gt;

&lt;p&gt;A common mistake is to store the file first and scan it later.&lt;/p&gt;

&lt;p&gt;A safer pattern is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;receive upload&lt;/li&gt;
&lt;li&gt;scan it immediately&lt;/li&gt;
&lt;li&gt;only then write it to disk, object storage, or downstream processing&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That way, unsafe files do not quietly enter the rest of your system.&lt;/p&gt;

&lt;p&gt;In real apps, you would typically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;scan in the route&lt;/li&gt;
&lt;li&gt;store only if the verdict is &lt;code&gt;clean&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;log blocked attempts&lt;/li&gt;
&lt;li&gt;surface a generic error to the client&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Hardening ideas for production
&lt;/h2&gt;

&lt;p&gt;This example is intentionally minimal, but here are the next things I would tighten in production.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Narrow the allowlist
&lt;/h3&gt;

&lt;p&gt;Do not accept more file types than you actually need.&lt;/p&gt;

&lt;p&gt;If your product only needs PDFs and PNGs, do not allow ZIP, TXT, or JPEG just because it is convenient.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Keep file size limits strict
&lt;/h3&gt;

&lt;p&gt;Large uploads increase cost and risk.&lt;/p&gt;

&lt;p&gt;Set the smallest &lt;code&gt;maxFileSizeBytes&lt;/code&gt; that still works for your business case.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Fail closed
&lt;/h3&gt;

&lt;p&gt;If scanning times out or a scanner crashes, the safe default is to reject the upload.&lt;/p&gt;

&lt;p&gt;That is why &lt;code&gt;failClosed: true&lt;/code&gt; is such a useful default for public-facing uploads.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Add signature scanning later if needed
&lt;/h3&gt;

&lt;p&gt;The example above uses ZIP guards and heuristics. If your threat model requires deeper detection, you can extend the scanner pipeline with your own YARA-based scanner.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Keep upload routes on the server only
&lt;/h3&gt;

&lt;p&gt;Do not move this logic into the client. The browser can help with UX, but the security decision belongs to the server.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why this approach fits Next.js well
&lt;/h2&gt;

&lt;p&gt;One thing I like about this setup is that it matches the way modern Next.js apps are built.&lt;/p&gt;

&lt;p&gt;You do not need a separate scanning service just to get started.&lt;/p&gt;

&lt;p&gt;You define:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one server route&lt;/li&gt;
&lt;li&gt;one policy object&lt;/li&gt;
&lt;li&gt;one scanner pipeline&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And you can iterate from there.&lt;/p&gt;

&lt;p&gt;That makes it practical for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;internal tools&lt;/li&gt;
&lt;li&gt;SaaS dashboards&lt;/li&gt;
&lt;li&gt;file submission forms&lt;/li&gt;
&lt;li&gt;CMS-like workflows&lt;/li&gt;
&lt;li&gt;apps that accept PDFs, images, or ZIP uploads&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;If your application accepts file uploads, security should not start after the file has already been stored.&lt;/p&gt;

&lt;p&gt;It should start at the upload gate.&lt;/p&gt;

&lt;p&gt;With Next.js App Router and Pompelmi, you can add that decision point with a small amount of code and a much safer default.&lt;/p&gt;

&lt;p&gt;The idea is simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;inspect first&lt;/li&gt;
&lt;li&gt;store later&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want, I can also write follow-up articles such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;How to secure file uploads in NestJS&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;How to secure file uploads in Fastify&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Magic bytes vs MIME type for file uploads&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;How to protect against ZIP bombs in Node.js&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Full example recap
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;lib/security.ts&lt;/code&gt;
&lt;/h3&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;CommonHeuristicsScanner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;composeScanners&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;createZipBombGuard&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;pompelmi&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;policy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;includeExtensions&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;pdf&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;png&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;jpg&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;jpeg&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;zip&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;txt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;allowedMimeTypes&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;application/pdf&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;image/png&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;image/jpeg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/zip&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;text/plain&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;maxFileSizeBytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;timeoutMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;concurrency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;failClosed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scanner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;composeScanners&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zipGuard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nf"&gt;createZipBombGuard&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;maxEntries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;512&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;maxTotalUncompressedBytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;maxCompressionRatio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;heuristics&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;CommonHeuristicsScanner&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;parallel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;stopOn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;suspicious&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;timeoutMsPerScanner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;tagSourceName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;app/api/upload/route.ts&lt;/code&gt;
&lt;/h3&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;createNextUploadHandler&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;@pompelmi/next-upload&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;scanner&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;@/lib/security&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;runtime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;nodejs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dynamic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;force-dynamic&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;POST&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createNextUploadHandler&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;scanner&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;h3&gt;
  
  
  &lt;code&gt;app/page.tsx&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;HomePage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setResult&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setLoading&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&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;handleSubmit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FormEvent&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLFormElement&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&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;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentTarget&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;input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;elements&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;namedItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;file&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;HTMLInputElement&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&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;formData&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;FormData&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;file&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

    &lt;span class="nf"&gt;setLoading&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;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;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;/api/upload&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;method&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&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;formData&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;response&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;setResult&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;setLoading&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&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="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;maxWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;720&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;40px auto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;fontFamily&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sans-serif&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Secure file upload demo&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Upload a file and let the server inspect it before storage.&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt; &lt;span class="na"&gt;onSubmit&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleSubmit&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"file"&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"file"&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt; &lt;span class="na"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;loading&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;marginLeft&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;loading&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Scanning...&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;Upload&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;pre&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;pre&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>nextjs</category>
      <category>node</category>
      <category>security</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to Scan File Uploads in Express</title>
      <dc:creator>Tommaso Bertocchi</dc:creator>
      <pubDate>Sun, 29 Mar 2026 16:29:39 +0000</pubDate>
      <link>https://forem.com/sonotommy/how-to-scan-file-uploads-in-express-1g9o</link>
      <guid>https://forem.com/sonotommy/how-to-scan-file-uploads-in-express-1g9o</guid>
      <description>&lt;p&gt;Many Express apps let users upload files.&lt;/p&gt;

&lt;p&gt;That usually starts as a product feature:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;profile pictures&lt;/li&gt;
&lt;li&gt;resumes&lt;/li&gt;
&lt;li&gt;PDFs&lt;/li&gt;
&lt;li&gt;invoices&lt;/li&gt;
&lt;li&gt;ZIP archives&lt;/li&gt;
&lt;li&gt;documents sent to internal workflows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But an upload endpoint is also part of your attack surface.&lt;/p&gt;

&lt;p&gt;A file can look harmless from its extension alone and still be risky once your app stores it, serves it, unzips it, or sends it to another system.&lt;/p&gt;

&lt;p&gt;In this tutorial, we’ll build a simple Express upload route that scans files &lt;strong&gt;before storage&lt;/strong&gt; using:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Express&lt;/strong&gt; for the API&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multer&lt;/strong&gt; for &lt;code&gt;multipart/form-data&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pompelmi&lt;/strong&gt; for file inspection&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By the end, you’ll have a route that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;accepts a file upload&lt;/li&gt;
&lt;li&gt;inspects the uploaded bytes&lt;/li&gt;
&lt;li&gt;blocks suspicious or malicious files&lt;/li&gt;
&lt;li&gt;only saves files that pass your policy&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why file uploads need scanning
&lt;/h2&gt;

&lt;p&gt;A lot of upload pipelines still trust checks that are too shallow, such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the filename extension&lt;/li&gt;
&lt;li&gt;the client-provided MIME type&lt;/li&gt;
&lt;li&gt;a simple allowlist like &lt;code&gt;.pdf&lt;/code&gt;, &lt;code&gt;.jpg&lt;/code&gt;, &lt;code&gt;.zip&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is not enough.&lt;/p&gt;

&lt;p&gt;A safer pattern is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;receive the file&lt;/li&gt;
&lt;li&gt;inspect it immediately&lt;/li&gt;
&lt;li&gt;decide whether it is safe enough for your route&lt;/li&gt;
&lt;li&gt;only then store or process it&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That “inspect first, store later” approach is exactly what we’ll implement here.&lt;/p&gt;




&lt;h2&gt;
  
  
  What we’re using
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Express
&lt;/h3&gt;

&lt;p&gt;Express is the HTTP layer for our upload endpoint.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multer
&lt;/h3&gt;

&lt;p&gt;Multer is a Node.js middleware for handling &lt;code&gt;multipart/form-data&lt;/code&gt;, which is the format commonly used for file uploads in Express apps.&lt;/p&gt;

&lt;p&gt;For this tutorial, we’ll use &lt;strong&gt;memory storage&lt;/strong&gt; so Multer gives us a &lt;code&gt;Buffer&lt;/code&gt; in &lt;code&gt;req.file.buffer&lt;/code&gt;. That makes it easy to scan the file before writing anything to disk.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pompelmi
&lt;/h3&gt;

&lt;p&gt;Pompelmi is an open-source file upload security library for Node.js. It can inspect uploaded files before storage and report a verdict such as &lt;code&gt;clean&lt;/code&gt;, &lt;code&gt;suspicious&lt;/code&gt;, or &lt;code&gt;malicious&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It is designed to help catch issues such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MIME spoofing and magic-byte mismatches&lt;/li&gt;
&lt;li&gt;risky archives&lt;/li&gt;
&lt;li&gt;deep nesting and archive abuse&lt;/li&gt;
&lt;li&gt;polyglot files&lt;/li&gt;
&lt;li&gt;optional YARA-based matches&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Project setup
&lt;/h2&gt;

&lt;p&gt;Create a new folder and install the dependencies:&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="nb"&gt;mkdir &lt;/span&gt;express-upload-scan
&lt;span class="nb"&gt;cd &lt;/span&gt;express-upload-scan
npm init &lt;span class="nt"&gt;-y&lt;/span&gt;
npm &lt;span class="nb"&gt;install &lt;/span&gt;express multer pompelmi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tutorial assumes you are running a recent Node.js version supported by Pompelmi.&lt;/p&gt;




&lt;h2&gt;
  
  
  Build the upload route
&lt;/h2&gt;

&lt;p&gt;Create a file named &lt;code&gt;server.mjs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&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;express&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="nx"&gt;multer&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;multer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;mkdir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;writeFile&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;node:fs/promises&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;join&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;node:path&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;randomUUID&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;node:crypto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;scanBytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;STRICT_PUBLIC_UPLOAD&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;pompelmi&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;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&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;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;3000&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;upload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;multer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;multer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;memoryStorage&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;fileSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// 10 MB&lt;/span&gt;
    &lt;span class="na"&gt;files&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="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;html&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`
    &amp;lt;h1&amp;gt;Upload a file&amp;lt;/h1&amp;gt;
    &amp;lt;form action="/upload" method="post" enctype="multipart/form-data"&amp;gt;
      &amp;lt;input type="file" name="file" required /&amp;gt;
      &amp;lt;button type="submit"&amp;gt;Upload&amp;lt;/button&amp;gt;
    &amp;lt;/form&amp;gt;
  `&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/upload&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;upload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;single&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;file&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;No file uploaded&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;report&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;scanBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;originalname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;mimeType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mimetype&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;STRICT_PUBLIC_UPLOAD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;failClosed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;verdict&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;clean&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;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;422&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Upload blocked&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;verdict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;verdict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;reasons&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reasons&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;uploadsDir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;uploads&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="nf"&gt;mkdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;uploadsDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;recursive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;safeName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&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="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;originalname&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;destination&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;uploadsDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;safeName&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;writeFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;verdict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;verdict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;safeName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;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="nf"&gt;next&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="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;err&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="nx"&gt;_next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;multer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MulterError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Upload rejected by Multer&lt;/span&gt;&lt;span class="dl"&gt;"&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="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&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;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="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;error&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;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Internal server error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;port&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="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="s2"&gt;`Server listening on http://localhost:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;port&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;p&gt;Run it with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node server.mjs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then open &lt;code&gt;http://localhost:3000&lt;/code&gt; and upload a file.&lt;/p&gt;




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

&lt;p&gt;Let’s break down the important parts.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Multer parses &lt;code&gt;multipart/form-data&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;This line creates the upload middleware:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;upload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;multer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;multer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;memoryStorage&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;fileSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;files&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="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;We are doing three useful things here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;using &lt;strong&gt;memory storage&lt;/strong&gt; so the file is available as &lt;code&gt;req.file.buffer&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;limiting the upload size to &lt;strong&gt;10 MB&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;only allowing &lt;strong&gt;one file&lt;/strong&gt; on this route&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That matters because if you scan after writing to disk, you’ve already accepted and stored the file. In this example, we keep the file in memory long enough to inspect it first.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Pompelmi scans the uploaded bytes
&lt;/h3&gt;

&lt;p&gt;This is the core step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;report&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;scanBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;originalname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;mimeType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mimetype&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;STRICT_PUBLIC_UPLOAD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;failClosed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here’s what each option is doing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;req.file.buffer&lt;/code&gt;: the actual uploaded file bytes&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;filename&lt;/code&gt;: useful metadata for policy checks and reporting&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mimeType&lt;/code&gt;: the MIME type supplied by the upload layer&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;policy: STRICT_PUBLIC_UPLOAD&lt;/code&gt;: a strict policy suitable for untrusted public uploads&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;failClosed: true&lt;/code&gt;: if inspection fails unexpectedly, block the upload instead of letting it through&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is a much safer default than “best effort” validation on a public endpoint.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Only clean files get stored
&lt;/h3&gt;

&lt;p&gt;This condition is the boundary between accepted and rejected uploads:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;verdict&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;clean&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;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;422&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Upload blocked&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;verdict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;verdict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;reasons&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reasons&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;If the verdict is not &lt;code&gt;clean&lt;/code&gt;, the request stops there.&lt;/p&gt;

&lt;p&gt;Only after the scan passes do we create the &lt;code&gt;uploads/&lt;/code&gt; directory and write the file to disk.&lt;/p&gt;

&lt;p&gt;That ordering is the key idea of the whole tutorial.&lt;/p&gt;




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

&lt;h3&gt;
  
  
  Clean upload
&lt;/h3&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;"ok"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"verdict"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"clean"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"filename"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"a9f7f5f9-06d2-4aa9-a73e-31bcb84d9b29-document.pdf"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"size"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;48213&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;h3&gt;
  
  
  Blocked upload
&lt;/h3&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;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Upload blocked"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"verdict"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"suspicious"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reasons"&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="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mime-mismatch"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Detected file signature does not match the declared MIME type"&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;Your exact &lt;code&gt;reasons&lt;/code&gt; output will depend on the file and the policy.&lt;/p&gt;




&lt;h2&gt;
  
  
  Test it with &lt;code&gt;curl&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;You can also test the route 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;curl &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s2"&gt;"file=@./test.pdf"&lt;/span&gt; http://localhost:3000/upload
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Try it with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a normal PDF&lt;/li&gt;
&lt;li&gt;a renamed file with a misleading extension&lt;/li&gt;
&lt;li&gt;a ZIP archive&lt;/li&gt;
&lt;li&gt;a file that should be blocked by your policy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That gives you a quick way to verify the decision boundary of your route.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why this pattern is better than extension checks
&lt;/h2&gt;

&lt;p&gt;A lot of apps still do something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;allowed&lt;/span&gt; &lt;span class="o"&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;.jpg&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;.png&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;.pdf&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;That is useful as a UX hint, but it is not a security boundary.&lt;/p&gt;

&lt;p&gt;File names can be changed easily.&lt;/p&gt;

&lt;p&gt;Client-provided MIME types can also be misleading.&lt;/p&gt;

&lt;p&gt;A real upload defense should look at the file itself, not just the label attached to it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Production notes
&lt;/h2&gt;

&lt;p&gt;The example above is intentionally simple, but here are a few things you should think about before using this in production.&lt;/p&gt;

&lt;h3&gt;
  
  
  Set tight Multer limits
&lt;/h3&gt;

&lt;p&gt;If you use memory storage, size limits matter.&lt;/p&gt;

&lt;p&gt;Keep &lt;code&gt;fileSize&lt;/code&gt;, &lt;code&gt;files&lt;/code&gt;, and route-specific constraints as small as your product allows.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keep error handling explicit
&lt;/h3&gt;

&lt;p&gt;Multer and your scanner can fail for different reasons.&lt;/p&gt;

&lt;p&gt;Return clear errors to clients, but avoid leaking unnecessary internal details.&lt;/p&gt;

&lt;h3&gt;
  
  
  Store only after inspection
&lt;/h3&gt;

&lt;p&gt;Do not move the write-to-disk step before the scan.&lt;/p&gt;

&lt;p&gt;Otherwise you lose the main security benefit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Match policy to route risk
&lt;/h3&gt;

&lt;p&gt;A public document upload endpoint, an internal admin tool, and an image-only avatar route do not all need the same policy.&lt;/p&gt;

&lt;p&gt;Choose the strictness based on the trust level and the downstream processing pipeline.&lt;/p&gt;

&lt;h3&gt;
  
  
  Consider what happens after upload
&lt;/h3&gt;

&lt;p&gt;Scanning at the boundary is a strong first layer, but also think about what your app does next:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Will the file be served back to users?&lt;/li&gt;
&lt;li&gt;Will another service parse it?&lt;/li&gt;
&lt;li&gt;Will you unzip it?&lt;/li&gt;
&lt;li&gt;Will a worker transform it?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The more processing a file triggers, the more important your upload boundary becomes.&lt;/p&gt;




&lt;h2&gt;
  
  
  A smaller version if you already have &lt;code&gt;req.file&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;If your app already uses Multer somewhere else, the minimal scanning step is just this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;scanBytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;STRICT_PUBLIC_UPLOAD&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;pompelmi&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;report&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;scanBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;originalname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;mimeType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mimetype&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;STRICT_PUBLIC_UPLOAD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;failClosed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;verdict&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;clean&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;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;422&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Upload blocked&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;verdict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;verdict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;reasons&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reasons&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;That is the core integration.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;If your Express app accepts user uploads, don’t treat that endpoint as a boring plumbing detail.&lt;/p&gt;

&lt;p&gt;Treat it like a security boundary.&lt;/p&gt;

&lt;p&gt;The simplest safe flow is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;receive the file&lt;/li&gt;
&lt;li&gt;scan the bytes&lt;/li&gt;
&lt;li&gt;block anything that is not clean&lt;/li&gt;
&lt;li&gt;store only the files you trust enough to keep&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That single change can make your upload pipeline much safer without turning your app architecture upside down.&lt;/p&gt;

&lt;p&gt;If you want to try this approach, take a look at &lt;strong&gt;Pompelmi&lt;/strong&gt; here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/pompelmi/pompelmi" rel="noopener noreferrer"&gt;github.com/pompelmi/pompelmi&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Docs: &lt;a href="https://pompelmi.github.io/pompelmi/" rel="noopener noreferrer"&gt;pompelmi.github.io/pompelmi&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://expressjs.com/en/guide/using-middleware.html" rel="noopener noreferrer"&gt;Express middleware guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://expressjs.com/en/guide/error-handling.html" rel="noopener noreferrer"&gt;Express error handling&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/expressjs/multer" rel="noopener noreferrer"&gt;Multer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/pompelmi/pompelmi" rel="noopener noreferrer"&gt;Pompelmi&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>node</category>
      <category>express</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
