<?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: Renato Silva</title>
    <description>The latest articles on DEV Community by Renato Silva (@renato_silva_71eef0fc385f).</description>
    <link>https://dev.to/renato_silva_71eef0fc385f</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%2F1698909%2F98e99ca4-6bff-40dc-9314-cf98388fbbf3.jpg</url>
      <title>DEV Community: Renato Silva</title>
      <link>https://dev.to/renato_silva_71eef0fc385f</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/renato_silva_71eef0fc385f"/>
    <language>en</language>
    <item>
      <title>Why I Started Rejecting My Own Giant PRs on a Solo Project</title>
      <dc:creator>Renato Silva</dc:creator>
      <pubDate>Thu, 20 Aug 2026 09:28:25 +0000</pubDate>
      <link>https://dev.to/renato_silva_71eef0fc385f/why-i-started-rejecting-my-own-giant-prs-on-a-solo-project-20ao</link>
      <guid>https://dev.to/renato_silva_71eef0fc385f/why-i-started-rejecting-my-own-giant-prs-on-a-solo-project-20ao</guid>
      <description>&lt;p&gt;I don't have teammates on this project. No one is waiting on my PRs, no one is blocked by my branch, and technically I could just push straight to &lt;code&gt;main&lt;/code&gt; and call it a day. For about eight months, that's exactly what I did. Then I started opening pull requests against myself, refusing to merge them until they passed a checklist, and my bug count dropped hard enough that I'm never going back.&lt;/p&gt;

&lt;p&gt;This isn't a productivity larp. It's a direct response to something I kept doing on a Node.js backend for a side project that grew into something people actually pay for: writing 1,200-line PRs that touched routing, database schema, auth middleware, and a new queue system all at once, then merging them at 1am because "it works locally."&lt;/p&gt;

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

&lt;p&gt;Here's an actual PR title from my own history, from back when I didn't bother with PRs at all, just commits:&lt;/p&gt;

&lt;p&gt;commit 4a9f2c1&lt;br&gt;
Author: me&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;add subscription billing, refactor user model, switch to bullmq, fix cors bug
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;One commit. Four unrelated concerns. When something broke in production three weeks later — turned out the user model refactor silently changed how &lt;code&gt;email&lt;/code&gt; uniqueness was enforced — I had no way to bisect it cleanly. &lt;code&gt;git bisect&lt;/code&gt; pointed at a commit that also happened to introduce a queue system, so I spent an hour reading unrelated BullMQ code before I found the actual bug in a Mongoose schema change two files away.&lt;/p&gt;

&lt;p&gt;The mega-PR problem people are complaining about on GitHub right now — the 4,000-line diff nobody can meaningfully review — isn't really a GitHub problem. It's a batching problem. Solo devs get it too, we just don't call it a "review bottleneck" because there's no reviewer to bottleneck. The cost shows up later, as debugging tax instead of review tax.&lt;/p&gt;

&lt;h2&gt;
  
  
  🧩 What Changed
&lt;/h2&gt;

&lt;p&gt;I started treating my own future self as the reviewer. Concretely, that meant three habits, in order of how much they actually helped.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. One deployable concern per PR
&lt;/h3&gt;

&lt;p&gt;Not one &lt;em&gt;file&lt;/em&gt;. One &lt;em&gt;concern&lt;/em&gt;. A PR can touch six files if they all serve the same change. It cannot touch six unrelated changes even if it's technically "one file."&lt;/p&gt;

&lt;p&gt;Before:&lt;/p&gt;

&lt;p&gt;feat: subscription billing, user model refactor, bullmq, cors fix&lt;/p&gt;

&lt;p&gt;After, same work, split into four PRs merged over two days:&lt;/p&gt;

&lt;p&gt;fix: cors origin whitelist for staging subdomain&lt;br&gt;
refactor: normalize email field before uniqueness check&lt;br&gt;
feat: add BullMQ queue for email jobs (behind flag)&lt;br&gt;
feat: enable Stripe subscription billing on user model&lt;/p&gt;

&lt;p&gt;Each of those is independently revertible. When the queue system had a memory leak two weeks later, &lt;code&gt;git revert&lt;/code&gt; on one commit fixed it without touching billing.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Feature flags instead of long-lived branches
&lt;/h3&gt;

&lt;p&gt;The old instinct was to keep a branch alive for a week while I built something big, then merge it all at once — the exact mega-PR pattern. Now I merge small, working pieces behind a flag, even when the feature isn't done.&lt;/p&gt;

&lt;p&gt;javascript&lt;br&gt;
// config/flags.js&lt;br&gt;
const flags = {&lt;br&gt;
  QUEUE_EMAIL_JOBS: process.env.FLAG_QUEUE_EMAIL_JOBS === 'true',&lt;br&gt;
};&lt;/p&gt;

&lt;p&gt;module.exports = flags;&lt;/p&gt;

&lt;p&gt;javascript&lt;br&gt;
// services/emailService.js&lt;br&gt;
const { QUEUE_EMAIL_JOBS } = require('../config/flags');&lt;/p&gt;

&lt;p&gt;async function sendWelcomeEmail(user) {&lt;br&gt;
  if (QUEUE_EMAIL_JOBS) {&lt;br&gt;
    await emailQueue.add('welcome', { userId: user.id });&lt;br&gt;
  } else {&lt;br&gt;
    await mailer.sendNow(user.email, 'welcome');&lt;br&gt;
  }&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;This let me merge the BullMQ integration in small pieces — queue setup, worker process, retry logic — over four separate PRs, none of which changed production behavior until I flipped &lt;code&gt;FLAG_QUEUE_EMAIL_JOBS&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt; in one final, tiny, easy-to-review PR:&lt;/p&gt;

&lt;p&gt;diff&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;FLAG_QUEUE_EMAIL_JOBS=false&lt;/li&gt;
&lt;li&gt;FLAG_QUEUE_EMAIL_JOBS=true&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If that broke something, the rollback was a one-line env change, not a &lt;code&gt;git revert&lt;/code&gt; across four commits with merge conflicts.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. A self-review checklist before I hit merge
&lt;/h3&gt;

&lt;p&gt;This is the part that actually changes behavior, because it forces a pause. Mine lives in &lt;code&gt;.github/pull_request_template.md&lt;/code&gt; and I fill it out even though I'm the only one who reads it:&lt;/p&gt;

&lt;p&gt;markdown&lt;/p&gt;

&lt;h2&gt;
  
  
  Self-review checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] This PR does ONE thing. If I can't summarize it in one sentence, split it.&lt;/li&gt;
&lt;li&gt;[ ] No schema change and feature logic in the same PR.&lt;/li&gt;
&lt;li&gt;[ ] New code path is behind a flag if it touches billing, auth, or queues.&lt;/li&gt;
&lt;li&gt;[ ] I ran this against the staging DB dump, not just local seed data.&lt;/li&gt;
&lt;li&gt;[ ] Rollback plan: revert commit / flip flag / neither needed.&lt;/li&gt;
&lt;li&gt;[ ] Diff is under ~300 lines, or I have a good reason it isn't.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last checkbox alone killed most of my mega-PRs. "Under 300 lines" isn't a magic number — it's just small enough that I can actually reread the whole diff in one sitting and notice the thing I got wrong, instead of skimming because I already know what I meant to write.&lt;/p&gt;

&lt;h2&gt;
  
  
  📉 Before / After, With Real Numbers
&lt;/h2&gt;

&lt;p&gt;I pulled stats from my own git log across a 3-month window before and after adopting this.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Avg lines changed per PR&lt;/td&gt;
&lt;td&gt;640&lt;/td&gt;
&lt;td&gt;145&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Production incidents traced to a merge&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time to &lt;code&gt;git bisect&lt;/code&gt; a regression&lt;/td&gt;
&lt;td&gt;~45 min avg&lt;/td&gt;
&lt;td&gt;~8 min avg&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PRs reverted in full&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;0 (partial reverts only)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The incident count matters most. Five of those six "before" incidents were bugs sitting quietly inside a large diff, unrelated to the actual thing I thought I was shipping. Smaller diffs didn't make me a better programmer overnight — they just made my mistakes smaller and easier to isolate.&lt;/p&gt;

&lt;h2&gt;
  
  
  🚦 Where I Still Cut Corners
&lt;/h2&gt;

&lt;p&gt;I'm not going to pretend this is pure discipline. Genuine one-off scripts, migrations I'll run exactly once, or throwaway debug endpoints still go straight to &lt;code&gt;main&lt;/code&gt; sometimes. The checklist is for anything touching auth, billing, data integrity, or anything a customer would notice if it broke. Applying full ceremony to a typo fix in a README would just be theater.&lt;/p&gt;

&lt;h2&gt;
  
  
  🙋 Your Turn
&lt;/h2&gt;

&lt;p&gt;If you're a solo dev or work on a small team with light review culture — do you actually PR your own work, or is &lt;code&gt;main&lt;/code&gt; still your review process? I'm curious whether feature flags feel like overhead to people working on smaller CRUD apps versus something like billing or queues where the blast radius of a bad merge is bigger.&lt;/p&gt;

&lt;p&gt;Drop your workflow in the comments, especially if you've got a better checklist item than mine — I'm always looking to steal a good one.&lt;/p&gt;

</description>
      <category>node</category>
      <category>git</category>
      <category>codereview</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Your API Doesn't Have an AI Problem, It Has a Design Problem</title>
      <dc:creator>Renato Silva</dc:creator>
      <pubDate>Wed, 19 Aug 2026 19:48:44 +0000</pubDate>
      <link>https://dev.to/renato_silva_71eef0fc385f/your-api-doesnt-have-an-ai-problem-it-has-a-design-problem-1l5f</link>
      <guid>https://dev.to/renato_silva_71eef0fc385f/your-api-doesnt-have-an-ai-problem-it-has-a-design-problem-1l5f</guid>
      <description>&lt;p&gt;Every week there's a new post about "adding AI to your API" — a chat endpoint, a summarization feature, an autocomplete widget. And every week, teams discover the same thing: the AI feature isn't the hard part. The hard part is that their API was never designed to answer real questions in the first place.&lt;/p&gt;

&lt;p&gt;AI doesn't create bad architecture. It just puts a spotlight on it and asks it to perform live, in front of an audience.&lt;/p&gt;

&lt;h2&gt;
  
  
  🔥 The Pattern Nobody Wants to Admit
&lt;/h2&gt;

&lt;p&gt;Here's the usual sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Team builds a CRUD API around whatever tables were easiest to model.&lt;/li&gt;
&lt;li&gt;Product asks for an AI feature — "summarize customer sentiment," "suggest a response," "cluster similar feedback."&lt;/li&gt;
&lt;li&gt;Engineering discovers the API can't answer "similar to what?" or "sentiment over what time window, grouped how?" without a pile of N+1 queries, ad hoc joins, or a background job nobody wants to own.&lt;/li&gt;
&lt;li&gt;Someone ships a &lt;code&gt;/ai/summarize&lt;/code&gt; endpoint that quietly does three database round trips, a Python script, and a prayer.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The AI didn't break the system. The AI just needed the system to answer real, compositional questions — and it turns out the system was only ever designed to answer "give me row 42."&lt;/p&gt;

&lt;h2&gt;
  
  
  🧩 Case Study: minimalist-feedback-api
&lt;/h2&gt;

&lt;p&gt;Let's make this concrete with a small, honest example — a feedback API that looks totally reasonable at first glance.&lt;/p&gt;

&lt;p&gt;sql&lt;br&gt;
CREATE TABLE feedback (&lt;br&gt;
  id SERIAL PRIMARY KEY,&lt;br&gt;
  message TEXT NOT NULL,&lt;br&gt;
  rating INTEGER,&lt;br&gt;
  submitted_at TIMESTAMP DEFAULT now(),&lt;br&gt;
  user_email TEXT&lt;br&gt;
);&lt;/p&gt;

&lt;p&gt;And the API surface:&lt;/p&gt;

&lt;p&gt;http&lt;br&gt;
GET  /feedback&lt;br&gt;
GET  /feedback/:id&lt;br&gt;
POST /feedback&lt;br&gt;
DELETE /feedback/:id&lt;/p&gt;

&lt;p&gt;This is fine for a v1. It's minimal, it's CRUD, it ships fast. The problem is what it's missing: there's no concept of a &lt;em&gt;category&lt;/em&gt;, no &lt;em&gt;tags&lt;/em&gt;, no &lt;em&gt;source&lt;/em&gt; (web, mobile, support ticket), no &lt;em&gt;status&lt;/em&gt; (new, triaged, resolved), and no relationship to a product area or feature. &lt;code&gt;rating&lt;/code&gt; is a bare integer with no scale documented anywhere except a Slack message from eight months ago.&lt;/p&gt;

&lt;p&gt;Nobody complained, because the only client was an admin dashboard doing &lt;code&gt;SELECT * FROM feedback ORDER BY submitted_at DESC LIMIT 50&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  🤖 Where the AI Feature Broke Everything
&lt;/h2&gt;

&lt;p&gt;Then someone asks for: "Can we get an AI summary of feedback trends by feature area, this week vs. last week?"&lt;/p&gt;

&lt;p&gt;Suddenly every missing modeling decision becomes a blocking issue:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;There's no &lt;code&gt;feature_area&lt;/code&gt;, so the LLM prompt starts doing keyword matching on free text ("if message contains 'checkout'...") — which is just a worse, slower, non-deterministic version of a foreign key.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;rating&lt;/code&gt; isn't validated or scaled consistently, so "average sentiment" is comparing 1–5 stars against some rows where someone typed &lt;code&gt;-1&lt;/code&gt; two years ago and it never got caught.&lt;/li&gt;
&lt;li&gt;There's no &lt;code&gt;submitted_at&lt;/code&gt; index strategy for range queries, so "this week vs last week" becomes two full table scans through a text-heavy table, on every request, because there's no caching layer and no aggregation endpoint either.&lt;/li&gt;
&lt;li&gt;The endpoint that gets built to serve this, &lt;code&gt;/ai/summary&lt;/code&gt;, ends up doing the query, the grouping, the prompt construction, and the LLM call all inline, with no separation between "fetch relevant data" and "generate summary," which means you can't cache the first part or test it independently of the model.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;http&lt;br&gt;
GET /ai/summary?range=week&lt;/p&gt;

&lt;p&gt;{&lt;br&gt;
  "summary": "Feedback improved slightly...",&lt;br&gt;
  "note": "best effort, based on keyword matching, may be wrong"&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;That &lt;code&gt;note&lt;/code&gt; field is the tell. It's an apology baked into the response schema.&lt;/p&gt;

&lt;h2&gt;
  
  
  🛠 The Actual Fix: Model the Domain, Not the Table
&lt;/h2&gt;

&lt;p&gt;The fix has almost nothing to do with AI. It's the modeling work that should have happened before anyone typed &lt;code&gt;CREATE TABLE&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;sql&lt;br&gt;
CREATE TABLE feedback (&lt;br&gt;
  id SERIAL PRIMARY KEY,&lt;br&gt;
  message TEXT NOT NULL,&lt;br&gt;
  sentiment_score NUMERIC(3,2), -- normalized -1.0 to 1.0, computed once&lt;br&gt;
  source TEXT NOT NULL,          -- 'web', 'mobile', 'support'&lt;br&gt;
  feature_area_id INTEGER REFERENCES feature_areas(id),&lt;br&gt;
  status TEXT NOT NULL DEFAULT 'new',&lt;br&gt;
  submitted_at TIMESTAMP NOT NULL DEFAULT now(),&lt;br&gt;
  user_id INTEGER REFERENCES users(id)&lt;br&gt;
);&lt;/p&gt;

&lt;p&gt;CREATE INDEX idx_feedback_submitted_at ON feedback (submitted_at);&lt;br&gt;
CREATE INDEX idx_feedback_feature_area ON feedback (feature_area_id);&lt;/p&gt;

&lt;p&gt;And the endpoint set stops being pure CRUD and starts modeling actual questions people ask:&lt;/p&gt;

&lt;p&gt;http&lt;br&gt;
GET /feedback?feature_area=checkout&amp;amp;since=2024-05-01&amp;amp;until=2024-05-08&lt;br&gt;
GET /feedback/aggregate?group_by=feature_area&amp;amp;range=week&lt;br&gt;
GET /feature-areas/:id/trend?window=30d&lt;/p&gt;

&lt;p&gt;Notice what changed: the aggregation is a first-class resource (&lt;code&gt;/feedback/aggregate&lt;/code&gt;), not something invented inline inside an AI endpoint. Now the AI feature is almost boring:&lt;/p&gt;

&lt;p&gt;python&lt;br&gt;
def generate_weekly_summary(feature_area_id: int) -&amp;gt; str:&lt;br&gt;
    trend = api.get(f"/feature-areas/{feature_area_id}/trend?window=7d")&lt;br&gt;
    prompt = build_summary_prompt(trend)  # deterministic, testable&lt;br&gt;
    return llm.complete(prompt)&lt;/p&gt;

&lt;p&gt;The LLM call is now the &lt;em&gt;last&lt;/em&gt; step, operating on well-shaped, pre-aggregated, already-correct data. If the summary is wrong, you can tell immediately whether it's a data problem or a prompting problem — because they're separated.&lt;/p&gt;

&lt;h2&gt;
  
  
  📐 What Good Looks Like
&lt;/h2&gt;

&lt;p&gt;A few concrete rules that fall out of this case study, not as abstract principles but as things you can check in a PR review:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;If an AI feature needs a join your API can't express, that join was always missing.&lt;/strong&gt; The AI request just made it visible faster than a human analyst would have.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aggregation endpoints are not optional sugar.&lt;/strong&gt; &lt;code&gt;/resource/aggregate&lt;/code&gt; or &lt;code&gt;/resource/:id/trend&lt;/code&gt; should exist before anyone builds a summarization feature on top, not as a side effect of building one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free text fields are where schema debt hides.&lt;/strong&gt; &lt;code&gt;message&lt;/code&gt; being a TEXT blob is fine; using string matching against it as a substitute for a &lt;code&gt;feature_area_id&lt;/code&gt; is a design smell wearing an AI costume.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Normalize before you summarize.&lt;/strong&gt; If &lt;code&gt;rating&lt;/code&gt; or &lt;code&gt;sentiment_score&lt;/code&gt; isn't validated at write time, no amount of prompt engineering downstream will make the aggregate trustworthy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep the retrieval and the generation separate, and testable separately.&lt;/strong&gt; If your only way to verify the LLM's output is to eyeball it, you've merged two very different failure modes into one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is AI-specific advice. It's just API design discipline that AI features are unusually good at exposing, because they demand compositional answers instead of row lookups.&lt;/p&gt;

&lt;h2&gt;
  
  
  💬 Over to You
&lt;/h2&gt;

&lt;p&gt;If you added an AI feature to an existing API recently — what actually broke first? Was it the schema, the missing aggregation layer, or something in how endpoints were shaped around CRUD instead of around the questions people actually ask?&lt;/p&gt;

&lt;p&gt;The uncomfortable version of this post is: if the AI feature made your API look bad, the API was already bad. AI is just an unusually blunt code reviewer.&lt;/p&gt;

</description>
      <category>api</category>
      <category>restapi</category>
      <category>softwaredesign</category>
      <category>ai</category>
    </item>
    <item>
      <title>Rate Limiting Lessons From a 100K-Request Meltdown</title>
      <dc:creator>Renato Silva</dc:creator>
      <pubDate>Fri, 14 Aug 2026 20:05:16 +0000</pubDate>
      <link>https://dev.to/renato_silva_71eef0fc385f/rate-limiting-lessons-from-a-100k-request-meltdown-7h0</link>
      <guid>https://dev.to/renato_silva_71eef0fc385f/rate-limiting-lessons-from-a-100k-request-meltdown-7h0</guid>
      <description>&lt;h2&gt;
  
  
  🔥 The Story That Made Every Backend Dev's Stomach Drop
&lt;/h2&gt;

&lt;p&gt;You probably saw it: a developer shipped a React component with a &lt;code&gt;useEffect&lt;/code&gt; that had a missing dependency array (or a state update that retriggered itself), and it quietly hammered their API with &lt;strong&gt;over 100,000 requests&lt;/strong&gt; before anyone noticed. No malicious actor, no botnet — just a bracket in the wrong place and a hook that fired on every render.&lt;/p&gt;

&lt;p&gt;The internet had a good laugh, but every backend dev reading that thread had the same intrusive thought: &lt;em&gt;"my API would've just... died."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That's the uncomfortable truth. A self-inflicted traffic spike from a buggy client is functionally indistinguishable from a DDoS if your server has no defenses. The fix isn't "tell frontend devs to be careful" — it's "assume they won't be, and build accordingly."&lt;/p&gt;

&lt;p&gt;This post walks through the three layers I now consider non-negotiable for any Node/Express API: &lt;strong&gt;token-bucket rate limiting&lt;/strong&gt;, &lt;strong&gt;circuit breakers&lt;/strong&gt;, and &lt;strong&gt;defensive defaults&lt;/strong&gt;. I'll also talk about a real (much smaller, thankfully) spike that hit my side project, &lt;code&gt;minimalist-feedback-api&lt;/code&gt;, and what actually saved it.&lt;/p&gt;

&lt;h2&gt;
  
  
  🪣 Why Token Bucket Beats Fixed Windows
&lt;/h2&gt;

&lt;p&gt;Most people's first rate limiter is a fixed window: "100 requests per minute per IP." It's easy to reason about and easy to implement badly. The problem is the boundary. If your window resets at :00, a client can send 100 requests at 11:59:59 and another 100 at 12:00:01 — 200 requests in two seconds, technically "within limits."&lt;/p&gt;

&lt;p&gt;Token bucket fixes this by modeling capacity as a continuously refilling resource instead of a hard reset:&lt;/p&gt;

&lt;p&gt;js&lt;br&gt;
// tokenBucket.js&lt;br&gt;
class TokenBucket {&lt;br&gt;
  constructor({ capacity, refillRatePerSec }) {&lt;br&gt;
    this.capacity = capacity;&lt;br&gt;
    this.tokens = capacity;&lt;br&gt;
    this.refillRate = refillRatePerSec;&lt;br&gt;
    this.lastRefill = Date.now();&lt;br&gt;
  }&lt;/p&gt;

&lt;p&gt;_refill() {&lt;br&gt;
    const now = Date.now();&lt;br&gt;
    const elapsedSec = (now - this.lastRefill) / 1000;&lt;br&gt;
    const refillAmount = elapsedSec * this.refillRate;&lt;br&gt;
    this.tokens = Math.min(this.capacity, this.tokens + refillAmount);&lt;br&gt;
    this.lastRefill = now;&lt;br&gt;
  }&lt;/p&gt;

&lt;p&gt;tryConsume(cost = 1) {&lt;br&gt;
    this._refill();&lt;br&gt;
    if (this.tokens &amp;gt;= cost) {&lt;br&gt;
      this.tokens -= cost;&lt;br&gt;
      return true;&lt;br&gt;
    }&lt;br&gt;
    return false;&lt;br&gt;
  }&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;module.exports = TokenBucket;&lt;/p&gt;

&lt;p&gt;Then the Express middleware, keyed per client (IP, API key, whatever identifies the caller):&lt;/p&gt;

&lt;p&gt;js&lt;br&gt;
// rateLimitMiddleware.js&lt;br&gt;
const TokenBucket = require('./tokenBucket');&lt;/p&gt;

&lt;p&gt;const buckets = new Map();&lt;/p&gt;

&lt;p&gt;function getBucket(key) {&lt;br&gt;
  if (!buckets.has(key)) {&lt;br&gt;
    buckets.set(key, new TokenBucket({ capacity: 20, refillRatePerSec: 2 }));&lt;br&gt;
  }&lt;br&gt;
  return buckets.get(key);&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;function rateLimit(req, res, next) {&lt;br&gt;
  const key = req.ip; // swap for API key if you have auth&lt;br&gt;
  const bucket = getBucket(key);&lt;/p&gt;

&lt;p&gt;if (bucket.tryConsume(1)) {&lt;br&gt;
    return next();&lt;br&gt;
  }&lt;/p&gt;

&lt;p&gt;res.status(429).set('Retry-After', '1').json({&lt;br&gt;
    error: 'Too many requests. Slow down.',&lt;br&gt;
  });&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;module.exports = rateLimit;&lt;/p&gt;

&lt;p&gt;The key insight: capacity 20 with a refill rate of 2/sec means a client gets a &lt;em&gt;burst allowance&lt;/em&gt; (handles legitimate rapid-fire usage like a form autosave) but can't sustain more than 2 requests/sec indefinitely. That's exactly the shape of a runaway &lt;code&gt;useEffect&lt;/code&gt; loop — it doesn't send 100 requests once, it sends them in a tight, sustained burst. Token bucket catches that pattern where a naive fixed window might not, depending on where the boundaries land.&lt;/p&gt;

&lt;p&gt;For anything beyond a single process, don't keep buckets in memory — use Redis (via something like &lt;code&gt;rate-limiter-flexible&lt;/code&gt;) so limits survive restarts and work across horizontally scaled instances. In-memory &lt;code&gt;Map&lt;/code&gt; is fine for a single-instance side project; it's a liability the moment you run two replicas behind a load balancer, because each instance tracks its own bucket and your effective limit doubles per replica.&lt;/p&gt;

&lt;h2&gt;
  
  
  🧯 Circuit Breakers: The Second Line of Defense
&lt;/h2&gt;

&lt;p&gt;Rate limiting protects your API from too many &lt;em&gt;incoming&lt;/em&gt; requests. Circuit breakers protect your API (and its downstream dependencies) from cascading failure once something's already struggling — usually a database, a third-party API, or an internal service call that's gone slow or unresponsive.&lt;/p&gt;

&lt;p&gt;Here's the pattern with &lt;code&gt;opossum&lt;/code&gt;, a solid circuit breaker library for Node:&lt;/p&gt;

&lt;p&gt;js&lt;br&gt;
const CircuitBreaker = require('opossum');&lt;br&gt;
const db = require('./db');&lt;/p&gt;

&lt;p&gt;async function fetchFeedback(id) {&lt;br&gt;
  return db.query('SELECT * FROM feedback WHERE id = $1', [id]);&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;const breakerOptions = {&lt;br&gt;
  timeout: 3000,              // fail fast after 3s&lt;br&gt;
  errorThresholdPercentage: 50, // trip if 50% of requests fail&lt;br&gt;
  resetTimeout: 10000,        // try again after 10s&lt;br&gt;
};&lt;/p&gt;

&lt;p&gt;const breaker = new CircuitBreaker(fetchFeedback, breakerOptions);&lt;/p&gt;

&lt;p&gt;breaker.fallback(() =&amp;gt; ({ error: 'Feedback service temporarily unavailable' }));&lt;/p&gt;

&lt;p&gt;app.get('/feedback/:id', async (req, res) =&amp;gt; {&lt;br&gt;
  const result = await breaker.fire(req.params.id);&lt;br&gt;
  res.json(result);&lt;br&gt;
});&lt;/p&gt;

&lt;p&gt;Without this, a slow database under load doesn't just cause slow responses — it causes &lt;em&gt;request pileup&lt;/em&gt;. Every incoming request holds a connection open waiting on a query that's never coming back fast enough, you exhaust your connection pool, and now healthy requests fail too. The circuit breaker trips, starts returning fast fallbacks immediately, and gives the database room to recover instead of getting buried under retries.&lt;/p&gt;

&lt;p&gt;Rate limiting stops the flood at the door. Circuit breakers stop one struggling dependency from taking the whole system down with it. You want both — they solve different failure modes.&lt;/p&gt;

&lt;h2&gt;
  
  
  🛡️ Defensive Defaults I Now Bake Into Every Express API
&lt;/h2&gt;

&lt;p&gt;Beyond the two big patterns above, there's a checklist of small things that cost nothing to add and save you on a bad day:&lt;/p&gt;

&lt;p&gt;js&lt;br&gt;
const express = require('express');&lt;br&gt;
const helmet = require('helmet');&lt;br&gt;
const compression = require('compression');&lt;/p&gt;

&lt;p&gt;const app = express();&lt;/p&gt;

&lt;p&gt;// Cap body size — don't let a malformed client send you a 500MB payload&lt;br&gt;
app.use(express.json({ limit: '100kb' }));&lt;/p&gt;

&lt;p&gt;// Basic security headers&lt;br&gt;
app.use(helmet());&lt;/p&gt;

&lt;p&gt;// Compress responses to reduce bandwidth under load&lt;br&gt;
app.use(compression());&lt;/p&gt;

&lt;p&gt;// Global request timeout so nothing hangs forever&lt;br&gt;
app.use((req, res, next) =&amp;gt; {&lt;br&gt;
  res.setTimeout(10000, () =&amp;gt; {&lt;br&gt;
    res.status(503).json({ error: 'Request timed out' });&lt;br&gt;
  });&lt;br&gt;
  next();&lt;br&gt;
});&lt;/p&gt;

&lt;p&gt;// Always have a catch-all error handler, even if it feels redundant&lt;br&gt;
app.use((err, req, res, next) =&amp;gt; {&lt;br&gt;
  console.error(err);&lt;br&gt;
  res.status(500).json({ error: 'Something went wrong' });&lt;br&gt;
});&lt;/p&gt;

&lt;p&gt;None of this is exciting. That's the point — defensive defaults are boring on purpose. The bracket-typo story went viral precisely because the API had no boring safety net, and 100,000 requests met zero resistance.&lt;/p&gt;

&lt;h2&gt;
  
  
  📈 The Day minimalist-feedback-api Got Hit
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;minimalist-feedback-api&lt;/code&gt; is a small feedback-collection service I built as a learning project — nothing fancy, just an endpoint for apps to POST feedback and a dashboard to read it. It's not built to handle enterprise traffic, but I treated it like production because that's where you actually learn this stuff.&lt;/p&gt;

&lt;p&gt;A few months in, one integrator's frontend had a retry loop with no backoff — every failed request immediately retried, and a brief blip in their own network turned into a sustained burst against my &lt;code&gt;/feedback&lt;/code&gt; endpoint. It wasn't 100,000 requests, but it was enough (a few thousand in under a minute) to be a real stress test.&lt;/p&gt;

&lt;p&gt;What actually saved it wasn't anything clever — it was the boring stuff: the token bucket limiter returned 429s immediately instead of letting requests queue up, the body size cap meant even the retries were cheap to reject, and the circuit breaker around my database call meant the brief connection pressure never turned into a full outage. The service degraded gracefully (some legitimate requests got 429'd too) instead of falling over entirely. That's the tradeoff you're signing up for: rate limiting means occasionally rejecting a request that would've been fine, in exchange for never going fully down.&lt;/p&gt;

&lt;h2&gt;
  
  
  🤔 What Would Your API Do?
&lt;/h2&gt;

&lt;p&gt;Honestly ask yourself: if a client-side bug sent your busiest endpoint 100,000 requests in five minutes right now, what would happen? Would it 429 gracefully, or would your database connection pool just... give up?&lt;/p&gt;

&lt;p&gt;If you're not sure, that uncertainty is the signal to add a rate limiter today — even a basic one. It's a couple hours of work that turns a viral "oops" story into a boring non-event. What's your go-to rate limiting setup, and have you ever had a spike (accidental or not) actually test it for real? I'd love to hear the war stories in the comments.&lt;/p&gt;

</description>
      <category>node</category>
      <category>express</category>
      <category>backend</category>
      <category>api</category>
    </item>
    <item>
      <title>I Stopped Trusting AI Agents With My API</title>
      <dc:creator>Renato Silva</dc:creator>
      <pubDate>Fri, 14 Aug 2026 19:51:29 +0000</pubDate>
      <link>https://dev.to/renato_silva_71eef0fc385f/i-stopped-trusting-ai-agents-with-my-api-117n</link>
      <guid>https://dev.to/renato_silva_71eef0fc385f/i-stopped-trusting-ai-agents-with-my-api-117n</guid>
      <description>&lt;h2&gt;
  
  
  🤖 The Problem
&lt;/h2&gt;

&lt;p&gt;A few weeks ago I wired an LLM agent up to &lt;code&gt;minimalist-feedback-api&lt;/code&gt;, my little side project for collecting product feedback. The pitch to myself was simple: let a support-bot agent read feedback threads and occasionally write a triage note or close a stale ticket, without me manually reviewing every call.&lt;/p&gt;

&lt;p&gt;It took about two days for the agent to do something I didn't ask for.&lt;/p&gt;

&lt;p&gt;Nothing catastrophic — it bulk-updated the status of a dozen feedback items because it decided, on its own, that they were "resolved" based on a fuzzy read of the conversation. Technically it used an endpoint I'd exposed to it. Technically the request was authenticated. But nobody had actually agreed that an agent should be allowed to do bulk writes, and there was no record of &lt;em&gt;why&lt;/em&gt; it thought that was a good idea.&lt;/p&gt;

&lt;p&gt;That's the part that got me. With a human client, a bad API call is a bug. With an agent, a bad API call is a &lt;em&gt;decision&lt;/em&gt;, made by a system that can also decide to make it again, faster, in a loop, at 3am.&lt;/p&gt;

&lt;p&gt;So I stopped trusting agents with the same trust model I give human-driven clients, and built a gatekeeper middleware specifically for tool-calling traffic.&lt;/p&gt;

&lt;h2&gt;
  
  
  🔐 What "Trust" Even Means for an Agent
&lt;/h2&gt;

&lt;p&gt;Before writing code, I had to get concrete about what I was actually worried about. It came down to three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Scope&lt;/strong&gt; — an API key belonging to "the support agent" should not be able to call every write endpoint just because it's authenticated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate&lt;/strong&gt; — agents don't get bored or embarrassed. A misbehaving loop can hit your API way harder than a person ever would.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit&lt;/strong&gt; — when something weird happens, I need to reconstruct not just &lt;em&gt;what&lt;/em&gt; was called, but &lt;em&gt;which agent, with what identity, doing what it claimed to be doing.&lt;/em&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Regular auth middleware answers "who are you." This needed to answer "are you allowed to do &lt;em&gt;this specific thing&lt;/em&gt;, right now, at this rate, and is someone going to know about it."&lt;/p&gt;

&lt;h2&gt;
  
  
  🏗️ The Shape of the Middleware
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;minimalist-feedback-api&lt;/code&gt; has a handful of write endpoints: create feedback, update status, delete feedback, bulk operations. I treated agent access as a distinct concern from normal API auth — it sits &lt;em&gt;after&lt;/em&gt; authentication and &lt;em&gt;before&lt;/em&gt; the route handler.&lt;/p&gt;

&lt;p&gt;js&lt;br&gt;
// middleware/agentGatekeeper.js&lt;br&gt;
const agentScopes = {&lt;br&gt;
  'agent:support-triage': ['feedback:update-status', 'feedback:read'],&lt;br&gt;
  'agent:analytics-readonly': ['feedback:read'],&lt;br&gt;
};&lt;/p&gt;

&lt;p&gt;function requireAgentScope(action) {&lt;br&gt;
  return (req, res, next) =&amp;gt; {&lt;br&gt;
    const agentId = req.headers['x-agent-id'];&lt;br&gt;
    const agentToken = req.headers['x-agent-token'];&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if (!agentId) {
  // Not an agent request, let normal auth handle it
  return next();
}

if (!verifyAgentToken(agentId, agentToken)) {
  return res.status(401).json({ error: 'invalid agent credentials' });
}

const allowed = agentScopes[agentId] || [];
if (!allowed.includes(action)) {
  return res.status(403).json({
    error: `agent '${agentId}' is not scoped for action '${action}'`,
  });
}

req.agent = { id: agentId, action };
next();
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

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

&lt;p&gt;module.exports = { requireAgentScope };&lt;/p&gt;

&lt;p&gt;The key decision here: &lt;strong&gt;scopes are actions, not endpoints.&lt;/strong&gt; &lt;code&gt;feedback:update-status&lt;/code&gt; and &lt;code&gt;feedback:delete&lt;/code&gt; are separate permissions even though they might hit similar routes, because "update a status field" and "permanently delete a record" are very different risk levels. My support-triage agent gets the former, never the latter. No agent in this system currently has delete access, on purpose — if it needs to happen, a human does it.&lt;/p&gt;

&lt;h2&gt;
  
  
  🚦 Rate-Limiting Per Agent, Not Per IP
&lt;/h2&gt;

&lt;p&gt;Standard rate limiters key off IP address, which is close to useless for agents — they usually run from the same handful of server IPs as your other backend traffic. I keyed limiting off the agent identity instead, with tighter windows than I'd ever apply to a human-facing key:&lt;/p&gt;

&lt;p&gt;js&lt;br&gt;
const rateLimit = require('express-rate-limit');&lt;/p&gt;

&lt;p&gt;const agentWriteLimiter = rateLimit({&lt;br&gt;
  windowMs: 60 * 1000,&lt;br&gt;
  max: 5, // an agent doing &amp;gt;5 writes/min is suspicious, full stop&lt;br&gt;
  keyGenerator: (req) =&amp;gt; req.agent?.id || req.ip,&lt;br&gt;
  handler: (req, res) =&amp;gt; {&lt;br&gt;
    logAgentEvent({&lt;br&gt;
      agentId: req.agent?.id,&lt;br&gt;
      action: req.agent?.action,&lt;br&gt;
      outcome: 'rate_limited',&lt;br&gt;
    });&lt;br&gt;
    res.status(429).json({ error: 'agent rate limit exceeded' });&lt;br&gt;
  },&lt;br&gt;
});&lt;/p&gt;

&lt;p&gt;Five writes a minute felt aggressive when I set it, but it's forced something useful: if the agent legitimately needs to do more than that, it should be batching its reasoning into fewer, larger, more deliberate calls — not firing off a write per sentence of its own chain of thought.&lt;/p&gt;

&lt;h2&gt;
  
  
  📝 Auditing: The Part I Actually Use Every Day
&lt;/h2&gt;

&lt;p&gt;Scopes and rate limits prevent damage. The audit log is what lets me &lt;em&gt;trust&lt;/em&gt; the system incrementally instead of all-or-nothing. Every agent-originated write gets logged with enough context to answer "why did this happen" without me guessing:&lt;/p&gt;

&lt;p&gt;js&lt;br&gt;
function auditAgentWrite(req, res, next) {&lt;br&gt;
  const original = res.json.bind(res);&lt;br&gt;
  res.json = (body) =&amp;gt; {&lt;br&gt;
    if (req.agent) {&lt;br&gt;
      logAgentEvent({&lt;br&gt;
        agentId: req.agent.id,&lt;br&gt;
        action: req.agent.action,&lt;br&gt;
        method: req.method,&lt;br&gt;
        path: req.originalUrl,&lt;br&gt;
        requestBody: req.body,&lt;br&gt;
        statusCode: res.statusCode,&lt;br&gt;
        timestamp: new Date().toISOString(),&lt;br&gt;
      });&lt;br&gt;
    }&lt;br&gt;
    return original(body);&lt;br&gt;
  };&lt;br&gt;
  next();&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;Wiring it all together on a real route looks like this:&lt;/p&gt;

&lt;p&gt;js&lt;br&gt;
router.patch(&lt;br&gt;
  '/feedback/:id/status',&lt;br&gt;
  requireAgentScope('feedback:update-status'),&lt;br&gt;
  agentWriteLimiter,&lt;br&gt;
  auditAgentWrite,&lt;br&gt;
  updateFeedbackStatus,&lt;br&gt;
);&lt;/p&gt;

&lt;p&gt;The log entries go to a plain table (&lt;code&gt;agent_audit_log&lt;/code&gt;) rather than a generic app log stream, because I wanted to query it directly: "show me every write &lt;code&gt;agent:support-triage&lt;/code&gt; made in the last 24 hours" is a query I actually run now, especially after a prompt or model change.&lt;/p&gt;

&lt;h2&gt;
  
  
  ⚖️ Trade-offs I'm Consciously Accepting
&lt;/h2&gt;

&lt;p&gt;This isn't zero-cost. Static scope tables mean I have to redeploy to change what an agent can do — I'm fine with that friction on purpose, because "redeploy to expand agent permissions" is a feature, not a bug, at this stage. A more dynamic, database-backed scope system would remove the friction and also remove the forcing function that makes me think twice.&lt;/p&gt;

&lt;p&gt;I also don't do anything fancy with the audit data yet — no anomaly detection, no auto-revocation. It's a log I read. That's deliberately unglamorous; I'd rather have a boring, reliable trail than a clever system I don't fully understand when it fires.&lt;/p&gt;

&lt;h2&gt;
  
  
  🙋 Over to You
&lt;/h2&gt;

&lt;p&gt;If you're letting an agent call real write endpoints today, what's actually stopping it from doing something scoped, rate-limited access wouldn't have caught anyway? I'm curious whether people are seeing failure modes that permission systems can't touch — like an agent staying &lt;em&gt;within&lt;/em&gt; scope but still making bad judgment calls.&lt;/p&gt;

&lt;p&gt;Happy to share the full &lt;code&gt;minimalist-feedback-api&lt;/code&gt; gatekeeper module if there's interest — it's small enough to drop into most Express projects in an afternoon.&lt;/p&gt;

</description>
      <category>node</category>
      <category>express</category>
      <category>security</category>
      <category>ai</category>
    </item>
    <item>
      <title>Is Node.js Losing Its Crown? The Rise of Bun, Deno, and Native Runtimes</title>
      <dc:creator>Renato Silva</dc:creator>
      <pubDate>Mon, 10 Aug 2026 09:26:08 +0000</pubDate>
      <link>https://dev.to/renato_silva_71eef0fc385f/is-nodejs-losing-its-crown-the-rise-of-bun-deno-and-native-runtimes-40kf</link>
      <guid>https://dev.to/renato_silva_71eef0fc385f/is-nodejs-losing-its-crown-the-rise-of-bun-deno-and-native-runtimes-40kf</guid>
      <description>&lt;p&gt;For more than a decade, if you wanted to build a JavaScript or TypeScript backend, there was only one real answer: &lt;strong&gt;Node.js&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It revolutionized web development, gave birth to the massive &lt;code&gt;npm&lt;/code&gt; ecosystem, and powered millions of applications worldwide. But nothing in tech stays static forever.&lt;/p&gt;

&lt;p&gt;In recent times, we’ve seen a massive shift. Developers are no longer taking Node.js for granted. Tools like &lt;strong&gt;Bun&lt;/strong&gt; and &lt;strong&gt;Deno&lt;/strong&gt; are no longer experimental projects—they are mature, production-ready runtimes that are directly challenging the king.&lt;/p&gt;

&lt;p&gt;Why is this happening, and should you consider switching your next project away from Node.js?&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The Speed Dilemma (Zig &amp;amp; Rust vs. C++)
&lt;/h2&gt;

&lt;p&gt;Node.js is built on top of Google's V8 engine and C++. It’s fast, but it carries over 15 years of legacy architecture.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Deno&lt;/strong&gt; was created by Ryan Dahl (the original creator of Node.js!) using &lt;strong&gt;Rust&lt;/strong&gt; to fix the security and architectural design flaws he regretted in Node.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bun&lt;/strong&gt; was built from scratch using &lt;strong&gt;Zig&lt;/strong&gt; and the JavaScriptCore engine (from Safari), specifically optimized for raw speed, lower memory footprint, and instantaneous cold starts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When you run benchmarks on HTTP server throughput, package installation speeds, or file I/O operations, Bun often leaves Node.js in the dust. Running &lt;code&gt;bun install&lt;/code&gt; feels like a magic trick compared to &lt;code&gt;npm install&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. All-in-One Tooling vs. "Tooling Fatigue"
&lt;/h2&gt;

&lt;p&gt;To build a modern TypeScript backend in Node.js, you usually need a constellation of extra tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;tsc&lt;/code&gt; or &lt;code&gt;esbuild&lt;/code&gt; for TypeScript compilation.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tsx&lt;/code&gt; or &lt;code&gt;ts-node&lt;/code&gt; for running scripts during development.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dotenv&lt;/code&gt; for environment variables.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Jest&lt;/code&gt; or &lt;code&gt;Vitest&lt;/code&gt; for running tests.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Bun and Deno completely eliminate this friction.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Both runtimes feature &lt;strong&gt;native TypeScript support&lt;/strong&gt; out of the box—no transpilation step required. They include built-in test runners, environment variable support, and even native bundlers. You clone a project, run one command, and everything just works.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. The Counter-Attack: Node.js Isn't Standing Still
&lt;/h2&gt;

&lt;p&gt;If you think the Node.js core team is sitting idly by, think again. The competition from Bun and Deno has been the best thing to happen to Node.js in years!&lt;/p&gt;

&lt;p&gt;Node.js has been aggressively shipping modern features to stay competitive:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Native &lt;code&gt;.env&lt;/code&gt; file parsing support.&lt;/li&gt;
&lt;li&gt;Built-in test runner (&lt;code&gt;node --test&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Experimental support for running TypeScript files directly.&lt;/li&gt;
&lt;li&gt;Significant performance improvements in HTTP and file system operations.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Node’s biggest superpower remains its &lt;strong&gt;unmatched ecosystem and stability&lt;/strong&gt;. Enterprise companies with millions of lines of code aren't going to migrate away from Node.js overnight just for a few milliseconds of performance gain.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Verdict: Which One Should You Use?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use Node.js&lt;/strong&gt; if you are building enterprise applications where long-term stability, massive community support, and ecosystem compatibility are non-negotiable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use Bun&lt;/strong&gt; if you are building high-performance microservices, CLI tools, or want an insanely fast development cycle with zero-config TypeScript.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use Deno&lt;/strong&gt; if security, strict web-standard APIs, and modern runtime architecture are your top priorities.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The "monopoly" of Node.js is over, and that’s a win for all developers. Competition breeds innovation.&lt;/p&gt;




&lt;h2&gt;
  
  
  What about you?
&lt;/h2&gt;

&lt;p&gt;Have you tried Bun or Deno in production, or are you sticking with Node.js for your daily work? What’s keeping you from making the switch?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Drop your thoughts and benchmarks in the comments below! ⚡👇&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>node</category>
      <category>bunjs</category>
      <category>backend</category>
    </item>
    <item>
      <title>How to Survive a Live Coding Interview Without Having a Panic Attack</title>
      <dc:creator>Renato Silva</dc:creator>
      <pubDate>Sun, 28 Jun 2026 15:16:40 +0000</pubDate>
      <link>https://dev.to/renato_silva_71eef0fc385f/how-to-survive-a-live-coding-interview-without-having-a-panic-attack-3nli</link>
      <guid>https://dev.to/renato_silva_71eef0fc385f/how-to-survive-a-live-coding-interview-without-having-a-panic-attack-3nli</guid>
      <description>&lt;p&gt;It’s the moment every developer dreads. &lt;/p&gt;

&lt;p&gt;You passed the initial screening, you know your tech stack inside out, and now you’re sitting on a Zoom call. The interviewer drops a link to a shared editor and says: &lt;em&gt;"Alright, here is the problem. Please share your screen and code the solution while explaining your thought process."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Suddenly, your hands start sweating. Your mind goes completely blank. You forget how a basic &lt;code&gt;for&lt;/code&gt; loop works, and you start questioning if you even know how to program at all.&lt;/p&gt;

&lt;p&gt;If you have ever experienced this, let me tell you a secret: &lt;strong&gt;You are not a bad developer. Live coding is just a fundamentally unnatural way to write software.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here is a survival guide on how to manage the anxiety, prepare effectively, and turn the interview into a conversation rather than an interrogation.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Shift Your Mindset: It’s Not About the Solution
&lt;/h2&gt;

&lt;p&gt;The biggest mistake candidates make is thinking that if they don’t finish the code or if it has a small bug, they failed. &lt;/p&gt;

&lt;p&gt;In 90% of professional tech interviews, the interviewer cares much more about &lt;strong&gt;how you think&lt;/strong&gt; than whether you get a 100% perfect syntax on the first try. They want to see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How do you react when you get stuck? (Do you panic, or do you ask questions?)&lt;/li&gt;
&lt;li&gt;Can you break a big problem into smaller pieces?&lt;/li&gt;
&lt;li&gt;Are you pleasant to work with?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Remember: They are looking for a future &lt;em&gt;colleague&lt;/em&gt;, not a compiler.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The "Think Out Loud" Framework (Your Superpower)
&lt;/h2&gt;

&lt;p&gt;Silence is your worst enemy during a live coding session. If you are silent, the interviewer has no idea if you are thinking of a brilliant solution or completely lost.&lt;/p&gt;

&lt;p&gt;Force yourself to speak. Explain your chaotic thoughts:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Okay, I’m thinking we need to filter this array first, but since the data structure is nested, I might need to normalize it. Let me try a simple approach first, and we can optimize it later."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you say this out loud, two amazing things happen:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It slows your heart rate down because you are pacing yourself.&lt;/li&gt;
&lt;li&gt;If you are going down a completely wrong path, a good interviewer will usually drop a hint to guide you back (&lt;em&gt;"That makes sense, but what if the array is empty?"&lt;/em&gt;).&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  3. The Practical Checklist to Stay Calm
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Ask Clarifying Questions First:&lt;/strong&gt; Never start typing immediately. Spend the first 3 minutes asking about edge cases. &lt;em&gt;"Can the input be null?", "Should this handle negative numbers?"&lt;/em&gt; This gives your brain time to calm down and process the problem.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Write Pseudo-code:&lt;/strong&gt; Before writing syntax, write comments.&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 1. Get the category ID from the input&lt;/span&gt;
&lt;span class="c1"&gt;// 2. Check if the category exists in the database&lt;/span&gt;
&lt;span class="c1"&gt;// 3. Return error or proceed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;This gives you a roadmap. If you freeze mid-way, you just need to look at your next comment.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Admit What You Don't Know:&lt;/strong&gt; If you forget a specific JavaScript method name, don't fake it. Say: &lt;em&gt;"I can't recall the exact native method name for this right now, so I'm going to create a placeholder function/variable called &lt;code&gt;formatData&lt;/code&gt; and come back to it."&lt;/em&gt; Interviewers respect this level of honesty.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  4. How to Actually Prepare (Without Burning Out)
&lt;/h2&gt;

&lt;p&gt;Don't just grind 500 LeetCode problems. That will only increase your anxiety. Instead:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Practice talking while coding:&lt;/strong&gt; Open an old project or a simple challenge, record yourself on Zoom, and force yourself to explain your code out loud to an empty room. It feels silly, but it builds the muscle memory.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Mock Interviews:&lt;/strong&gt; Ask a developer friend to give you a random problem and watch you solve it for 30 minutes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Live coding is a performance. And just like any performance, the more you practice the act of &lt;em&gt;performing&lt;/em&gt;, the less terrifying it becomes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Over to you...
&lt;/h2&gt;

&lt;p&gt;What is your relationship with live coding interviews? Do you think they are a valid way to test a developer's skills, or should the industry ban them forever? &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Share your worst (or best) interview stories in the comments! 👇&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>career</category>
      <category>interview</category>
      <category>programming</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Your node_modules is Heavier Than a Black Hole (And How to Fix It)</title>
      <dc:creator>Renato Silva</dc:creator>
      <pubDate>Tue, 16 Jun 2026 18:16:30 +0000</pubDate>
      <link>https://dev.to/renato_silva_71eef0fc385f/your-nodemodules-is-heavier-than-a-black-hole-and-how-to-fix-it-32jl</link>
      <guid>https://dev.to/renato_silva_71eef0fc385f/your-nodemodules-is-heavier-than-a-black-hole-and-how-to-fix-it-32jl</guid>
      <description>&lt;p&gt;We’ve all seen the meme: a black hole, a highway collapsing, or the universe warping under the unimaginable weight of a single folder named &lt;code&gt;node_modules&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It used to be a joke. Today, it’s a production hazard.&lt;/p&gt;

&lt;p&gt;If you create a fresh project using some modern meta-frameworks or tools, before you even write your first &lt;code&gt;console.log()&lt;/code&gt;, you already have &lt;strong&gt;tens of thousands of files&lt;/strong&gt; sitting in your directory. We have reached a point where we need thousands of external packages just to render text on a screen or route a basic HTTP request.&lt;/p&gt;

&lt;p&gt;How did the JavaScript ecosystem become so heavily dependent on others, and why is this breaking modern software engineering?&lt;/p&gt;




&lt;h2&gt;
  
  
  The "Left-Pad" Trauma and the Security Nightmare
&lt;/h2&gt;

&lt;p&gt;A few years ago, the internet famously broke because a developer unpublished a tiny 11-line package called &lt;code&gt;left-pad&lt;/code&gt;. Today, the problem is much worse, but it’s silent. It hides under the name of &lt;strong&gt;Supply Chain Attacks&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When you install a major framework, you aren't just trusting that framework. You are trusting the hundreds of anonymous open-source developers who wrote the micro-dependencies &lt;em&gt;that the framework relies on&lt;/em&gt;. &lt;/p&gt;

&lt;p&gt;Every time you run &lt;code&gt;npm install&lt;/code&gt;, you are playing roulette with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Malicious packages disguised as typos.&lt;/li&gt;
&lt;li&gt;Deprecated code running in your production environment.&lt;/li&gt;
&lt;li&gt;The infamous &lt;code&gt;Found 87 vulnerabilities (12 critical)&lt;/code&gt; message that nobody actually knows how to completely clear.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Art of "No-Dependency" Coding
&lt;/h2&gt;

&lt;p&gt;A silent revolution is happening. Senior developers are actively looking at their &lt;code&gt;package.json&lt;/code&gt; and asking: &lt;strong&gt;"Can I write this myself in 10 lines of code instead of installing a 5MB package?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Modern runtimes like Node.js (with its native test runners and &lt;code&gt;.env&lt;/code&gt; support), Bun, and Deno are trying to cure this sickness by built-in tools that eliminate the need for basic third-party utilities.&lt;/p&gt;

&lt;p&gt;Look at our backend stacks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do you really need a massive utility library just to capitalize a string or filter an array? &lt;strong&gt;No, native JavaScript array methods are incredibly fast now.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Do you need a dependency to format a date? &lt;strong&gt;Often, the native &lt;code&gt;Intl.DateTimeFormat&lt;/code&gt; API does exactly what you want.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keeping your code close to the metal (or native to the runtime) makes your application faster, secure, and infinitely easier to upgrade.&lt;/p&gt;




&lt;h2&gt;
  
  
  Finding the Sweet Spot
&lt;/h2&gt;

&lt;p&gt;Don't get me wrong. I am not saying you should build your own ORM instead of using Prisma, or rewrite Fastify from scratch. Frameworks and complex tools solve massive, hard problems, and they deserve to be in your project.&lt;/p&gt;

&lt;p&gt;The problem is the &lt;strong&gt;micro-dependency addiction&lt;/strong&gt;. Installing an entire package just to check if a number is even (yes, &lt;code&gt;is-even&lt;/code&gt; is a real package with millions of downloads) isn't smart engineering—it's laziness.&lt;/p&gt;

&lt;p&gt;The next time you are tempted to run &lt;code&gt;npm install &amp;lt;package&amp;gt;&lt;/code&gt;, take 2 minutes to think: &lt;em&gt;Can I write a simple, typed function to handle this?&lt;/em&gt; Your deployment speed, your security team, and your laptop's hard drive will thank you.&lt;/p&gt;




&lt;h2&gt;
  
  
  Time to confess...
&lt;/h2&gt;

&lt;p&gt;What is the most ridiculous, tiny package you have ever found buried deep inside your &lt;code&gt;node_modules&lt;/code&gt;? Are we too lazy to write plain JavaScript/TypeScript nowadays?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Let’s discuss (and share our dependency horror stories) below! 📦👇&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>node</category>
      <category>webdev</category>
      <category>architecture</category>
    </item>
    <item>
      <title>How I Stopped Losing Track of Clients, Invoices, and Money as a Freelancer (And the System I Built to Fix It)</title>
      <dc:creator>Renato Silva</dc:creator>
      <pubDate>Wed, 10 Jun 2026 16:29:42 +0000</pubDate>
      <link>https://dev.to/renato_silva_71eef0fc385f/how-i-stopped-losing-track-of-clients-invoices-and-money-as-a-freelancer-and-the-system-i-built-2egk</link>
      <guid>https://dev.to/renato_silva_71eef0fc385f/how-i-stopped-losing-track-of-clients-invoices-and-money-as-a-freelancer-and-the-system-i-built-2egk</guid>
      <description>&lt;p&gt;If you've been freelancing for more than a few months, you know the feeling: a client emails asking about an invoice you're not sure you sent. A project deadline sneaks up because it was buried in a note somewhere. You check your bank account and can't tell if that deposit was from the March project or the April one.&lt;/p&gt;

&lt;p&gt;It's not that you're disorganized. It's that freelancing forces you to be a designer AND an accountant AND a project manager AND a sales team — all at once, with no system holding it together.&lt;/p&gt;

&lt;p&gt;I spent way too long managing my freelance work across scattered tools: spreadsheets for invoices, a to-do app for tasks, email threads for client info, a notes app for project details. Everything lived somewhere different, and nothing talked to anything else.&lt;/p&gt;

&lt;p&gt;So I built a system in Notion that connects everything into one workspace. Here's the framework behind it — whether you use my template or build your own.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 5 things every freelancer needs to track
&lt;/h2&gt;

&lt;p&gt;After trying dozens of setups, I landed on five core areas. Not more, not less. Every piece of freelance admin falls into one of these:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Clients
&lt;/h3&gt;

&lt;p&gt;Not just names and emails — you need to see a client's full picture at a glance. Are they a lead, active, or past? What's their rate? What projects have you done for them? What invoices are outstanding?&lt;/p&gt;

&lt;p&gt;Most freelancers track this in their head until they have 5+ clients. Then things start slipping.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; A single client database where every client links to their projects and invoices. You click on "Acme Co" and see everything — every project, every invoice, every payment — without searching.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Projects
&lt;/h3&gt;

&lt;p&gt;Every piece of work needs a status (not started, in progress, completed, on hold), a deadline, a fee, and a connection to the client who's paying for it.&lt;/p&gt;

&lt;p&gt;The key insight: projects aren't tasks. "Website Redesign" is a project. "Design the homepage" is a task inside that project. Mixing these up is why most freelancer to-do lists become unusable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; A project pipeline with both a table view (for detail) and a kanban board (for a visual overview of what's where). Each project links to its client.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Tasks
&lt;/h3&gt;

&lt;p&gt;The daily work. Each task belongs to a project, has a priority, a due date, and a status. You need two views: a flat list for "what do I need to do today?" and a board for dragging things between To Do, In Progress, and Done.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; A task database linked to projects, with a kanban board view. Filter by "not done" and you have your daily action list.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Invoices
&lt;/h3&gt;

&lt;p&gt;This is where most freelancers lose money — literally. You finish a project, forget to invoice for two weeks, then can't remember the exact amount or what it was for.&lt;/p&gt;

&lt;p&gt;Every invoice should link to both the client AND the project, have a clear status (draft, sent, paid, overdue), and show the amount and dates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; An invoice tracker that connects to both clients and projects. You can see all unpaid invoices in one view, and every invoice traces back to the work it covers.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Finances
&lt;/h3&gt;

&lt;p&gt;Income and expenses in one place. The critical feature most freelancers miss: linking income entries to their invoices. When a payment lands, you mark which invoice it covers. Now you can trace the full path: Client → Project → Invoice → Payment.&lt;/p&gt;

&lt;p&gt;Expenses get tracked separately with categories (software, marketing, education, office). Come tax time, you have everything in one place instead of scrolling through bank statements.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; A finance database with income linked to invoices, and expenses categorized. You can see your net position at any time without opening a spreadsheet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why connected databases matter
&lt;/h2&gt;

&lt;p&gt;The magic isn't in any single database — it's in the links between them. When you click on a client, you see their projects. Click a project, you see its tasks and invoices. Click an invoice, you see the payment.&lt;/p&gt;

&lt;p&gt;This means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You never lose track of what you owe or what you're owed&lt;/li&gt;
&lt;li&gt;Every task traces back to a project and a paying client&lt;/li&gt;
&lt;li&gt;Your finances connect to real work, not just anonymous numbers&lt;/li&gt;
&lt;li&gt;Status changes in one place make sense everywhere else&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've tried building something like this in spreadsheets, you know it falls apart fast. Spreadsheets don't link. Notion does.&lt;/p&gt;

&lt;h2&gt;
  
  
  The system I built
&lt;/h2&gt;

&lt;p&gt;I put this exact framework into a Notion template called &lt;strong&gt;Freelance OS&lt;/strong&gt;. It has all five databases pre-built and connected, filled with sample data so you can see how it works before swapping in your own info.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Client manager with status tracking&lt;/li&gt;
&lt;li&gt;Project pipeline (table + kanban)&lt;/li&gt;
&lt;li&gt;Task manager with priority levels and board view&lt;/li&gt;
&lt;li&gt;Invoice tracker linked to clients and projects&lt;/li&gt;
&lt;li&gt;Finance tracker with income linked to invoices&lt;/li&gt;
&lt;li&gt;Setup guide to get running in 5 minutes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It works on Notion's free plan and takes about 5 minutes to set up.&lt;/p&gt;

&lt;p&gt;If you want to skip building this yourself: &lt;a href="https://rensil.gumroad.com/l/gzkibm" rel="noopener noreferrer"&gt;&lt;strong&gt;Get Freelance OS here&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Or build your own
&lt;/h2&gt;

&lt;p&gt;If you prefer to build it yourself, here's the order that works best:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Clients first&lt;/strong&gt; — this is the foundation everything links to&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Projects second&lt;/strong&gt; — add a relation column pointing to Clients&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tasks third&lt;/strong&gt; — add a relation column pointing to Projects&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Invoices fourth&lt;/strong&gt; — add relations to both Clients AND Projects&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Finances last&lt;/strong&gt; — add a relation to Invoices for income entries&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Build in this order because each database links to the one before it. If you try to build finances first, you'll have nothing to connect it to.&lt;/p&gt;

&lt;p&gt;The most important Notion feature to learn: &lt;strong&gt;relations and rollups&lt;/strong&gt;. Relations link databases. Rollups pull data from linked entries (like summing all invoice amounts for a client). These two features turn five separate tables into one connected system.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;Freelancing doesn't have to mean chaos. The moment you connect your clients to your projects to your invoices to your money, everything gets simpler. You spend less time on admin and more time on the work that actually pays.&lt;/p&gt;

&lt;p&gt;Whether you build this yourself or grab the template, the framework is the same: five databases, all connected, one home for everything.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you found this useful, I'm building more tools for freelancers. Follow me here or check out &lt;a href="https://rensil.gumroad.com/l/gzkibm" rel="noopener noreferrer"&gt;Freelance OS on Gumroad&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>freelancing</category>
      <category>notion</category>
      <category>productivity</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The Cloud is a Scam (For 90% of Your Projects)</title>
      <dc:creator>Renato Silva</dc:creator>
      <pubDate>Mon, 08 Jun 2026 20:10:15 +0000</pubDate>
      <link>https://dev.to/renato_silva_71eef0fc385f/the-cloud-is-a-scam-for-90-of-your-projects-2fdj</link>
      <guid>https://dev.to/renato_silva_71eef0fc385f/the-cloud-is-a-scam-for-90-of-your-projects-2fdj</guid>
      <description>&lt;p&gt;For the past seven years, we’ve been brainwashed into believing that if your app isn’t deployed across multiple AWS availability zones, using serverless functions, and managed via an intricate mesh of cloud-native tools, you aren’t doing "real" modern development.&lt;/p&gt;

&lt;p&gt;We were promised infinite scalability, zero maintenance, and pay-as-you-go pricing.&lt;/p&gt;

&lt;p&gt;But nobody told us about the hidden costs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The $500 surprise bill because an infinite loop triggered a serverless function overnight.&lt;/li&gt;
&lt;li&gt;The nightmare of debugging "cold starts" that make your API feel sluggish.&lt;/li&gt;
&lt;li&gt;The reality that 95% of applications will &lt;strong&gt;never&lt;/strong&gt; need to scale dynamically to millions of users in seconds.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tech industry is finally waking up from the cloud hangover, and the &lt;strong&gt;"De-clouding"&lt;/strong&gt; movement is officially here.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Rise of the $5 VPS and the "Local-First" Database
&lt;/h2&gt;

&lt;p&gt;Companies like Basecamp famously saved $1.5 million a year by leaving the cloud and buying their own hardware. But you don't need to buy a physical server rack to benefit from this mindset shift.&lt;/p&gt;

&lt;p&gt;Lately, there’s a massive resurgence in keeping things incredibly lean:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The Revenge of SQLite:&lt;/strong&gt; For years, SQLite was treated as a "toy" database. Today, with tools like Prisma and modern storage, devs are realizing that a local, single-file SQLite database running on a tiny virtual private server (VPS) can handle hundreds of concurrent requests per second with &lt;strong&gt;zero network latency&lt;/strong&gt;. No AWS RDS required.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Predictable Pricing:&lt;/strong&gt; Deploying a standard Node.js/Fastify monolith on a flat-rate provider (like Hetzner, DigitalOcean, or a simple Render instance) means you know &lt;em&gt;exactly&lt;/em&gt; how much you will pay at the end of the month. No complex math, no bandwidth tax.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Why We Fell for the Hype
&lt;/h2&gt;

&lt;p&gt;We mistook architectural complexity for engineering maturity. &lt;/p&gt;

&lt;p&gt;We started designing infrastructure for the scale of Netflix while having the traffic of a local bakery. Serverless and micro-cloud architectures are fantastic tools for highly unpredictable, massive workloads. But for a standard SaaS, a portfolio, or a business API? It's just an expensive layer of friction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bringing Sanity Back to the Backend
&lt;/h2&gt;

&lt;p&gt;Going back to basics isn't "regression"; it's pragmatism. &lt;/p&gt;

&lt;p&gt;When you build an API with a straightforward framework, protect it with a local memory rate-limiter, and write to a local or predictable database, you remove 90% of the moving parts that could break. You spend less time configuring YAML files and more time actually writing features.&lt;/p&gt;




&lt;h2&gt;
  
  
  Let's talk numbers...
&lt;/h2&gt;

&lt;p&gt;Are you still fully bought into the serverless/cloud-native dream, or have you started moving your side projects (and company apps) back to simpler, predictable hosting? What’s the craziest cloud bill you’ve ever seen?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Let’s debate in the comments below! 💸👇&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>cloud</category>
      <category>devops</category>
      <category>backend</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Stop Building Space Shuttles When All You Need Is a Bicycle</title>
      <dc:creator>Renato Silva</dc:creator>
      <pubDate>Fri, 22 May 2026 19:18:14 +0000</pubDate>
      <link>https://dev.to/renato_silva_71eef0fc385f/stop-building-space-shuttles-when-all-you-need-is-a-bicycle-7c2</link>
      <guid>https://dev.to/renato_silva_71eef0fc385f/stop-building-space-shuttles-when-all-you-need-is-a-bicycle-7c2</guid>
      <description>&lt;p&gt;We’ve all been there. You join a new project, excited to look at the codebase. It's a simple application—maybe a Todo app, a simple blog, or a feedback collector. &lt;/p&gt;

&lt;p&gt;You open the repository, expecting a clean, straightforward structure. Instead, you find:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;12 different microservices.&lt;/li&gt;
&lt;li&gt;A Kubernetes configuration that looks like a NASA launch manual.&lt;/li&gt;
&lt;li&gt;5 layers of abstractions (Interfaces, Repositories, DTOs, Presenters, Adapters) just to fetch a single row from a database.&lt;/li&gt;
&lt;li&gt;Docker containers eating 90% of your laptop's RAM just to run &lt;code&gt;npm run dev&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of this... for an application that gets 50 users a day.&lt;/p&gt;

&lt;p&gt;Welcome to the era of &lt;strong&gt;Overengineering&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  How Did We Get Here?
&lt;/h2&gt;

&lt;p&gt;As developers, we suffer from what I call &lt;strong&gt;"Resume-Driven Development" (RDD)&lt;/strong&gt;. We don't choose tools based on what the project needs today; we choose tools based on what looks cool on our LinkedIn profile or what Netflix uses to handle billions of requests.&lt;/p&gt;

&lt;p&gt;We became so obsessed with &lt;em&gt;scalability&lt;/em&gt; that we forgot about &lt;em&gt;maintainability&lt;/em&gt; and &lt;em&gt;velocity&lt;/em&gt;. &lt;/p&gt;

&lt;p&gt;If it takes a junior developer three days just to set up the local environment and understand how a single HTTP request routes through twenty layers of architecture, your system isn't "advanced." It's broken.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Power of the "Boring" Monolith
&lt;/h2&gt;

&lt;p&gt;There is a silent counter-movement happening right now. Senior developers who have tasted the pain of managing distributed transactions and network latency in tiny microservices are screaming: &lt;strong&gt;Go back to the monolith!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Building a "boring", well-structured monolithic application using simple tools (like a clean Fastify/Node.js setup, standard relational databases, and straightforward code) gives you something a distributed system can't:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Unmatched Speed:&lt;/strong&gt; You can ship features in hours, not weeks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mental Clarity:&lt;/strong&gt; You can open one folder and understand the entire data flow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cheap Hosting:&lt;/strong&gt; A single tiny instance on Render, Fly.io, or DigitalOcean can easily handle thousands of concurrent users if your code is efficient.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Pragmatism &amp;gt; Hype
&lt;/h2&gt;

&lt;p&gt;Don't get me wrong. If you are building the next Uber, Netflix, or Amazon, yes—bring on the microservices, Kafka, and complex caching layers. You earned that complexity.&lt;/p&gt;

&lt;p&gt;But if you are starting an MVP, testing a product, or building a standard internal tool, &lt;strong&gt;keep it stupidly simple (KISS)&lt;/strong&gt;. Use a reliable SQLite or Postgres database, write clean functions, validate your data with Zod, protect your endpoints with a simple Rate Limiter, and get things done.&lt;/p&gt;

&lt;p&gt;Architecture shouldn't be a monument to your ego. It should be a tool to deliver value to the user.&lt;/p&gt;




&lt;h2&gt;
  
  
  Let's Be Honest...
&lt;/h2&gt;

&lt;p&gt;What is the worst case of overengineering you have ever witnessed (or accidentally built) in your career? Are we, as a community, making coding harder than it needs to be?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Drop your horror stories in the comments below! 🛰️👇&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>backend</category>
      <category>programming</category>
      <category>architecture</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>Is 'Vibe Coding' Making Us Better Developers, or Just Lazier Managers?</title>
      <dc:creator>Renato Silva</dc:creator>
      <pubDate>Sat, 16 May 2026 07:34:13 +0000</pubDate>
      <link>https://dev.to/renato_silva_71eef0fc385f/is-vibe-coding-making-us-better-developers-or-just-lazier-managers-2cip</link>
      <guid>https://dev.to/renato_silva_71eef0fc385f/is-vibe-coding-making-us-better-developers-or-just-lazier-managers-2cip</guid>
      <description>&lt;p&gt;If you’ve been on tech Twitter, LinkedIn, or scrolling through Dev.to lately, you’ve probably heard the latest buzzword taking over the industry: &lt;strong&gt;Vibe Coding&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Coined to describe the workflow where a developer sits back, thinks about a feature, prompts an AI tool (like Cursor, GitHub Copilot, or Claude), and lets the machine do 90% of the heavy lifting, it’s being praised as the ultimate productivity hack. You don't write the code; you just &lt;em&gt;guide the vibe&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;It sounds amazing. But as someone who loves building and diving deep into backend architecture, it makes me wonder: &lt;strong&gt;Are we actually becoming super-developers, or are we just becoming lazy code-reviewers who don’t understand our own systems?&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Dream: From Idea to Production in Minutes
&lt;/h2&gt;

&lt;p&gt;Let’s be honest: the "vibe" is real. &lt;/p&gt;

&lt;p&gt;Being able to prompt an AI agent to set up a boilerplate boilerplate, write basic CRUD operations, or configure a tricky library saves hours of stack-overflowing. For solo founders, hackers, and junior devs looking to ship MVPs quickly, Vibe Coding feels like having a senior engineer sitting right next to you.&lt;/p&gt;

&lt;p&gt;You focus on the product, the business logic, and the user experience. The AI handles the syntax. &lt;/p&gt;

&lt;h2&gt;
  
  
  The Nightmare: "Zombie Architectures"
&lt;/h2&gt;

&lt;p&gt;But here is where the vibe gets ruined. What happens when the AI writes 1,000 lines of complex, asynchronous Node.js code, it works on your machine, but three weeks later it breaks in production under heavy load?&lt;/p&gt;

&lt;p&gt;If you didn't write the code line by line, &lt;strong&gt;how long will it take you to debug it?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When we rely too much on AI to generate architecture, we run into three massive risks:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The Black Box Effect:&lt;/strong&gt; You know &lt;em&gt;what&lt;/em&gt; the application does, but you don't know &lt;em&gt;how&lt;/em&gt; it does it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Technical Debt on Steroids:&lt;/strong&gt; AI writes clean-looking code, but it doesn't always know about your specific edge cases, security validation (like strict Zod schemas), or rate-limiting needs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Death of Junior Problem-Solving:&lt;/strong&gt; If a junior developer spends their formative years just reviewing AI code, do they ever actually learn how to solve hard logical problems from scratch?&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Balancing the Vibe with Engineering Discipline
&lt;/h2&gt;

&lt;p&gt;I don't think Vibe Coding is bad. In fact, it's inevitable. The tools are too good to ignore. But we need to change how we define "coding."&lt;/p&gt;

&lt;p&gt;The future developer isn't just someone who types syntax; they are a &lt;strong&gt;System Architect and a Quality Inspector&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;To survive the era of Vibe Coding without deploying broken systems, we must follow three golden rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Never accept code you don't understand:&lt;/strong&gt; If an AI generates a complex Regex or database query, make it explain it to you before you commit.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Enforce strict validation and security:&lt;/strong&gt; AI loves to skip error handling. Make sure your inputs are validated (e.g., using Zod) and your endpoints are safe (e.g., using Rate Limiters).&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Write integration tests:&lt;/strong&gt; Let the AI write the code, but you should control the tests to ensure the machine actually delivered what you asked for.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What is your take?
&lt;/h2&gt;

&lt;p&gt;Are you currently "vibe coding" your way through your daily job, or are you resisting the urge to keep your engineering skills sharp? Do you think this will ruin the next generation of software engineers, or elevate them?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Let’s argue in the comments below! 👇&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Giving Your API a Voice: Sending Emails with Node.js and Nodemailer</title>
      <dc:creator>Renato Silva</dc:creator>
      <pubDate>Mon, 04 May 2026 16:29:03 +0000</pubDate>
      <link>https://dev.to/renato_silva_71eef0fc385f/giving-your-api-a-voice-sending-emails-with-nodejs-and-nodemailer-1b1c</link>
      <guid>https://dev.to/renato_silva_71eef0fc385f/giving-your-api-a-voice-sending-emails-with-nodejs-and-nodemailer-1b1c</guid>
      <description>&lt;p&gt;A great backend application doesn't just store data in a database; it communicates with the outside world. Whether it's a welcome email, a password reset, or a notification for a new feedback, knowing how to integrate an email service is a crucial skill for any developer.&lt;/p&gt;

&lt;p&gt;In this article, I’ll show you how I integrated email notifications into my Feedback API using &lt;strong&gt;Nodemailer&lt;/strong&gt; and the &lt;strong&gt;Provider Pattern&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Goal
&lt;/h2&gt;

&lt;p&gt;When a user submits feedback, the system should:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Validate the data.&lt;/li&gt;
&lt;li&gt;Save it to the database.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Notify the administrator via email.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Architecture Matters: The Provider Pattern
&lt;/h2&gt;

&lt;p&gt;Instead of hardcoding Nodemailer directly into my business logic, I used the &lt;strong&gt;Provider Pattern&lt;/strong&gt;. This keeps the code decoupled. If I decide to switch from Nodemailer to AWS SES or SendGrid in the future, I only need to change one file.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Defining the Contract (Abstract Class)
&lt;/h3&gt;

&lt;p&gt;First, we define what a &lt;code&gt;MailProvider&lt;/code&gt; should do:&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;// src/providers/mail-provider/mail-provider.js&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MailProvider&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;sendMail&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;subject&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="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="s2"&gt;Method 'sendMail' must be implemented.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;MailProvider&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Implementing with Nodemailer
&lt;/h3&gt;

&lt;p&gt;Now, we create the actual implementation. We use environment variables to keep credentials safe.&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;// src/providers/mail-provider/nodemailer-provider.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nodemailer&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="s2"&gt;nodemailer&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="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="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="s2"&gt;../../env&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;NodemailerProvider&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;transporter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;transporter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;nodemailer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createTransport&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="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MAIL_HOST&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="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MAIL_PORT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;user&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;MAIL_USER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;pass&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;MAIL_PASS&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="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;sendMail&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;subject&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="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;transporter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendMail&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Feedback Tool &amp;lt;no-reply@feedback.com&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;html&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="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;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;NodemailerProvider&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Integrating into the Use Case
&lt;/h3&gt;

&lt;p&gt;Our &lt;strong&gt;Use Case&lt;/strong&gt; (the business logic) doesn't care how the email is sent; it only knows that it has a &lt;code&gt;mailProvider&lt;/code&gt; capable of sending one.&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;// src/use-cases/submit-feedback.js&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&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;parsedData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;submitFeedbackSchema&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;request&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;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;parsedData&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Persisting data&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;feedback&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;feedbackRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// Sending notification&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mailProvider&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;NodemailerProvider&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;mailProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendMail&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Admin &amp;lt;admin@example.com&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;New Feedback Received!&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="s2"&gt;`&amp;lt;h1&amp;gt;New Feedback&amp;lt;/h1&amp;gt;&amp;lt;p&amp;gt;From: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/p&amp;gt;&amp;lt;p&amp;gt;&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="s2"&gt;&amp;lt;/p&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;✅ Email sent!&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;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="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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;❌ Email failed, but feedback was saved:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;feedback&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;
  
  
  Key Takeaways
&lt;/h3&gt;

&lt;h4&gt;
  
  
  1. Don't Let Email Failures Break Your App
&lt;/h4&gt;

&lt;p&gt;I wrapped the &lt;code&gt;sendMail&lt;/code&gt; call in a &lt;code&gt;try/catch&lt;/code&gt; block. If the email service is down, the user's feedback is still saved in the database. The user shouldn't see an error 500 just because a notification failed.&lt;/p&gt;

&lt;h4&gt;
  
  
  2. Environment Validation
&lt;/h4&gt;

&lt;p&gt;Always use a tool like &lt;strong&gt;Zod&lt;/strong&gt; to validate your SMTP credentials (&lt;code&gt;MAIL_HOST&lt;/code&gt;, &lt;code&gt;MAIL_USER&lt;/code&gt;, etc.). If your credentials are missing, your app should fail early during startup, not when a user tries to send feedback.&lt;/p&gt;

&lt;h4&gt;
  
  
  3. Testing with Mailtrap
&lt;/h4&gt;

&lt;p&gt;During development, I used &lt;strong&gt;Mailtrap&lt;/strong&gt;. It acts as a "fake" SMTP server that catches your emails in a virtual inbox, so you don't accidentally spam real addresses while testing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;Adding an email service makes your application feel professional and alive. By using providers and dependency injection, you ensure your code remains maintainable and ready for scale.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do you handle background tasks like emails in your projects? Let's talk in the comments!&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>node</category>
      <category>backend</category>
      <category>webdev</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
