<?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>Git Worktrees: The Missing Piece for Parallel AI Agents</title>
      <dc:creator>Renato Silva</dc:creator>
      <pubDate>Mon, 31 Aug 2026 16:38:50 +0000</pubDate>
      <link>https://dev.to/renato_silva_71eef0fc385f/git-worktrees-the-missing-piece-for-parallel-ai-agents-10lm</link>
      <guid>https://dev.to/renato_silva_71eef0fc385f/git-worktrees-the-missing-piece-for-parallel-ai-agents-10lm</guid>
      <description>&lt;h2&gt;
  
  
  🔧 The Problem
&lt;/h2&gt;

&lt;p&gt;If you're running more than one AI coding agent at a time — Claude Code in one terminal, Aider in another, maybe a Cursor background agent chewing on a refactor — you've probably hit the same wall: they all want to work on the same repo, but they can't share a working directory without stepping on each other.&lt;/p&gt;

&lt;p&gt;The usual workarounds are all bad:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;git stash&lt;/code&gt; juggling&lt;/strong&gt; — you stash, switch branches, let the agent work, unstash, repeat. Fine for one agent. A nightmare for three running concurrently, because stash is a single shared stack and agents don't know how to negotiate over it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloning the repo N times&lt;/strong&gt; — works, but now you've got N full copies of &lt;code&gt;.git&lt;/code&gt;, N sets of dependencies to install, and N places for config drift to sneak in. On a large monorepo this is also just slow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One giant branch with agents committing to subdirectories&lt;/strong&gt; — merge conflicts waiting to happen, and agents lose the ability to see a clean diff of just their own work.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What you actually want is N independent working directories, backed by &lt;em&gt;one&lt;/em&gt; &lt;code&gt;.git&lt;/code&gt;, so history, remotes, and object storage stay unified while the checked-out files stay isolated. That's exactly what &lt;code&gt;git worktree&lt;/code&gt; gives you, and it's been sitting in Git core since version 2.5 (2015), mostly ignored until "run four agents at once" became a normal Tuesday.&lt;/p&gt;

&lt;h2&gt;
  
  
  🌳 Worktrees in Practice
&lt;/h2&gt;

&lt;p&gt;The core workflow is boring in the best way:&lt;/p&gt;

&lt;p&gt;bash&lt;/p&gt;

&lt;h1&gt;
  
  
  from your main checkout
&lt;/h1&gt;

&lt;p&gt;git worktree add ../myapp-agent-a feature/agent-a&lt;br&gt;
git worktree add ../myapp-agent-b feature/agent-b&lt;br&gt;
git worktree add ../myapp-agent-c fix/flaky-test&lt;/p&gt;

&lt;h1&gt;
  
  
  see what's active
&lt;/h1&gt;

&lt;p&gt;git worktree list&lt;/p&gt;

&lt;h1&gt;
  
  
  /home/dev/myapp            abcd123 [main]
&lt;/h1&gt;

&lt;h1&gt;
  
  
  /home/dev/myapp-agent-a    ef01234 [feature/agent-a]
&lt;/h1&gt;

&lt;h1&gt;
  
  
  /home/dev/myapp-agent-b    5678aaa [feature/agent-b]
&lt;/h1&gt;

&lt;h1&gt;
  
  
  /home/dev/myapp-agent-c    9911bbb [fix/flaky-test]
&lt;/h1&gt;

&lt;p&gt;Each directory is a real, complete checkout — you can &lt;code&gt;cd&lt;/code&gt; into it, run tests, open it in an editor, point an agent at it — and none of them affect each other's index or working tree. Behind the scenes they all share &lt;code&gt;.git/objects&lt;/code&gt;, so you're not duplicating blobs, and any commit made in one worktree is immediately visible to &lt;code&gt;git log&lt;/code&gt; in the others (once you fetch/checkout).&lt;/p&gt;

&lt;p&gt;Cleaning up is just as direct:&lt;/p&gt;

&lt;p&gt;bash&lt;br&gt;
git worktree remove ../myapp-agent-c&lt;/p&gt;

&lt;h1&gt;
  
  
  or, if the agent left the directory dirty and you don't care:
&lt;/h1&gt;

&lt;p&gt;git worktree remove --force ../myapp-agent-c&lt;/p&gt;

&lt;p&gt;For an agent-driven workflow, I wrap this in a small script so I'm not hand-typing branch names every time I spin one up:&lt;/p&gt;

&lt;p&gt;bash&lt;/p&gt;

&lt;h1&gt;
  
  
  !/usr/bin/env bash
&lt;/h1&gt;

&lt;h1&gt;
  
  
  spawn-agent.sh 
&lt;/h1&gt;

&lt;p&gt;set -euo pipefail&lt;/p&gt;

&lt;p&gt;task="$1"&lt;br&gt;
branch="agent/${task}"&lt;br&gt;
worktree_path="../$(basename "$(pwd)")-${task}"&lt;/p&gt;

&lt;p&gt;git worktree add -b "$branch" "$worktree_path" main&lt;br&gt;
cd "$worktree_path"&lt;/p&gt;

&lt;h1&gt;
  
  
  per-worktree setup so agents don't fight over node_modules etc.
&lt;/h1&gt;

&lt;p&gt;cp ../.env.example .env&lt;br&gt;
npm install --prefer-offline&lt;/p&gt;

&lt;p&gt;echo "Worktree ready at $worktree_path on branch $branch"&lt;/p&gt;

&lt;p&gt;Now "give the agent a sandbox" is a single command, and tearing it down after review/merge is another single command. No stash stack, no second clone, no confusion about which branch is checked out where.&lt;/p&gt;

&lt;h2&gt;
  
  
  ⚠️ The Gotchas Nobody Mentions
&lt;/h2&gt;

&lt;p&gt;Worktrees are not a free lunch, and the failure modes are exactly the kind of thing that eats an afternoon if you don't know to look for them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Shared package manager caches can lie to you.&lt;/strong&gt; &lt;code&gt;node_modules&lt;/code&gt;, &lt;code&gt;.venv&lt;/code&gt;, and build caches are &lt;em&gt;not&lt;/em&gt; shared between worktrees by default — each one needs its own install. If your agents are installing dependencies in parallel across worktrees pointed at the same global npm/pip cache, you can get lock contention or, worse, a half-written cache entry that silently corrupts a build in a different worktree. Pin a per-worktree cache directory if you're running installs concurrently:&lt;/p&gt;

&lt;p&gt;bash&lt;br&gt;
npm install --cache "$(pwd)/.npm-cache"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;IDE indexing goes haywire.&lt;/strong&gt; VS Code, JetBrains IDEs, and language servers built with a single-repo assumption will happily index every worktree directory you open as if it's an unrelated project — which is technically correct but means you're running 4x the TypeScript server memory, 4x the file watchers, and sometimes 4x the "go to definition" confusion if symlinks or path aliases assume a fixed repo root. If you're not actively reading code in a worktree, don't leave it open in the IDE — close the window when the agent is just running headless.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Branches can't be checked out twice.&lt;/strong&gt; This one bites people immediately: Git will refuse to let two worktrees point at the same branch.&lt;/p&gt;

&lt;p&gt;fatal: 'feature/agent-a' is already checked out at '/home/dev/myapp-agent-a'&lt;/p&gt;

&lt;p&gt;This is a feature, not a bug — it's the mechanism that prevents two agents from independently committing to the same branch and creating divergent history in two places at once. But it does mean your orchestration script needs a real branch-per-agent naming scheme, not "reuse main for everything."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Detached HEAD surprises.&lt;/strong&gt; If an agent (or you) checks out a commit instead of a branch, you get a detached HEAD in that worktree — harmless, but if the agent then commits and you forget to create a branch before removing the worktree, &lt;code&gt;git worktree remove&lt;/code&gt; will happily let you lose those commits to garbage collection. Always &lt;code&gt;git branch tmp-recovery&lt;/code&gt; before tearing down a detached-HEAD worktree you're unsure about.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Submodules and &lt;code&gt;.git&lt;/code&gt; hooks need extra care.&lt;/strong&gt; Hooks live in the shared &lt;code&gt;.git&lt;/code&gt; directory by default (or &lt;code&gt;.git/worktrees/&amp;lt;name&amp;gt;&lt;/code&gt; for some internals), so a hook that assumes &lt;code&gt;$(pwd)&lt;/code&gt; is the repo root can misbehave across worktrees. If you use submodules, &lt;code&gt;git worktree add&lt;/code&gt; doesn't initialize them for you — add &lt;code&gt;--recurse-submodules&lt;/code&gt; or run &lt;code&gt;git submodule update --init&lt;/code&gt; explicitly per worktree.&lt;/p&gt;

&lt;h2&gt;
  
  
  🚀 Putting It Together
&lt;/h2&gt;

&lt;p&gt;The pattern that's worked well for me running 3-4 agents concurrently:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;One "orchestrator" checkout (your normal working directory) that never runs an agent directly — it's just for review and merging.&lt;/li&gt;
&lt;li&gt;One worktree per active task, named after the task, not the agent.&lt;/li&gt;
&lt;li&gt;A teardown step that force-removes the worktree &lt;em&gt;and&lt;/em&gt; deletes the branch once merged, so &lt;code&gt;git worktree list&lt;/code&gt; doesn't slowly fill up with zombie sandboxes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;bash&lt;br&gt;
git worktree remove --force ../myapp-agent-a&lt;br&gt;
git branch -d agent/task-a   # or -D if the agent's commits got squashed on merge&lt;/p&gt;

&lt;p&gt;It's not a glamorous feature — worktrees have existed for a decade specifically for things like hotfix-while-mid-feature workflows — but it maps almost perfectly onto "isolated sandbox per autonomous process" once you swap the human for an agent. The shared object store keeps disk usage sane, and the isolated working trees keep agents from corrupting each other's in-progress edits.&lt;/p&gt;

&lt;p&gt;How are you isolating your parallel agents right now — worktrees, containers, or something else entirely? And if you've hit a worktree gotcha that isn't on this list, I'd genuinely like to hear about it in the comments.&lt;/p&gt;

</description>
      <category>git</category>
      <category>ai</category>
      <category>productivity</category>
      <category>cli</category>
    </item>
    <item>
      <title>SSE vs WebSockets vs Polling: Real-Time Sync From the Backend</title>
      <dc:creator>Renato Silva</dc:creator>
      <pubDate>Thu, 27 Aug 2026 19:23:10 +0000</pubDate>
      <link>https://dev.to/renato_silva_71eef0fc385f/sse-vs-websockets-vs-polling-real-time-sync-from-the-backend-4hgc</link>
      <guid>https://dev.to/renato_silva_71eef0fc385f/sse-vs-websockets-vs-polling-real-time-sync-from-the-backend-4hgc</guid>
      <description>&lt;p&gt;There's been a nice trick going around: use &lt;code&gt;BroadcastChannel&lt;/code&gt; in the browser to sync state across tabs without a server round-trip. It's elegant, but it only solves half the problem — it syncs tabs on &lt;em&gt;one&lt;/em&gt; device. The moment you have two different users, or one user on a phone and a laptop, you need the server to be the source of truth and push updates out.&lt;/p&gt;

&lt;p&gt;So let's flip it: how do you actually push consistent state to N connected clients from a Node API, and which transport should you reach for?&lt;/p&gt;

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

&lt;p&gt;Say you're building something boring and real: a shared cart, a live dashboard, a "someone else is editing this" indicator. Multiple clients need to see the same state change at roughly the same time, without everyone hammering &lt;code&gt;GET /state&lt;/code&gt; every 500ms.&lt;/p&gt;

&lt;p&gt;You've got three realistic options:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Polling&lt;/strong&gt; — client asks, server answers, repeat&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SSE (Server-Sent Events)&lt;/strong&gt; — server pushes a one-way stream over plain HTTP&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WebSockets&lt;/strong&gt; — full duplex, server and client both push&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each one has a different cost model, and picking the "cool" one (WebSockets) is often the wrong call.&lt;/p&gt;

&lt;h2&gt;
  
  
  🐢 Polling: the boring baseline
&lt;/h2&gt;

&lt;p&gt;Polling gets a bad reputation it doesn't fully deserve. It's stateless, trivially horizontally scalable, works through every proxy and CDN ever built, and requires zero special infrastructure.&lt;/p&gt;

&lt;p&gt;javascript&lt;br&gt;
// client&lt;br&gt;
setInterval(async () =&amp;gt; {&lt;br&gt;
  const res = await fetch('/api/state');&lt;br&gt;
  const state = await res.json();&lt;br&gt;
  renderState(state);&lt;br&gt;
}, 2000);&lt;/p&gt;

&lt;p&gt;The honest trade-off: latency is bounded by your interval, and cost scales linearly with (clients × interval). Ten thousand clients polling every 2 seconds is 5,000 requests/sec hitting your server &lt;em&gt;even when nothing changed&lt;/em&gt;. Fine for a demo, painful at scale, and it never actually feels "real-time" — there's always a visible lag.&lt;/p&gt;

&lt;h2&gt;
  
  
  📡 SSE: push, but only one way
&lt;/h2&gt;

&lt;p&gt;SSE is the underrated option. It's just an HTTP response that never closes, with a text protocol on top. No new protocol, no special client library, works over regular HTTP/1.1 and HTTP/2, and reconnects automatically via &lt;code&gt;EventSource&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here's a minimal Node/Express version that keeps a registry of connected clients and broadcasts state changes:&lt;/p&gt;

&lt;p&gt;javascript&lt;br&gt;
import express from 'express';&lt;br&gt;
const app = express();&lt;/p&gt;

&lt;p&gt;let state = { count: 0 };&lt;br&gt;
const clients = new Set();&lt;/p&gt;

&lt;p&gt;app.get('/events', (req, res) =&amp;gt; {&lt;br&gt;
  res.set({&lt;br&gt;
    'Content-Type': 'text/event-stream',&lt;br&gt;
    'Cache-Control': 'no-cache',&lt;br&gt;
    Connection: 'keep-alive',&lt;br&gt;
  });&lt;br&gt;
  res.flushHeaders();&lt;/p&gt;

&lt;p&gt;// send current state immediately so late joiners aren't out of sync&lt;br&gt;
  res.write(&lt;code&gt;data: ${JSON.stringify(state)}\n\n&lt;/code&gt;);&lt;/p&gt;

&lt;p&gt;clients.add(res);&lt;br&gt;
  req.on('close', () =&amp;gt; clients.delete(res));&lt;br&gt;
});&lt;/p&gt;

&lt;p&gt;function broadcast(newState) {&lt;br&gt;
  state = newState;&lt;br&gt;
  const payload = &lt;code&gt;data: ${JSON.stringify(state)}\n\n&lt;/code&gt;;&lt;br&gt;
  for (const res of clients) res.write(payload);&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;app.post('/increment', express.json(), (req, res) =&amp;gt; {&lt;br&gt;
  broadcast({ count: state.count + 1 });&lt;br&gt;
  res.sendStatus(204);&lt;br&gt;
});&lt;/p&gt;

&lt;p&gt;app.listen(3000);&lt;/p&gt;

&lt;p&gt;javascript&lt;br&gt;
// client&lt;br&gt;
const source = new EventSource('/events');&lt;br&gt;
source.onmessage = (e) =&amp;gt; renderState(JSON.parse(e.data));&lt;/p&gt;

&lt;p&gt;That's the whole system. No socket library, no handshake upgrade dance, no ping/pong heartbeat logic to babysit — the browser handles reconnects for you.&lt;/p&gt;

&lt;p&gt;The catch: SSE is one-directional. Clients still need a normal &lt;code&gt;POST&lt;/code&gt;/&lt;code&gt;fetch&lt;/code&gt; to send actions back. For a lot of real apps (dashboards, notifications, live scores, cart sync) that's not a limitation, it's a feature — you get a clean separation between "write path" (REST) and "read/subscribe path" (SSE).&lt;/p&gt;

&lt;p&gt;Also worth knowing: browsers cap concurrent &lt;code&gt;EventSource&lt;/code&gt; connections per origin (6 over HTTP/1.1), and some corporate proxies buffer streaming responses, which can delay delivery. HTTP/2 mostly fixes the connection-limit problem since it multiplexes over one TCP connection.&lt;/p&gt;

&lt;h2&gt;
  
  
  🔌 WebSockets: when you actually need two-way
&lt;/h2&gt;

&lt;p&gt;WebSockets are the right tool when the client needs to push frequently too — collaborative editing, multiplayer cursors, chat, game state. Otherwise they're often overkill: you now own a stateful, bidirectional connection with your own reconnect logic, your own heartbeat, and your own message framing.&lt;/p&gt;

&lt;p&gt;javascript&lt;br&gt;
import { WebSocketServer } from 'ws';&lt;br&gt;
const wss = new WebSocketServer({ port: 8080 });&lt;/p&gt;

&lt;p&gt;let state = { count: 0 };&lt;/p&gt;

&lt;p&gt;wss.on('connection', (ws) =&amp;gt; {&lt;br&gt;
  ws.send(JSON.stringify(state));&lt;/p&gt;

&lt;p&gt;ws.on('message', (raw) =&amp;gt; {&lt;br&gt;
    const msg = JSON.parse(raw);&lt;br&gt;
    if (msg.type === 'increment') {&lt;br&gt;
      state = { count: state.count + 1 };&lt;br&gt;
      const payload = JSON.stringify(state);&lt;br&gt;
      for (const client of wss.clients) {&lt;br&gt;
        if (client.readyState === client.OPEN) client.send(payload);&lt;br&gt;
      }&lt;br&gt;
    }&lt;br&gt;
  });&lt;br&gt;
});&lt;/p&gt;

&lt;p&gt;This works fine on one process. The real cost shows up when you scale horizontally: connections are pinned to whichever server instance accepted them, so a broadcast has to fan out across processes too — usually via Redis pub/sub, NATS, or a managed service like Pusher/Ably. That's infrastructure SSE and polling don't force on you nearly as early.&lt;/p&gt;

&lt;h2&gt;
  
  
  📊 Honest comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Polling&lt;/th&gt;
&lt;th&gt;SSE&lt;/th&gt;
&lt;th&gt;WebSockets&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Direction&lt;/td&gt;
&lt;td&gt;client-pull&lt;/td&gt;
&lt;td&gt;server-push (one-way)&lt;/td&gt;
&lt;td&gt;bidirectional&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transport&lt;/td&gt;
&lt;td&gt;plain HTTP&lt;/td&gt;
&lt;td&gt;plain HTTP (streamed)&lt;/td&gt;
&lt;td&gt;own protocol over TCP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reconnect handling&lt;/td&gt;
&lt;td&gt;trivial (just retry)&lt;/td&gt;
&lt;td&gt;built into &lt;code&gt;EventSource&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;you build it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Horizontal scaling&lt;/td&gt;
&lt;td&gt;trivial (stateless)&lt;/td&gt;
&lt;td&gt;needs shared client registry&lt;/td&gt;
&lt;td&gt;needs pub/sub fan-out&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Proxy/firewall friendliness&lt;/td&gt;
&lt;td&gt;best&lt;/td&gt;
&lt;td&gt;good&lt;/td&gt;
&lt;td&gt;can be blocked/downgraded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Good fit&lt;/td&gt;
&lt;td&gt;low-frequency, infrequent updates&lt;/td&gt;
&lt;td&gt;dashboards, notifications, live state&lt;/td&gt;
&lt;td&gt;chat, collab editing, multiplayer&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A pattern I keep coming back to: start with SSE for anything that's fundamentally "server tells clients what changed." Only reach for WebSockets once you have a genuine, frequent client-to-server-to-other-clients requirement that a &lt;code&gt;POST&lt;/code&gt; + SSE combo can't express cleanly. Polling is still the right call for admin dashboards or anything where a few seconds of staleness is genuinely fine and you'd rather not run a persistent-connection service at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  🧠 The part that actually matters: consistency, not transport
&lt;/h2&gt;

&lt;p&gt;Here's the thing none of the three options solve for you: what happens when two broadcasts race, or a client reconnects mid-update and misses a message? The transport is the easy 20%. The hard part is designing your broadcast payload so a client can always recover a consistent view — either by sending full state snapshots (like the example above) instead of deltas, or by including a version/sequence number so clients can detect gaps and request a resync.&lt;/p&gt;

&lt;p&gt;If you only ever broadcast diffs, a single dropped message means every client after it is silently wrong forever. That's the bug that doesn't show up in your demo and absolutely shows up in production three weeks later.&lt;/p&gt;

&lt;p&gt;What's your default pick for this kind of problem — do you reach for SSE first, or do you go straight to WebSockets out of habit? Curious how many people are still shipping raw polling in 2024 and just not talking about it.&lt;/p&gt;

</description>
      <category>node</category>
      <category>websocket</category>
      <category>realtime</category>
      <category>backend</category>
    </item>
    <item>
      <title>Graph Search Isn't Just a LeetCode Trick: BFS in Prod</title>
      <dc:creator>Renato Silva</dc:creator>
      <pubDate>Mon, 24 Aug 2026 09:40:56 +0000</pubDate>
      <link>https://dev.to/renato_silva_71eef0fc385f/graph-search-isnt-just-a-leetcode-trick-bfs-in-prod-5g2c</link>
      <guid>https://dev.to/renato_silva_71eef0fc385f/graph-search-isnt-just-a-leetcode-trick-bfs-in-prod-5g2c</guid>
      <description>&lt;p&gt;Every few months "six degrees of separation" and graph traversal puzzles trend again, and every time the comments split into two camps: people who think BFS/DFS are pure interview theater, and people quietly using them in production without telling anyone. I'm in the second camp. Here's a real feature — "related feedback threads" — built on a boring Node/Postgres stack, where breadth-first search turned out to be exactly the right tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  🎯 The Setup
&lt;/h2&gt;

&lt;p&gt;We run a support/feedback tool where users can link feedback items to each other: "this is related to that," "this duplicates that," "this was split off from that." Over time these links form a graph. Individually each link is trivial — a row in a join table. But support agents kept asking a very reasonable question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"If I'm looking at ticket #4521, what's the full cluster of stuff connected to it, even indirectly?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's not a JOIN. That's a graph traversal.&lt;/p&gt;

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

&lt;p&gt;Our schema looks like this:&lt;/p&gt;

&lt;p&gt;sql&lt;br&gt;
CREATE TABLE feedback (&lt;br&gt;
  id SERIAL PRIMARY KEY,&lt;br&gt;
  title TEXT NOT NULL,&lt;br&gt;
  created_at TIMESTAMPTZ DEFAULT now()&lt;br&gt;
);&lt;/p&gt;

&lt;p&gt;CREATE TABLE feedback_links (&lt;br&gt;
  source_id INT REFERENCES feedback(id),&lt;br&gt;
  target_id INT REFERENCES feedback(id),&lt;br&gt;
  relation TEXT NOT NULL, -- 'related', 'duplicate', 'split_from'&lt;br&gt;
  PRIMARY KEY (source_id, target_id)&lt;br&gt;
);&lt;/p&gt;

&lt;p&gt;A single feedback item might link to 3 others, each of which links to 2 more, and so on. Agents don't just want direct neighbors — they want the whole connected component up to some reasonable depth, because context that's two or three hops away is often exactly what explains why a bug report and a feature request are secretly the same underlying issue.&lt;/p&gt;

&lt;p&gt;The naive fix — recursive JOINs pulled straight into application code with no depth limit — either times out on a dense cluster or returns way more noise than an agent can use in a support ticket sidebar.&lt;/p&gt;

&lt;h2&gt;
  
  
  🕸️ Modeling Feedback as a Graph
&lt;/h2&gt;

&lt;p&gt;Once you say "connected component up to N hops," you've already described BFS. Depth-first search would work too, but it explores one branch all the way down before backtracking, which is the wrong shape for "show me everything within 3 degrees, closest first." BFS naturally processes nodes in order of distance from the source, which maps directly onto the UI requirement: show closest-related items first, and stop expanding once you hit the depth cap.&lt;/p&gt;

&lt;h2&gt;
  
  
  🔍 The BFS Implementation
&lt;/h2&gt;

&lt;p&gt;We pull the edges relevant to the starting node's component lazily, level by level, straight from Postgres, and do the traversal logic in Node:&lt;/p&gt;

&lt;p&gt;javascript&lt;br&gt;
async function findRelatedFeedback(pool, startId, maxDepth = 3, maxResults = 50) {&lt;br&gt;
  const visited = new Set([startId]);&lt;br&gt;
  const result = [];&lt;br&gt;
  let frontier = [startId];&lt;br&gt;
  let depth = 0;&lt;/p&gt;

&lt;p&gt;while (frontier.length &amp;gt; 0 &amp;amp;&amp;amp; depth &amp;lt; maxDepth &amp;amp;&amp;amp; result.length &amp;lt; maxResults) {&lt;br&gt;
    const { rows } = await pool.query(&lt;br&gt;
      &lt;code&gt;SELECT source_id, target_id, relation&lt;br&gt;
       FROM feedback_links&lt;br&gt;
       WHERE source_id = ANY($1) OR target_id = ANY($1)&lt;/code&gt;,&lt;br&gt;
      [frontier]&lt;br&gt;
    );&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const nextFrontier = [];

for (const row of rows) {
  const neighbor = frontier.includes(row.source_id) ? row.target_id : row.source_id;
  if (!visited.has(neighbor)) {
    visited.add(neighbor);
    nextFrontier.push(neighbor);
    result.push({ id: neighbor, depth: depth + 1, relation: row.relation });
  }
}

frontier = nextFrontier;
depth++;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

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

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

&lt;p&gt;This is textbook BFS with two production-shaped guardrails bolted on: &lt;code&gt;maxDepth&lt;/code&gt; so a densely connected cluster can't blow up the response, and &lt;code&gt;maxResults&lt;/code&gt; so a single mega-hub node (some "general feedback" catch-all ticket with 200 links) can't turn one API call into a full graph dump. Those two limits are doing more work for user experience than the algorithm itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  🐘 Doing It in Postgres Instead
&lt;/h2&gt;

&lt;p&gt;You can also push the whole traversal into the database with a recursive CTE, which is worth knowing even if you don't end up using it:&lt;/p&gt;

&lt;p&gt;sql&lt;br&gt;
WITH RECURSIVE related AS (&lt;br&gt;
  SELECT source_id AS id, 0 AS depth&lt;br&gt;
  FROM feedback WHERE id = $1&lt;br&gt;
  UNION&lt;br&gt;
  SELECT id, 0 FROM feedback WHERE id = $1&lt;/p&gt;

&lt;p&gt;UNION ALL&lt;/p&gt;

&lt;p&gt;SELECT&lt;br&gt;
    CASE WHEN fl.source_id = r.id THEN fl.target_id ELSE fl.source_id END,&lt;br&gt;
    r.depth + 1&lt;br&gt;
  FROM feedback_links fl&lt;br&gt;
  JOIN related r&lt;br&gt;
    ON fl.source_id = r.id OR fl.target_id = r.id&lt;br&gt;
  WHERE r.depth &amp;lt; 3&lt;br&gt;
)&lt;br&gt;
SELECT DISTINCT id, MIN(depth) AS depth&lt;br&gt;
FROM related&lt;br&gt;
WHERE id &amp;lt;&amp;gt; $1&lt;br&gt;
GROUP BY id&lt;br&gt;
ORDER BY depth;&lt;/p&gt;

&lt;p&gt;We tried this first. It's elegant and it's fewer round trips. But recursive CTEs don't cleanly enforce a "stop after N total nodes visited, regardless of depth" limit — you can cap depth, but capping total breadth requires awkward window functions or a hard row limit that can cut off a level halfway through and give you an inconsistent-looking result set. Doing BFS in application code, level by level, gave us a natural point to check "have I collected enough?" between each round trip. More queries, but more control.&lt;/p&gt;

&lt;h2&gt;
  
  
  ⚖️ Complexity Trade-offs at Small Scale
&lt;/h2&gt;

&lt;p&gt;Here's the part that actually matters for a "boring CRUD app with a graph feature" like ours: at our scale (tens of thousands of feedback items, average node degree under 4), textbook BFS complexity of O(V + E) is a non-issue. We're not traversing millions of edges. The real costs are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Round trips, not Big O.&lt;/strong&gt; Each BFS level is a network hop to Postgres. At depth 3 that's at most 3 queries, which is fine. If we ever needed depth 10, we'd batch differently or move to a native graph store.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fan-out nodes, not graph size.&lt;/strong&gt; The actual risk isn't "the graph is too big," it's "one node has too many neighbors." A single popular feedback thread with 80 links can make one BFS level return more rows than three normal traversals combined. This is why &lt;code&gt;maxResults&lt;/code&gt; matters more than &lt;code&gt;maxDepth&lt;/code&gt; in practice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cycles are silent but real.&lt;/strong&gt; Feedback links can form loops (A relates to B relates to C relates back to A), and without the &lt;code&gt;visited&lt;/code&gt; set, plain recursive traversal would infinite-loop or duplicate work. It's the kind of bug that doesn't show up in a demo with 5 tickets and absolutely shows up once support agents start linking things liberally.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We explicitly did &lt;em&gt;not&lt;/em&gt; reach for Neo4j or any dedicated graph database. The join table plus application-level BFS handles our volume with room to spare, and it means one less piece of infrastructure to operate. If our average node degree climbed into the hundreds, or if we needed shortest-path-with-weighted-relations queries across millions of edges, that calculus would flip. Picking the graph database on day one for a feature that queries at most a few thousand edges is optimizing for a scale problem we don't have yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  🚀 Where This Goes Next
&lt;/h2&gt;

&lt;p&gt;The obvious next step is weighting edges by relation type — a "duplicate" link probably matters more than a "loosely related" one — which turns this from plain BFS into something closer to Dijkstra territory. We haven't needed it yet, but it's a good sign that starting with the simplest correct algorithm leaves you room to grow instead of boxing you in.&lt;/p&gt;

&lt;p&gt;Have you shipped a "basic" algorithm like BFS or DFS in a real feature and had people be surprised it wasn't over-engineered? I'd like to hear what problem it solved for you — and whether you eventually outgrew it.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>algorithms</category>
      <category>node</category>
      <category>webdev</category>
    </item>
    <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>
  </channel>
</rss>
