<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Gerardo León</title>
    <description>The latest articles on DEV Community by Gerardo León (@gerardo_leon).</description>
    <link>https://dev.to/gerardo_leon</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4057025%2Fc8593235-2590-493c-a5f1-d7e3e789be64.png</url>
      <title>DEV Community: Gerardo León</title>
      <link>https://dev.to/gerardo_leon</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gerardo_leon"/>
    <language>en</language>
    <item>
      <title>Self-Hosting a Dockerized App on WSL2, Exposed with a Cloudflare Tunnel</title>
      <dc:creator>Gerardo León</dc:creator>
      <pubDate>Thu, 17 Sep 2026 03:14:42 +0000</pubDate>
      <link>https://dev.to/gerardo_leon/self-hosting-a-dockerized-app-on-wsl2-exposed-with-a-cloudflare-tunnel-3c0l</link>
      <guid>https://dev.to/gerardo_leon/self-hosting-a-dockerized-app-on-wsl2-exposed-with-a-cloudflare-tunnel-3c0l</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; A proof-of-concept to-do app — Node/Express and Postgres, running in Docker on WSL2 — reachable at a real domain through a named cloudflared tunnel. No login system, just a per-browser cookie hash. If you already have WSL2 and a Cloudflare-managed domain, expect this to take a few minutes, not hours.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I wanted a small proof of concept: a to-do list with real persistence, running in Docker, reachable from my own domain instead of &lt;code&gt;localhost&lt;/code&gt;. Nothing about that sounded hard on paper to me, but knew I was a bit rusty on it. So, brushing off the dust, I encountered with three lessons I'm sharing here with you... besides of the fact I don't want to be paying for small hosting for small PoCs lol&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjx3ezyjustb57rlyuvpn.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjx3ezyjustb57rlyuvpn.png" alt=" " width="800" height="603"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What You'll Need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Windows OS&lt;/li&gt;
&lt;li&gt;WSL2 with an Ubuntu distro&lt;/li&gt;
&lt;li&gt;Docker Engine installed inside WSL2 (not Docker Desktop — more on why below)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cloudflared&lt;/code&gt; (I used v2026.9.1)&lt;/li&gt;
&lt;li&gt;A domain managed in Cloudflare, so you can route a subdomain to a tunnel&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What We're Building
&lt;/h2&gt;

&lt;p&gt;The app itself is deliberately small: Node.js and Express on &lt;code&gt;node:alpine&lt;/code&gt;, one Postgres table (&lt;code&gt;todos&lt;/code&gt;), and no real authentication. Each visitor gets an &lt;code&gt;httpOnly&lt;/code&gt; cookie with a random hash (&lt;code&gt;crypto.randomBytes(16).toString('hex')&lt;/code&gt;), and every query filters by that hash. That gives you a separate list per browser without a login screen — fine for a POC, not something I'd ship for anything that needed real accounts. The table clears itself every two hours, with a warning in the UI so nobody's surprised when their list disappears.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5tmssfr90i22p67yo510.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5tmssfr90i22p67yo510.png" alt=" " width="663" height="493"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I built two ways to deploy it, mostly to compare them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A bare &lt;code&gt;Dockerfile&lt;/code&gt; that connects to the Postgres already running natively on the Windows host.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;docker-compose.yml&lt;/code&gt; with its own Postgres container, plus a sidecar container running &lt;code&gt;crond&lt;/code&gt; for the periodic cleanup.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One of these paths turned out to be a dead end, for reasons that only showed up once I changed how Docker itself was running — which is the first detour.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvz9i8rt0582j33tqz4v1.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvz9i8rt0582j33tqz4v1.PNG" alt=" " width="546" height="591"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Not &lt;code&gt;pg_cron&lt;/code&gt;?
&lt;/h3&gt;

&lt;p&gt;Before reaching for an app-level timer, I checked whether Postgres could handle the two-hour cleanup itself, natively, via &lt;code&gt;pg_cron&lt;/code&gt;. It turns out &lt;code&gt;pg_cron&lt;/code&gt; does have real, current support on Windows. It just isn't a &lt;code&gt;CREATE EXTENSION&lt;/code&gt; away: you have to compile it yourself with Visual Studio Build Tools (&lt;code&gt;nmake /F Makefile.win&lt;/code&gt;) and touch &lt;code&gt;shared_preload_libraries&lt;/code&gt;, which means restarting the Postgres service.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; for a POC scoped around "minimum requirements," a build toolchain and a service restart is too much ceremony for a job that runs once every two hours. I dropped &lt;code&gt;pg_cron&lt;/code&gt; in favor of a plain &lt;code&gt;setInterval&lt;/code&gt; in the Node app for the bare-Dockerfile path, and a &lt;code&gt;crond&lt;/code&gt; sidecar container for the docker-compose path — the more idiomatic Docker pattern for periodic jobs anyway.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  From Docker Desktop to Docker Engine on WSL2
&lt;/h2&gt;

&lt;p&gt;I started this project thinking of Docker Desktop, though favored later for the raw Docker Engine to be in flavor country (Homer S. line, lol) and the way for this is installing WSL2 in Windows, which takes barely a command in Powershell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;wsl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--install&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Ubuntu&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice: This requires a Windows restart. When Ubuntu comes back up, it'll ask you to create a Unix user and password, BAU.&lt;/p&gt;

&lt;p&gt;Inside Ubuntu, install Docker Engine by running the following:&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;-fsSL&lt;/span&gt; https://get.docker.com | &lt;span class="nb"&gt;sudo &lt;/span&gt;sh
&lt;span class="nb"&gt;sudo &lt;/span&gt;usermod &lt;span class="nt"&gt;-aG&lt;/span&gt; docker &lt;span class="nv"&gt;$USER&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Watch out:&lt;/strong&gt; the new &lt;code&gt;docker&lt;/code&gt; group membership only takes effect in a fresh login session. Until you log out and back in, keep prefixing commands with &lt;code&gt;sudo&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Verify the install with:&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;sudo &lt;/span&gt;docker info
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Networking Catch
&lt;/h3&gt;

&lt;p&gt;Moving Docker into WSL2 broke the bare-Dockerfile path. With Docker Desktop, &lt;code&gt;host.docker.internal&lt;/code&gt; reached the Postgres instance running natively on Windows without any fuss. Once Docker was running inside the WSL2 VM itself, that same hostname had to cross two separate network hops instead of one, and it stopped resolving cleanly.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Watch out:&lt;/strong&gt; if you're moving from Docker Desktop to Docker Engine in WSL2 and something depended on &lt;code&gt;host.docker.internal&lt;/code&gt; reaching a service on native Windows, expect it to need rework. Rather than fight WSL2-to-Windows networking, I just used the docker-compose path — Postgres in its own container, inside the same WSL2 VM as the app — and the problem disappeared.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Deploying the Stack
&lt;/h2&gt;

&lt;p&gt;With Docker Engine running inside WSL2, bringing the stack up looks like any other docker-compose project:&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;cd&lt;/span&gt; /mnt/c/%PROJECT%/todo-docker
&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env
&lt;span class="c"&gt;# edit .env: DB_USER, DB_PASSWORD, DB_NAME&lt;/span&gt;

&lt;span class="nb"&gt;sudo &lt;/span&gt;docker compose up &lt;span class="nt"&gt;--build&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;docker compose ps
curl &lt;span class="nt"&gt;-i&lt;/span&gt; http://localhost:3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The project files live on the Windows filesystem, mounted into WSL2 at &lt;code&gt;/mnt/c/...&lt;/code&gt;. Docker's build runs against that path with no copying step and no surprises — WSL2's filesystem interop handles it transparently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Exposing It: Cloudflare Tunnel
&lt;/h2&gt;

&lt;p&gt;With the app answering on &lt;code&gt;localhost:3000&lt;/code&gt;, the last piece is making it reachable at a real domain. &lt;code&gt;cloudflared&lt;/code&gt; handles that without opening a port on your router:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cloudflared.exe tunnel login
cloudflared.exe tunnel create todo-docker-poc
cloudflared.exe tunnel route dns todo-docker-poc todo-list-docker.gerardoleon.dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;cloudflared.exe&lt;/code&gt; is a native Windows binary. WSL2 can call it directly through Windows interop, since it's already on the Windows &lt;code&gt;PATH&lt;/code&gt; that WSL inherits — there's no need to install a second copy inside the Ubuntu distro.&lt;/p&gt;

&lt;p&gt;After filling in &lt;code&gt;cloudflared/config.yml&lt;/code&gt; with the tunnel ID from &lt;code&gt;tunnel create&lt;/code&gt;, running the tunnel looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cloudflared.exe tunnel &lt;span class="nt"&gt;--config&lt;/span&gt; &lt;span class="s2"&gt;"C:&lt;/span&gt;&lt;span class="se"&gt;\%&lt;/span&gt;&lt;span class="s2"&gt;PROJECT%&lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s2"&gt;odo-docker&lt;/span&gt;&lt;span class="se"&gt;\c&lt;/span&gt;&lt;span class="s2"&gt;loudflared&lt;/span&gt;&lt;span class="se"&gt;\c&lt;/span&gt;&lt;span class="s2"&gt;onfig.yml"&lt;/span&gt; run todo-docker-poc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Path Gotcha
&lt;/h3&gt;

&lt;p&gt;This is the one that cost me the most time for the smallest reason. &lt;code&gt;cloudflared.exe&lt;/code&gt;, even when you call it from WSL2's bash, is still a Windows process — it interprets paths the Windows way, not the Linux way. Passing it &lt;code&gt;--config /mnt/c/Users/.../config.yml&lt;/code&gt; fails with "the system cannot find the path specified," which reads like a permissions problem but isn't one.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Watch out:&lt;/strong&gt; when a Windows binary is invoked from WSL2 via interop, give it Windows-style paths (&lt;code&gt;C:\...\config.yml&lt;/code&gt;), not the &lt;code&gt;/mnt/c/...&lt;/code&gt; form WSL2 itself uses. The binary's process, not the shell calling it, decides how paths get parsed.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvghh0aud5gk6whzml7bn.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvghh0aud5gk6whzml7bn.png" alt=" " width="800" height="242"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Problem&lt;/th&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;pg_hba.conf&lt;/code&gt; only allows &lt;code&gt;127.0.0.1&lt;/code&gt;/&lt;code&gt;::1&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Docker container can't reach native Postgres&lt;/td&gt;
&lt;td&gt;Add a scoped &lt;code&gt;host&lt;/code&gt; rule for the Docker bridge subnet, e.g. &lt;code&gt;host tododb todoapp 172.16.0.0/12 scram-sha-256&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;host.docker.internal&lt;/code&gt; unreachable after moving to WSL2&lt;/td&gt;
&lt;td&gt;Bare-Dockerfile path can't reach host Postgres&lt;/td&gt;
&lt;td&gt;Use the docker-compose path instead — Postgres in its own container, same WSL2 VM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;cloudflared.exe&lt;/code&gt; says "can't find the specified path"&lt;/td&gt;
&lt;td&gt;Tunnel won't start with a &lt;code&gt;/mnt/c/...&lt;/code&gt; config path&lt;/td&gt;
&lt;td&gt;Pass the Windows-style path instead — &lt;code&gt;cloudflared.exe&lt;/code&gt; parses paths as Windows, not WSL2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;docker&lt;/code&gt; group permission denied&lt;/td&gt;
&lt;td&gt;Can't run &lt;code&gt;docker&lt;/code&gt; without &lt;code&gt;sudo&lt;/code&gt; right after install&lt;/td&gt;
&lt;td&gt;Log out and back in for group membership to apply, or use &lt;code&gt;sudo&lt;/code&gt; for the rest of that session&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Achievements 🎉🙌
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Self-hosted a real Postgres-backed app in Docker, running inside WSL2&lt;/li&gt;
&lt;li&gt;Exposed it at a real FQDN (&lt;code&gt;todo-list-docker.gerardoleon.dev&lt;/code&gt;) through a named cloudflared tunnel&lt;/li&gt;
&lt;li&gt;Verified it end-to-end: adding, checking off, and deleting tasks through the public URL, with persistence actually holding up in Postgres&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a deeper look at wiring up automated publishing around a project like this, see &lt;a href="https://dev.to/gerardo_leon/connect-your-github-repo-to-npm-for-package-deployments-33ec"&gt;connecting a GitHub repo to a package registry&lt;/a&gt;. If you want to take this further with real accounts instead of a cookie hash, that's the natural next post.&lt;/p&gt;

&lt;p&gt;Try it yourself cloning here: &lt;a href="https://github.com/elparaquecosadeque/todo-list-docker" rel="noopener noreferrer"&gt;https://github.com/elparaquecosadeque/todo-list-docker&lt;/a&gt;&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Do I need Docker Desktop for this?
&lt;/h3&gt;

&lt;p&gt;No. Docker Engine inside WSL2, installed with the official convenience script, is enough — Docker Desktop was actually the thing I moved away from here, not a requirement. The app/Postgres/docker-compose side of this has nothing WSL2-specific in it either: it's the same Docker Engine install and the same compose stack you'd run on a standalone Ubuntu box, since Postgres lives in its own container and never has to reach out to a host OS. The one part that would change is exposing it — on standalone Ubuntu you'd install the native Linux &lt;code&gt;cloudflared&lt;/code&gt; binary instead of &lt;code&gt;cloudflared.exe&lt;/code&gt;, and none of the Windows-path gotcha from this post would apply, since there's no Windows layer in between.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why not just use &lt;code&gt;pg_cron&lt;/code&gt; for the cleanup job?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;pg_cron&lt;/code&gt; has real Windows support, but it needs a compiled build via Visual Studio Build Tools and a Postgres service restart to enable &lt;code&gt;shared_preload_libraries&lt;/code&gt;. For a two-hour cleanup timer on a minimal POC, an app-level &lt;code&gt;setInterval&lt;/code&gt; or a &lt;code&gt;crond&lt;/code&gt; sidecar container gets the same result for a fraction of the setup cost.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why WSL2 instead of Docker Desktop on native Windows?
&lt;/h3&gt;

&lt;p&gt;Mainly to run Docker Engine directly, without the Desktop app, and to actually learn WSL2 instead of treating it as a black box underneath Docker Desktop. The trade-off is that anything relying on &lt;code&gt;host.docker.internal&lt;/code&gt; to reach a service on native Windows may need rework — in this case, moving Postgres into the same compose stack as the app.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>wsl2</category>
      <category>cloudflared</category>
      <category>selfhosted</category>
    </item>
    <item>
      <title>Do you assume or confirm?</title>
      <dc:creator>Gerardo León</dc:creator>
      <pubDate>Fri, 07 Aug 2026 05:21:41 +0000</pubDate>
      <link>https://dev.to/gerardo_leon/do-you-assume-or-confirm-57m3</link>
      <guid>https://dev.to/gerardo_leon/do-you-assume-or-confirm-57m3</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; This article describes a debugging case from 2023. Some of the technologies mentioned here have since changed in relevance. In particular, Moment.js is now considered a legacy project in maintenance mode and is generally not recommended for new applications. The debugging lessons and reasoning process described here are still applicable, but the specific technical choices should be understood in their original context.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Chapter 1: How I learnt to distrust JavaScript &lt;code&gt;Date&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Daniel Kahneman’s famous book &lt;em&gt;Thinking, Fast and Slow&lt;/em&gt; describes two modes of thinking:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;System 1&lt;/strong&gt; is automatic, fast, emotional, and quick to respond to stimuli. It constantly operates in the background and often bases its output on assumptions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;System 2&lt;/strong&gt; is invoked consciously. It is slower, more deliberate, logical, and methodical, basing its output on reasoning.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When it comes to problem-solving, we usually expect System 2 to take over. However, familiarity with a domain gradually builds a stack of assumptions that System 1 may stop revalidating.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;strong&gt;That overconfidence can mislead us and make our decisions less reliable than we think.&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If we were to visualize both systems while debugging, a good analogy would be Wile E. Coyote realizing that he has been running on thin air while chasing the Road Runner.&lt;/p&gt;

&lt;p&gt;Leaving System 1 in command for too long, without verifying the premises we have already accepted as valid, is basically us playing Coyote.&lt;/p&gt;

&lt;p&gt;Therefore, it is worth regularly asking ourselves:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Am I assuming these premises, or have I already confirmed what I’m basing this on?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This question can help prevent us from getting trapped in debugging dead ends.&lt;/p&gt;

&lt;p&gt;What criteria should trigger this question?&lt;/p&gt;

&lt;p&gt;I tend to use a simple one:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“For the next hypothesis I’m about to accept, have I confirmed its foundations, or am I assuming them?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This simple question can save a significant amount of time and effort.&lt;/p&gt;

&lt;h2&gt;
  
  
  Application problem
&lt;/h2&gt;

&lt;p&gt;A bit of context:&lt;/p&gt;

&lt;p&gt;Two systems communicate through event-based messaging.&lt;/p&gt;

&lt;p&gt;The originator system sends events in a specific order. The receiving system applies the event data to its state and updates its UI accordingly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Problem
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Even though the originator system sends events in the expected order, the receiver's UI does not always update in that same order.&lt;/li&gt;
&lt;li&gt;These symptoms were already known, although the root cause remained unclear. A race condition in the receiver's event interpreter had become the main suspect.&lt;/li&gt;
&lt;li&gt;The fact that multiple events were emitted within the same second appeared to support the race-condition theory.&lt;/li&gt;
&lt;li&gt;We were considering changes in the originator system because, after a recent optimization, some events were being produced in roughly half the previous processing time.&lt;/li&gt;
&lt;li&gt;When I was asked why the originator needed to change, I could not provide a solid explanation. I was under pressure to deliver a fix quickly, but my reasoning was mostly based on assumptions.&lt;/li&gt;
&lt;li&gt;I then learned that the receiver had an event timestamp comparator implemented using Moment.js.&lt;/li&gt;
&lt;li&gt;Event A and Event B could arrive within the same second and even share the same first three fractional-second digits.&lt;/li&gt;
&lt;li&gt;Event A could take slightly longer to process than Event B because additional business rules were applied to it.&lt;/li&gt;
&lt;li&gt;Before updating the UI, the receiver's timestamp comparator would skip an event if its timestamp was older than the timestamp of the event previously applied.&lt;/li&gt;
&lt;li&gt;JavaScript's legacy &lt;code&gt;Date&lt;/code&gt; object stores time with millisecond precision. This means that fractional seconds beyond three digits are not preserved by &lt;code&gt;Date&lt;/code&gt;, and Moment.js inherits that limitation when relying on JavaScript dates.&lt;/li&gt;
&lt;li&gt;As a workaround, the comparison was changed to operate on the original timestamp strings so that additional fractional-second digits could be taken into account when two events appeared otherwise equal.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Lesson
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Read the documentation.&lt;/li&gt;
&lt;li&gt;Get to the root of the problem before proposing a convenient workaround.&lt;/li&gt;
&lt;li&gt;Do not mistake an assumption for a confirmed constraint.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Solution
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Keep access to the raw timestamp representation.&lt;/li&gt;
&lt;li&gt;When two events are equal at millisecond precision, use the additional fractional-second digits from the original timestamp as a tiebreaker.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Another problem
&lt;/h3&gt;

&lt;p&gt;The previous solution improved things, but another issue appeared.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I expected timestamps to compare correctly, yet some reported cases still produced unexpected tiebreaker values.&lt;/li&gt;
&lt;li&gt;Simple string slicing turned out to be unreliable because the fractional-second representation was not always identical.&lt;/li&gt;
&lt;li&gt;I switched to a regular expression that identified the relevant fractional-second portion more explicitly.&lt;/li&gt;
&lt;li&gt;Reliability improved, but some additional reports still came in.&lt;/li&gt;
&lt;li&gt;This time, the problem was not the comparison logic itself. Some timestamps contained fewer fractional-second digits than expected.&lt;/li&gt;
&lt;li&gt;The affected timestamps corresponded to values whose complete representations would have contained trailing zeroes.&lt;/li&gt;
&lt;li&gt;The extraction function behaved correctly when trailing zeroes were present, so the next question became: where were those zeroes disappearing?&lt;/li&gt;
&lt;li&gt;A teammate suggested checking the serialization layer.&lt;/li&gt;
&lt;li&gt;Testing confirmed that serialization was modifying the timestamp representation.&lt;/li&gt;
&lt;li&gt;The application was using Newtonsoft.Json, and its date serialization behavior could omit trailing fractional-second zeroes depending on the configured format.&lt;/li&gt;
&lt;li&gt;An &lt;code&gt;IsoDateTimeConverter&lt;/code&gt; was configured with an explicit datetime format that preserved the required fractional-second representation.&lt;/li&gt;
&lt;li&gt;After that change, the event timestamp comparison became considerably more reliable.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Lesson
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;If the bug is not in the business layer, inspect the supporting layers as well.&lt;/li&gt;
&lt;li&gt;Serialization, parsing, formatting, and transport layers can silently transform data that the business logic assumes to be unchanged.&lt;/li&gt;
&lt;li&gt;When working with structured strings, identify the exact format you expect instead of relying on positional assumptions such as arbitrary slicing.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Solution
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Use an explicit and validated format when extracting structured values from strings.&lt;/li&gt;
&lt;li&gt;Configure serialization behavior deliberately when formatting affects application logic.&lt;/li&gt;
&lt;li&gt;Avoid relying on default serializer behavior for data whose exact representation matters.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Looking back
&lt;/h2&gt;

&lt;p&gt;There is another architectural lesson I would add today.&lt;/p&gt;

&lt;p&gt;If event ordering is a fundamental property of the system, relying exclusively on timestamps to establish that order can be fragile.&lt;/p&gt;

&lt;p&gt;Timestamp precision, serialization behavior, clock synchronization, processing delays, and formatting differences can all affect the result.&lt;/p&gt;

&lt;p&gt;When strict ordering is required, an explicit sequence number, event version, or another monotonic ordering mechanism can provide a stronger guarantee than timestamps alone.&lt;/p&gt;

&lt;p&gt;That does not invalidate the debugging solution described above—it simply moves the responsibility for ordering closer to the domain itself instead of deriving it indirectly from time.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Daniel Kahneman, &lt;em&gt;Thinking, Fast and Slow&lt;/em&gt; (2011)&lt;/li&gt;
&lt;li&gt;Moment.js issue regarding sub-millisecond precision:
&lt;code&gt;https://github.com/moment/moment/issues/3256&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Moment.js project status and documentation:
&lt;code&gt;https://momentjs.com/docs/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Newtonsoft.Json issue regarding trailing fractional-second zeroes:
&lt;code&gt;https://github.com/JamesNK/Newtonsoft.Json/issues/1511&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I’ll come back to this series whenever I stumble upon situations that bring up the same question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Are you assuming or confirming?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I want to keep challenging the reasoning patterns that affect not only how we work, but also how we approach everyday problems.&lt;/p&gt;

</description>
      <category>debugging</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Rotate AWS IAM Access Keys by script</title>
      <dc:creator>Gerardo León</dc:creator>
      <pubDate>Thu, 06 Aug 2026 04:50:33 +0000</pubDate>
      <link>https://dev.to/gerardo_leon/rotate-aws-iam-access-keys-by-script-4g8g</link>
      <guid>https://dev.to/gerardo_leon/rotate-aws-iam-access-keys-by-script-4g8g</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Update (2026):&lt;/strong&gt; This article documents a legacy approach and is preserved for historical and educational purposes. AWS now recommends temporary credentials through IAM Identity Center for human access and local development, and IAM roles for workloads, instead of long-term IAM user access keys whenever possible.&lt;/p&gt;

&lt;p&gt;The 12-hour expiration described below applied to temporary credentials or an organization-specific setup; IAM user access keys do not expire automatically. The original script also deletes the oldest key before validating the replacement and overwrites the &lt;code&gt;default&lt;/code&gt; profile in &lt;code&gt;.aws/credentials&lt;/code&gt;. Do not use it unchanged in a production environment. A safer rotation flow is: create the new key, configure and test it, deactivate the old key, verify that it is no longer used, and only then delete it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Context
&lt;/h2&gt;

&lt;p&gt;Provided IAM access keys were set to expire in 12 hours. Developing locally constantly resulted in &lt;code&gt;Token expired&lt;/code&gt; exceptions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solution
&lt;/h2&gt;

&lt;p&gt;Create an IAM user access key on demand through a script, following AWS recommendations to rotate access keys periodically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem
&lt;/h2&gt;

&lt;p&gt;Creating these access keys and adding them to the &lt;code&gt;.aws/credentials&lt;/code&gt; file is a very manual process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solution
&lt;/h2&gt;

&lt;p&gt;Script the whole process through the AWS CLI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Required
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;An IAM user name.&lt;/li&gt;
&lt;li&gt;A valid access key for the one-time setup.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Requirements
&lt;/h2&gt;

&lt;p&gt;Given one valid IAM access key for the IAM user, create another access key and configure it under the &lt;code&gt;default&lt;/code&gt; profile in &lt;code&gt;.aws/credentials&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Given two valid IAM access keys for the IAM user, delete the oldest one and then create a new one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Script
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Assuming AWS CLI is installed and you have sufficient permissions&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;param&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="n"&gt;Parameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Mandatory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;$true&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="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nv"&gt;$IAMUserName&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="c"&gt;# Fetch the access keys for the user&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$keys&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;iam&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;list-access-keys&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--user-name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$IAMUserName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ConvertFrom-Json&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# If 2 keys exist, delete the oldest one&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$keys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AccessKeyMetadata&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Count&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-eq&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&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="c"&gt;# Sorting by CreateDate to find the oldest key&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nv"&gt;$oldestKey&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$keys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AccessKeyMetadata&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Sort-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;CreateDate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Select-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-First&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;1&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="c"&gt;# Delete the oldest key&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;iam&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;delete-access-key&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--user-name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$IAMUserName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--access-key-id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$oldestKey&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AccessKeyId&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="c"&gt;# Create a new access key&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$newKey&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;iam&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;create-access-key&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--user-name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$IAMUserName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ConvertFrom-Json&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# Write to the .aws/credentials file&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$credentialsPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;USERPROFILE&lt;/span&gt;&lt;span class="s2"&gt;\.aws\credentials"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$credentialsContent&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="sh"&gt;@"
[default]
aws_access_key_id = &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;$newKey&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AccessKey&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AccessKeyId&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;
aws_secret_access_key = &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;$newKey&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AccessKey&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SecretAccessKey&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;
"@&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# Overwrite the credentials file with the new keys&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# You might want to append or edit it if you use multiple profiles&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Set-Content&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$credentialsPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Value&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$credentialsContent&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="n"&gt;Write-Output&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Updated .aws/credentials with new access keys."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Usage
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;\RotateKeys.ps1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-IAMUserName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"YourIAMUserName"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Preconditions
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html" rel="noopener noreferrer"&gt;AWS CLI&lt;/a&gt; installed.&lt;/li&gt;
&lt;li&gt;An &lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/id_users_create.html#id_users_create_console" rel="noopener noreferrer"&gt;AWS IAM user created&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Sufficient IAM permissions to list, create, deactivate, and delete access keys, according to the rotation process you implement.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html" rel="noopener noreferrer"&gt;AWS IAM security best practices&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/id-credentials-access-keys-update.html" rel="noopener noreferrer"&gt;Update access keys&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/cli/latest/reference/iam/" rel="noopener noreferrer"&gt;AWS CLI IAM command reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://gist.github.com/nosavvy33/9adb00860ef3c9e4e4714a5bf427a141" rel="noopener noreferrer"&gt;Original PowerShell Gist&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>cicd</category>
      <category>aws</category>
      <category>cloud</category>
      <category>iam</category>
    </item>
    <item>
      <title>Tinkering with ChatGPT: Gmails to Trello cards</title>
      <dc:creator>Gerardo León</dc:creator>
      <pubDate>Thu, 06 Aug 2026 04:19:35 +0000</pubDate>
      <link>https://dev.to/gerardo_leon/tinkering-with-chatgpt-gmails-to-trello-cards-38df</link>
      <guid>https://dev.to/gerardo_leon/tinkering-with-chatgpt-gmails-to-trello-cards-38df</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Update — August 2026&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This article documents an experiment originally published in April 2023. The Gmail-to-Trello concept and both APIs remain available, but the linked implementation should now be treated as an &lt;strong&gt;unmaintained experimental prototype&lt;/strong&gt;, not as a production-ready or plug-and-play tool.&lt;/p&gt;

&lt;p&gt;The repository targets the unsupported Node.js 16 runtime and uses &lt;code&gt;pkg&lt;/code&gt;, which was archived in 2024. It also requires security and reliability fixes, including removing credentials from logs, correcting email deduplication and startup behavior, improving Gmail time-based queries, and adding safer error handling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not run the current repository with real Gmail or Trello credentials without first reviewing and updating the code and its dependencies.&lt;/strong&gt; If you test it, use non-production accounts, least-privilege credentials, and revoke the tokens afterward.&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%2Fmiro.medium.com%2Fv2%2Fda%3Atrue%2Fresize%3Afit%3A1200%2F0%2A5JgWJnbem458UFP8" 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%2Fmiro.medium.com%2Fv2%2Fda%3Atrue%2Fresize%3Afit%3A1200%2F0%2A5JgWJnbem458UFP8" alt="Photo by Rolf van Root on Unsplash" width="1200" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Photo by &lt;a href="https://unsplash.com/@freshvanroot" rel="noopener noreferrer"&gt;Rolf van Root&lt;/a&gt; on &lt;a href="https://unsplash.com/" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why ChatGPT?
&lt;/h2&gt;

&lt;p&gt;As a manager, my coding duties have reduced drastically. So, looked everywhere to build or automate something that could impact any close one’s work. Found ChatGPT could be a helper to get some inspiration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use case
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Someone close does some grunt work that constraints it to share a Gmail account with other three people.&lt;/li&gt;
&lt;li&gt;Work is distributed by emails, each member has to pick their corresponding emails and address them.&lt;/li&gt;
&lt;li&gt;Problem: most of the times, someone opens an email that does not matter to its responsibility, and leaves it marked as read; so confusion as to if that email was addressed takes over the place.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What’s in the market?
&lt;/h2&gt;

&lt;p&gt;My diagnostic is the group is prone to carelessly leave emails read by any reason; so my solution, after speaking with the affected, is to move the emails to a place where their state is visible and it’s easier to distribute tasks to the responsible ones. The most common tool for transparentizing such type of workspace, to me, is Trello or any board-like tool, besides that has a free tier, accessible to many users, and allows to order work in lists.&lt;/p&gt;

&lt;p&gt;Did some research prior to get dirty with code, found there’s a &lt;a href="https://workspace.google.com/marketplace/app/trello_for_gmail/710372554046" rel="noopener noreferrer"&gt;Trello add-on for Gmail&lt;/a&gt;. However, it ain’t cool enough: asks to open a mail to see the add-on in action, this did not avoid the scenario were oblivious users could skip creating Trello cards for each email read. Besides that did not provide any way to automate card creation. So that was a no-way.&lt;/p&gt;

&lt;p&gt;Without further ado, hands on work…&lt;/p&gt;

&lt;h2&gt;
  
  
  Solution
&lt;/h2&gt;

&lt;p&gt;There, I came up with some requirements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tool should create cards automatically based off a emails from an account.&lt;/li&gt;
&lt;li&gt;Tool should not prompt user for any input once it’s setup.&lt;/li&gt;
&lt;li&gt;Tool should create cards on a time basis.&lt;/li&gt;
&lt;li&gt;Tool should integrate with Gmail and Trello.&lt;/li&gt;
&lt;li&gt;Tool should be a plug-and-play thingy.&lt;/li&gt;
&lt;li&gt;Cards should be concesive enough and avoid to copy-paste all of the email contents.&lt;/li&gt;
&lt;li&gt;Cards should not duplicate.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From there, the inputs and outputs define as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Inputs:&lt;/strong&gt; Gmail credentials, Trello integration requirements, user preferences on time basis for these cards to create, target Trello list, target Trello board.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Outputs:&lt;/strong&gt; Trello cards in given Trello list of a Trello board.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Caveats
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;I already mentioned I don’t code much often, and have got a bit dusty so decided to use ChatGPT for this journey.&lt;/li&gt;
&lt;li&gt;Trello has its capricious way to integrate with custom tools, so that adds a bit of overhead to the final solution.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Talk is cheap, show me the code:&lt;/p&gt;

&lt;p&gt;Below is the link to the GitHub repo, where I include prompts used with ChatGPT in &lt;code&gt;prompts.txt&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/nosavvy33/gmails-to-trello" rel="noopener noreferrer"&gt;&lt;strong&gt;nosavvy33/gmails-to-trello on GitHub&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Next steps
&lt;/h2&gt;

&lt;p&gt;See if this can become a Google add-on so that setup overhead reduces.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final words
&lt;/h2&gt;

&lt;p&gt;Surprised about how good of a work starter ChatGPT can be, no doubt it still needs polishing and human intervention for its proposed solutions to work. Personally, I come from another profession menaced by AI: translator; where there’s a saying “machine translation will replace translators that translate like machines”. I think of ChatGPT the same way, machine-generated software will replace software developers that write software as machines.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Connect your Github repo to NPM for package deployments</title>
      <dc:creator>Gerardo León</dc:creator>
      <pubDate>Sat, 01 Aug 2026 21:41:18 +0000</pubDate>
      <link>https://dev.to/gerardo_leon/connect-your-github-repo-to-npm-for-package-deployments-33ec</link>
      <guid>https://dev.to/gerardo_leon/connect-your-github-repo-to-npm-for-package-deployments-33ec</guid>
      <description>&lt;h3&gt;
  
  
  Most important... 🧐
&lt;/h3&gt;

&lt;p&gt;You should have already a library to publish to npm, if not, you can &lt;a href="https://dev.to/gerardo_leon/create-your-first-angular-components-library-2co"&gt;follow my previous tutorial&lt;/a&gt;, it will have you ready for this post 😀 &lt;/p&gt;

&lt;h2&gt;
  
  
  The NPM fun: first publish 👨‍💻
&lt;/h2&gt;

&lt;p&gt;Now we are ready for the NPM fun, add these two scripts to your root package.json:&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;"build-lib"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ng build custom-ui"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"publish-lib"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npm run build-lib &amp;amp;&amp;amp; npm login &amp;amp;&amp;amp; npm publish ./dist/custom-ui --access public"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also, make sure to add these lines to the library's package.json:&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;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@gblp/custom-ui"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;---&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;make&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;sure&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;you&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;include&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;your&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;npm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;username&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"repository"&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"git"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"git+https://github.com/elparaquecosadeque/npm-test.git"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"publishConfig"&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;"access"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"public"&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;After doing so, run this in your terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run publish-lib
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And your terminal should prompt you for login to npm and then publish&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9q05gdwwlubmbfcypxmb.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9q05gdwwlubmbfcypxmb.png" alt=" " width="800" height="508"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There is our package is already published to NPM:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff7p59rxbk5noyp6d12sq.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff7p59rxbk5noyp6d12sq.png" alt=" " width="800" height="490"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's automate through Github Actions 🐙
&lt;/h2&gt;

&lt;p&gt;Create a .github/workflows directory in your root, and then a publish.yml resembling below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Publish npm package&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;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
  &lt;span class="na"&gt;id-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&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;publish&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@v5&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@v5&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;24&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="na"&gt;registry-url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://registry.npmjs.org&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 --ignore-scripts&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:lib&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 publish ./dist/custom-ui --access public --provenance&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Push it and let's go to npm interface. There click on Settings &amp;gt; in Trusted Publisher, enable GitHub Actions:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frp575fmkbdchmi361ayv.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frp575fmkbdchmi361ayv.png" alt=" " width="800" height="350"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This will ask you in the form for some details you are good to fill out:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fakvuqj5ecyaqbhmzc7dl.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fakvuqj5ecyaqbhmzc7dl.png" alt=" " width="791" height="704"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Finish with Setup connection button and you're done here:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffic66xqpsgqf8bt6slfz.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffic66xqpsgqf8bt6slfz.png" alt=" " width="800" height="268"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Github side: trigger NPM publish 🚀
&lt;/h3&gt;

&lt;p&gt;In your repo, go to Actions &amp;gt; Under Actions, "Publish npm package" &amp;gt; Run workflow&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkna335y0scwsrcizqkiq.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkna335y0scwsrcizqkiq.png" alt=" " width="800" height="555"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As we have defined the trigger workflow_dispatch this means the trigger is manual, let's give it the first go by click on "Run workflow"&lt;/p&gt;

&lt;h3&gt;
  
  
  Beware: Version conflict 🚀
&lt;/h3&gt;

&lt;p&gt;If you followed to the letter, you must have come across the error below:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcocy5u63eauqeqtlxzur.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcocy5u63eauqeqtlxzur.png" alt=" " width="800" height="505"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is quite expressive: NPM cannot publish over previous versions. Let's quickly fix by adding to our projects/custom-ui/package.json:&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="w"&gt;  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0.0.2"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With that, let's try again:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqmk2qqbwhvdfuvnpdhu3.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqmk2qqbwhvdfuvnpdhu3.png" alt=" " width="799" height="298"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And now lovingly crafted 💓, we see it mirrored in your NPM profile:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu9swvb99z22vps9xnnkj.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu9swvb99z22vps9xnnkj.png" alt=" " width="800" height="512"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Achievements 🎉🙌
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Created your first NPM package for reusability in your multiple projects&lt;/li&gt;
&lt;li&gt;Integrated CI/CD for your repo&lt;/li&gt;
&lt;li&gt;Revisit the repo to verify if you missed something: &lt;a href="https://github.com/elparaquecosadeque/npm-test" rel="noopener noreferrer"&gt;https://github.com/elparaquecosadeque/npm-test&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>npm</category>
      <category>angular</category>
      <category>cicd</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Create your first Angular components library</title>
      <dc:creator>Gerardo León</dc:creator>
      <pubDate>Sat, 01 Aug 2026 05:30:58 +0000</pubDate>
      <link>https://dev.to/gerardo_leon/create-your-first-angular-components-library-2co</link>
      <guid>https://dev.to/gerardo_leon/create-your-first-angular-components-library-2co</guid>
      <description>&lt;h2&gt;
  
  
  Build the local lib 🔨🤓
&lt;/h2&gt;

&lt;p&gt;To create an angular components lib, we add an parameter to the usual "ng new":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ng new npm-test &lt;span class="nt"&gt;--create-application&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false

cd &lt;/span&gt;npm-test

ng generate library custom-ui &lt;span class="nt"&gt;--prefix&lt;/span&gt; ui

ng generate component components/button &lt;span class="nt"&gt;--project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;custom-ui
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will get you a simple component called button in projects/custom-ui/src/lib&lt;/p&gt;




&lt;h3&gt;
  
  
  Optional: Build a tester app 📖*️
&lt;/h3&gt;

&lt;p&gt;We create a test app to see what we built in our library. For so, run the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ng generate application library-tester
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There, to invoke the library in the library-tester app, do:&lt;/p&gt;

&lt;p&gt;1) Export the component in *&lt;em&gt;public-api.ts *&lt;/em&gt; by copying the line below:&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="o"&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/components/button.component/button.component&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;2) Import the component in the library-tester app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app.ts&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;Component&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;signal&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;@angular/core&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;RouterOutlet&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;@angular/router&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;ButtonComponent&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;custom-ui&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="nd"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
&lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;app-root&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="na"&gt;imports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;RouterOutlet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ButtonComponent&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="na"&gt;templateUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./app.html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="na"&gt;styleUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./app.css&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;App&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;library-tester&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;// app.html&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ui&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/ui-button&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;Run these to verify in two different terminals:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ng serve &lt;span class="nt"&gt;--project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;library-tester

ng build custom-ui &lt;span class="nt"&gt;--watch&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once done, in your localhost you should get something close to this:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdhkt606m4ise0jcl6h5a.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdhkt606m4ise0jcl6h5a.png" alt=" " width="750" height="275"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's make the component configurable 🌶️🔥
&lt;/h2&gt;

&lt;p&gt;Do the following changes:&lt;/p&gt;

&lt;p&gt;1) In button.component.html:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;    &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"custom-btn"&lt;/span&gt; &lt;span class="na"&gt;[style.background-color]=&lt;/span&gt;&lt;span class="s"&gt;"color"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      {{ text }}
    &lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;2) Your button.component.ts should resemble this:&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;Component&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="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/core&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;CommonModule&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;@angular/common&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="nd"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ui-button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;standalone&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;imports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;CommonModule&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;templateUrl&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.component.html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;styleUrls&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;./button.component.css&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ButtonComponent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Click me&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="nd"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#f68f3b&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;2.a) &lt;em&gt;Optional, add some custom css:&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.custom-btn&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#3b82f6&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;white&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10px&lt;/span&gt; &lt;span class="m"&gt;20px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;pointer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.custom-btn&lt;/span&gt;&lt;span class="nd"&gt;:hover&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#2563eb&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;3) In the library tester's app.html you can now pass those variables in the DOM definition:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;ui-button&lt;/span&gt; &lt;span class="na"&gt;text=&lt;/span&gt;&lt;span class="s"&gt;"Hey there!"&lt;/span&gt; &lt;span class="na"&gt;color=&lt;/span&gt;&lt;span class="s"&gt;"blue"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/ui-button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;By doing all these steps, now your component should look like below:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Floma1ir4oe97qsxywhlb.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Floma1ir4oe97qsxywhlb.png" alt=" " width="345" height="197"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Achievements 🎉🙌
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Created your first Angular library for reusability in your multiple projects&lt;/li&gt;
&lt;li&gt;Keep style, look-and-feel consistency across your apps&lt;/li&gt;
&lt;li&gt;Revisit the repo to verify if you missed something: &lt;a href="https://github.com/elparaquecosadeque/npm-test" rel="noopener noreferrer"&gt;https://github.com/elparaquecosadeque/npm-test&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What's next 🏁🤓
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;In this next article, we will learn to publish our Angular components library to npmjs (NPM) to make it available thru &lt;code&gt;npm install&lt;/code&gt; command&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Happy coding! 👨‍💻&lt;/p&gt;

</description>
      <category>angular</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
