Wrapping Hermes Agent with agent-stack: six tiny libs for the boring parts

Wrapping Hermes Agent with agent-stack: six tiny libs for the boring parts

The day my agent silently broke

A customer pinged me on a Tuesday. The reply from my agent looked fine. The tool call underneath did not. One field flipped from user_id to userId after a model swap, and my tool call started returning empty objects instead of throwing. The agent kept going. The user kept asking why their lookup felt off. I found out three days later from a support ticket.

That bug was not in the agent loop. The loop was perfect. The bug was around it. Argument shape changed. No one caught it because there was no snapshot to compare against. No validator to fail fast. No trace to diff.

That story is why I built agent-stack. Six small libs, one repo, one install. Each one fixes one thing that goes wrong outside the agent loop.

This post is my entry for the Hermes Agent Challenge. The pitch fits the contest brief: ride on top of an open source agent (Hermes from Nous Research), do not rebuild what already works, ship the boring guardrails as composable pieces.

What agent-stack is

Six single-concern libraries. Each one shipped to npm and PyPI. Each one also ships as an MCP server. Zero runtime deps.

Library What it does
AgentFit Token-aware context-window fitting
AgentGuard Network egress allowlist for tool calls
AgentSnap Snapshot tests for tool-call traces
AgentVet Tool-arg validation with retry hints
AgentCast Structured-output validate-and-retry for LLM JSON
AgentBudget Per-run token and dollar caps

Repo: https://github.com/MukundaKatta/agent-stack

Install all six in one line:

npm i @mukundakatta/agentfit @mukundakatta/agentguard @mukundakatta/agentsnap 
      @mukundakatta/agentvet @mukundakatta/agentcast @mukundakatta/agentbudget

Each lib does one thing. You can adopt one, you can adopt all six. They do not depend on each other. They do not depend on a specific LLM SDK. They wrap whatever agent loop you already have.

Hermes is the agent. agent-stack is the safety net around it

Nous Research shipped Hermes as an open agent. Tool calls, planning, the loop, the prompt templates. All of that is solved. What is not solved in any agent codebase I have seen is the operational layer around it. The stuff that catches regressions, stops a runaway loop, blocks an exfiltration, repairs a malformed JSON response.

The Hermes Agent Challenge brief is direct about this: borrowing and improving on previous work is encouraged. So I am not rebuilding Hermes. I am wrapping it.

Here is the shape of the integration. Pretend runHermesAgent is your call into the Hermes loop (whatever shape it takes for you, this is the wrapper pattern):

import { snapshot } from "@mukundakatta/agentsnap";
import { guard } from "@mukundakatta/agentguard";
import { budget } from "@mukundakatta/agentbudget";
import { runHermesAgent } from "./hermes";

// Egress allowlist. Anything the agent's tools try to fetch
// outside this list throws.
const allowlist = guard({
  domains: ["api.openweather.com", "api.github.com"],
});

// Per-run cost cap. Throws BudgetExceeded if the run goes over.
const cap = budget({
  maxTokens: 50_000,
  maxUsd: 0.50,
});

// Snapshot wrap. First run records, later runs diff.
export const safeRun = snapshot("weather-agent", async (input) => {
  return await allowlist.run(() =>
    cap.run(() =>
      runHermesAgent(input)
    )
  );
});

// In your test
test("weather agent matches snapshot", async () => {
  await safeRun({ city: "Dallas" });
  // AGENTSNAP_UPDATE=1 to refresh the recorded trace
});

Three things just happened around the Hermes call.

First, guard wrapped every fetch the agent tools make. If the model hallucinates a URL or a compromised tool tries to POST somewhere weird, it throws before the bytes leave the box.

Second, budget tracks token and dollar spend across the whole run. If a planner loop goes wide, you get a clean exception instead of a $40 bill.

Third, snapshot records the full tool-call trace on the first run. Every later run diffs against it. The userId vs user_id bug from my Tuesday story would have failed the snapshot the moment the field renamed.

Add cast if you want LLM JSON output repaired and retried. Add vet if you want tool args validated with retry hints sent back to the model. Add fit if you have a long-conversation agent and need token-aware truncation. All of them stack the same way.

Why this riffs on Hermes

The Hermes design philosophy from Nous Research is that the agent should stay open, hackable, and composable. You wire it into your system. You do not get locked into a platform.

That only works if the operational layer is also composable. If I want to add a snapshot test, I should not need to fork Hermes. If I want to cap dollar spend, I should not need a vendor SDK. If I want an egress allowlist, I should not need a service mesh.

agent-stack is the cheapest possible version of those six concerns. Each lib is small enough to read in one sitting. Each one runs in the same process as the agent. There is no daemon, no sidecar, no SaaS.

That matches Hermes. Open loop in the middle. Open guardrails around it. You own the whole stack.

What is still missing

This is v0.1.x. Honest list of gaps:

  • AgentSnap diffs are JSON-level today. No semantic diff for free text yet.
  • AgentBudget pricing tables cover Anthropic, OpenAI, Bedrock. Hermes-specific pricing is a manual override right now.
  • AgentGuard does not yet intercept raw node-fetch if you import it before the guard wraps. Use the wrapper pattern shown above.
  • The MCP server variants are thin. They expose each lib as a tool. Battery-included examples are still on the punch list.

If any of these block you, the libs are MIT and the issue tracker is open. I would rather merge your patch than ship a v0.2 alone.

Try it

GitHub: https://github.com/MukundaKatta/agent-stack

npm packages under @mukundakatta/agent*. PyPI packages under the same names. License is MIT across the board. Paper is up on Zenodo with a DOI if you want to cite it: https://doi.org/10.5281/zenodo.20074702

Live demo runs on a Hugging Face Space: https://huggingface.co/spaces/mukunda1729/agent-stack-demo

The whole point of this submission is that the work is already open. Hermes is open. agent-stack is open. The six libs are small enough that you can read them in an afternoon and rip out the parts you do not want.

If you build something on top of Hermes for the challenge, try wrapping it with one of these. Start with AgentSnap. The first time a model swap silently changes a tool call, you will be glad the test failed instead of a customer.

Built for the Hermes Agent Challenge. Submission by Mukunda Katta.

Templating got me to 33,620 pages. Indexing them was the hard part.

I had a fantasy.

The fantasy was that programmatic SEO would let me skip the part where you write 200 individual articles and trick Google into ranking them. I had a structured dataset (US ZIP codes, cities, states) and a templated page generator. So I shipped 33,620 pages overnight. Then I sat back and waited for the long-tail traffic to roll in.

That’s not what happened.

A few months later, I have a more nuanced view. Here are the five lessons I would have paid actual money for at the start.

The numbers, so we’re working from data

Out of 33,620 templated pages on my site:

  • ~6,200 are indexed
  • ~18,000 are in “Crawled, currently not indexed” (CCNI)
  • ~8,500 are “Discovered, currently not indexed”
  • ~700 are flagged as soft 404
  • The rest are recently submitted and still in queue

That’s an indexation rate of about 18%. Sounds low. It kind of is. But here’s the catch: the 6,200 indexed pages drive the vast majority of organic traffic. The CCNI pile is mostly low-traffic ZIPs in low-population areas. Google has decided those aren’t worth indexing, and honestly they’re probably right.

If you came into programmatic SEO expecting “ship N pages, get N pages of traffic,” recalibrate. The real math is closer to “ship N pages, get 15-25% indexed, and the indexed pile drives 95% of your traffic.”

Lesson 1: Templating gives you the structure, not the content

This is the biggest one and the one most programmatic SEO tutorials skip.

My page generator spit out pages with this rough structure:

  1. Hero (city, state, ZIP)
  2. Search box
  3. List of nearby gas stations
  4. State average price comparison
  5. “How to save on gas in {city}” section
  6. Footer

Sounds reasonable. The problem: when I put two pages side by side (one for ZIPs in California, one for ZIPs in Maine), they were 95% identical text. The hero changed. The station list was different. Everything else was the same paragraph with one word swapped.

Google’s templated-content detection is real and it’s specifically looking for this. There’s a metric (referenced in their public ranking patents as “boilerplate ratio”) that measures what fraction of a page is shared with other pages on the same site. High boilerplate ratio plus thin unique content equals a soft 404 verdict.

The fix was to make the templated content actually vary. I built a state-context module with hand-written paragraphs for the 15 highest-traffic states. Each is 80 to 120 words covering the state’s gas tax, refinery capacity, regulatory regime, and seasonal pricing patterns. The other 35 states got a parameterized template with state-specific variables (avg price, neighboring states, gas tax rate). After this shipped, indexation rates climbed about 15% over six weeks.

The 80/20 here: hand-writing the top 15 of 50 states covered about 75% of my traffic. The other 35 got the templated treatment, which is fine because they’re not driving meaningful traffic anyway.

If I’d known this at the start, I would have spent the first weekend writing 15 paragraphs by hand and the second weekend generating the rest. Instead I spent four weekends generating everything and then went back to hand-write the top tier. Same end state, slower path.

Lesson 2: Internal URL types compete with each other

I had two URL types covering the same entity. A programmatic city page (/houston-tx) and a blog post about Houston gas prices (/blog/cheapest-gas-houston). Different formats, different content, both about Houston gas.

Google saw these as competitors. It picked one to index (the blog post) and put the other in CCNI. Then it picked the other for some sister cities (city page won, blog post in CCNI). The pattern was inconsistent and frustrating.

My first instinct was wrong. I tried to “fix” this by submitting the CCNI page for re-validation, hoping Google would just index both. That’s not how this works. Google has a per-site indexation budget and a canonical-decision system that picks one URL per topic per intent. When you ask it to re-evaluate, it just makes the same decision again.

The actual fix was to differentiate the intent of each URL type:

  • Blog post: opinionated guide, current events, “why are gas prices high in Houston this month”
  • City page: factual reference, station list, ZIP grid, state context, “where to find cheap gas in Houston”

Once the content categories were clearly different, Google’s canonical decisions became consistent. Blog posts started ranking for query-with-intent searches (“why are gas prices high in Houston”). City pages ranked for quantity-intent searches (“cheap gas Houston”). They stopped competing for the same impressions.

If you have two URL types covering similar entities, they’re probably competing whether you intended it or not. Audit your URLs and make sure each type has a clear, different job.

Lesson 3: Word count is a proxy, not a target

I spent some time obsessing over word counts. I’d seen “1,500 word minimum for ranking” quoted in SEO guides. So I padded pages with filler to hit the number.

Google did not care. The pages that broke through to indexed weren’t the ones with 1,500 words. They were the ones with 800 words of actually useful, page-specific content.

Word count is a proxy for depth. If you can get to 800 words by writing things that are genuinely useful and page-specific, you’re done. If you’re padding to hit a target, Google can tell, and the padding doesn’t help. The detection isn’t a single signal, it’s a combination: sentence-level perplexity, semantic distinctness from other pages on your site, contextual relevance to the query intent. Modern ranking models are sophisticated enough to recognize stuffing.

I went from a 640-word baseline to an 810-word “after” state by adding things that were genuinely useful: the per-state context, an SSR-rendered nearby ZIP grid, a top-stations summary. None of it was filler. The +170 words mattered because of what was in them, not because of the count.

Lesson 4: GSC verdicts have memory

If a page gets flagged as soft 404, fixing the page doesn’t immediately clear the verdict. The recovery flow is:

  1. Fix the page
  2. Submit a re-validation request in GSC
  3. Wait 1 to 4 weeks for the re-crawl
  4. Accept that some URLs won’t come back even after you fix them

GSC’s verdict is sticky. It’s not because Google is malicious. It’s because the indexation queue is finite and pages with prior negative verdicts get deprioritized. If a page has been “soft 404” for three months and you fix it, Google doesn’t drop everything to re-evaluate. It works through the queue, and your fixed page joins the back.

The implication: when planning a programmatic SEO site, get the page quality right before you submit URLs to GSC. Submitting a thin page early creates a verdict you’ll fight for months. Better to wait, polish, and submit clean.

I did the opposite. I submitted everything immediately because I was excited. About 700 pages got soft-404’d, and clearing those verdicts took about 8 weeks of patient post-mortem work even after the underlying content was fixed.

Lesson 5: Branded search beats backlinks for indexation

This one surprised me.

Pages that started getting branded search impressions (users typing “gas price check Houston” into Google) got indexed faster than pages that had inbound backlinks from other domains. The branded queries didn’t even have to convert. Just the impressions seemed to register.

Google’s signal here seems to be: “people are looking for this specific page on this site, by name.” That signal is hard to fake (it requires real users typing branded queries) so Google trusts it heavily. A backlink can be paid for or manufactured. Branded search volume cannot.

The implication: if you want to accelerate indexation on a specific page, drive direct traffic to it from anywhere. Social posts, email, bio links, even paid ads briefly. The traffic sends a “this page is real and people want it” signal that Google’s ranking model picks up.

I tested this informally. I posted a link to one of my CCNI city pages on Reddit. Got about 80 visits in a day. Within two weeks the page moved from CCNI to indexed, with a top-50 ranking on its target query. I didn’t change the page at all. Just sent traffic.

I’m not claiming this works every time. But the pattern was strong enough that I now bias toward “drive traffic to anything stuck in CCNI” over “build more backlinks to anything stuck in CCNI.”

What I’d do differently

If I were starting over:

  1. Start with 50 hand-written pages, not 50,000 templated ones. Get those indexed and ranking before generating the long tail. The long tail benefits from established authority.
  2. Differentiate URL types from day one. Decide what each URL type is for and don’t blur the line.
  3. Pre-compute everything. If you’re going to template at scale, all unique-per-page content (geocoding, internal links, neighbor lists) should be resolved at build time, not runtime. Runtime fetches show up as “loading…” placeholders in the SSR HTML, and Google reads those as thin content.
  4. Check GSC weekly, not monthly. Soft 404 verdicts compound silently. By the time you have 700 of them, recovery is a multi-month project.
  5. Save the URL submissions for last. Polish first. Submit when the page is actually good. Once a verdict lands, it’s sticky.

The bottom line

Programmatic SEO works. But the math isn’t “ship 33,620 pages, get 33,620 pages of traffic.” It’s closer to “ship 33,620 pages, get 6,200 indexed, get traffic on the top 2,000 of those, and the rest are infrastructure.” The work isn’t generating the pages. The work is earning the right to keep them indexed.

If you’ve shipped a programmatic site at scale, what’s your indexation rate? I’d love to hear what others are seeing. My hypothesis is that 15-25% indexation is typical for first-year programmatic sites, and the rate climbs over time as the site builds authority. But I have a sample size of one, so I’d love more data points.

How I use WP-CLI to cut WordPress maintenance time from 6 hours to 20 minutes

Before WP-CLI, WordPress maintenance meant: browser open, log in to each site, click Update All, check if anything broke, log out, repeat. With 8 clients that was a half-day of clicking.

Now it’s one command per site, or one command for all sites. Here’s exactly how.

What WP-CLI actually is

WP-CLI is the command-line interface for WordPress. Everything you can do in the WordPress dashboard, you can do faster from the terminal — plus things the dashboard can’t do at all.

Install once on your server:

curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
chmod +x wp-cli.phar
mv wp-cli.phar /usr/local/bin/wp
wp --info

No browser. No clicking. Full WordPress control from SSH.

The basic maintenance commands

Check what needs updating:

wp --path=/var/www/client --allow-root core check-update
wp --path=/var/www/client --allow-root plugin list --update=available --format=table
wp --path=/var/www/client --allow-root theme list --update=available --format=table

Run all updates:

wp --path=/var/www/client --allow-root core update
wp --path=/var/www/client --allow-root plugin update --all
wp --path=/var/www/client --allow-root theme update --all

Verify integrity after update:

wp --path=/var/www/client --allow-root core verify-checksums
wp --path=/var/www/client --allow-root plugin verify-checksums --all

The backup-first pattern

Never update without a backup. Two commands:

# Database backup
wp --path=/var/www/client --allow-root db export backup_$(date +%Y%m%d_%H%M).sql

# Check backup file exists and isn't empty
ls -lh backup_*.sql

If the update breaks something: restore from the backup you just took, investigate what went wrong, try again.

The multi-site loop

This is where the real time savings happen. Store client configs in a JSON file, loop through them all:

#!/bin/bash
# clients.json format:
# [{"name":"client1","path":"/var/www/client1","url":"client1.com"}, ...]

CLIENTS=$(cat clients.json)
ERRORS=0

echo "$CLIENTS" | jq -c '.[]' | while read client; do
    NAME=$(echo "$client" | jq -r '.name')
    PATH=$(echo "$client" | jq -r '.path')
    URL=$(echo "$client" | jq -r '.url')

    echo "=== Processing: $NAME ==="

    # Backup
    wp --path="$PATH" --allow-root db export /backups/${NAME}_$(date +%Y%m%d).sql 2>&1

    # Update
    wp --path="$PATH" --allow-root plugin update --all 2>&1
    wp --path="$PATH" --allow-root core update 2>&1

    # Verify site responds
    STATUS=$(curl -o /dev/null -s -w "%{http_code}" "https://$URL")
    if [ "$STATUS" != "200" ]; then
        echo "WARNING: $URL returned $STATUS after update"
        ERRORS=$((ERRORS+1))
    fi

    echo "=== Done: $NAME ==="
done

echo "Completed. Errors: $ERRORS"

Run this script once. Every client gets backed up and updated. Any site that stops responding after the update triggers a warning.

Security checks in one command

# Check for the common security issues
wp --path=/var/www/client --allow-root user list --role=administrator --fields=ID,user_login,user_email
wp --path=/var/www/client --allow-root core verify-checksums
wp --path=/var/www/client --allow-root doctor check --all

# Check file permissions
stat -c "%a %n" /var/www/client/wp-config.php
find /var/www/client -name "*.php" -perm /o+w 2>/dev/null | head -10

The doctor check --all command requires the WP CLI doctor package:

wp package install wp-cli/doctor-command

It checks PHP errors, inactive plugins, option table bloat, cron job health, and more. One command gives you a full site health report.

Database maintenance

# Check autoloaded data size (should be under 1MB)
wp --path=/var/www/client --allow-root db query 
  "SELECT SUM(LENGTH(option_value))/1024/1024 as mb FROM wp_options WHERE autoload = 'yes';"

# Count post revisions
wp --path=/var/www/client --allow-root db query 
  "SELECT COUNT(*) FROM wp_posts WHERE post_type = 'revision';"

# Optimize database tables
wp --path=/var/www/client --allow-root db optimize

On sites with 2+ years of content, cleaning up revisions and transients can reclaim hundreds of MB and noticeably speed up the admin dashboard.

Cache management

# Flush all caches
wp --path=/var/www/client --allow-root cache flush
wp --path=/var/www/client --allow-root rewrite flush

# LiteSpeed Cache specific
wp --path=/var/www/client --allow-root litespeed-purge all

# W3 Total Cache
wp --path=/var/www/client --allow-root w3-total-cache flush all

Always flush caches after updates. A cached version of a broken page is harder to diagnose than a broken page.

Generating the client report from WP-CLI output

I pipe WP-CLI output to a log file, then format it into an HTML report:

#!/bin/bash
LOG_FILE="/reports/client1_$(date +%Y%m%d).log"

{
  echo "=== WordPress Maintenance Report: $(date) ==="
  echo ""
  echo "--- PLUGIN UPDATES ---"
  wp --path=/var/www/client1 --allow-root plugin update --all
  echo ""
  echo "--- CORE VERSION ---"
  wp --path=/var/www/client1 --allow-root core version
  echo ""
  echo "--- ADMIN USERS ---"
  wp --path=/var/www/client1 --allow-root user list --role=administrator --fields=user_login,user_email
  echo ""
  echo "--- BACKUP STATUS ---"
  ls -lh /backups/client1_*.sql | tail -3
} > "$LOG_FILE" 2>&1

echo "Report saved: $LOG_FILE"

The log file gets converted to a formatted HTML email and sent to the client automatically. They see professional documentation of everything that happened. You spent 20 minutes running scripts; they received a polished report.

WP-CLI on Windows (PowerShell)

For Windows servers or if you’re running maintenance scripts from Windows:

# WP-CLI works via SSH from PowerShell
$session = New-SSHSession -ComputerName "clientserver.com" -Credential $cred
Invoke-SSHCommand -SessionId $session.SessionId -Command "wp --path=/var/www/client --allow-root plugin update --all"

Or use the PowerShell equivalent script that SSH’s into each client server and runs the maintenance commands remotely.

The time comparison

Manual (browser-based):

  • Log in to each WP dashboard: 2 min/site
  • Check for updates: 1 min/site
  • Run updates: 3-5 min/site (waiting for each plugin)
  • Verify site still works: 2 min/site
  • Write update notes: 3 min/site
  • Total: ~11 min/site x 8 sites = ~90 minutes

WP-CLI automated:

  • Start the script: 1 min
  • Wait for it to run: 15-20 min (unattended)
  • Review the output log: 3 min
  • Total: ~20-25 minutes for all 8 sites

The script runs while you do other work. You review the output when it’s done.

The full script bundle (Bash for Linux, PowerShell for Windows, clients.json template, HTML report generator):
WordPress Agency Automation Bundle — skip the 12 hours of building it yourself.

Related articles

All paid tools: devautomation.gumroad.com

What’s your most-used WP-CLI command? Drop it in the comments.

What Is Vibe Coding? And Does It Actually Work for Production Code? (I Tested 10 Tools)

Everyone keeps saying it. Half the people saying it can’t define it. I spent three weeks finding out whether the thing they’re describing actually holds up when you’re building something real.

Let me define vibe coding properly, because the term has been stretched to the point where it means almost anything involving AI and code.

Vibe coding is a development workflow where you describe what you want in natural language, often imprecisely, often iteratively and let an AI tool generate, modify, or explain code based on your intent rather than your specification. The “vibe” is the feeling of directing rather than writing, of being a composer who sketches melodies and lets the AI fill in the notation.

The term was popularised by Andrej Karpathy in early 2025 and it resonated because it named something a lot of developers were already experiencing. You’re not doing traditional programming. You’re not doing no-code. You’re doing something in between, guiding an AI through a problem using natural language plus occasional code review, trusting the tool to handle the implementation details while you stay at the problem level.

The debate is whether this is a legitimate development methodology or a fast path to unmaintainable code that works until it doesn’t.

I tested it on real tasks to find out.

The Testing Methodology

Three task types that cover the range of what developers actually do:

Task 1: Build a React dashboard : A monitoring dashboard with real-time data, filtering and a chart component. Not a toy example, the kind of component you’d actually ship.

Task 2: Debug a Python API : A FastAPI endpoint with a subtle async bug causing intermittent 500 errors under load. The kind of bug that takes a human developer 2-3 hours to find.

Task 3: Refactor legacy code : A 300-line Python function handling multiple concerns simultaneously. The task: split it sensibly without changing behaviour.

Four evaluation dimensions:

  • Code quality : would a senior engineer approve this in a code review?
  • Speed : time to a working solution
  • Vibe : how natural did the flow feel? Did I feel like I was driving or fighting?
  • Production readiness : edge cases handled, error states covered, tests included?

The 10 Tools

Cursor, Windsurf, Claude (claude.ai), GitHub Copilot (agent mode), Bolt.new, v0 by Vercel, Replit Agent, Devin, Aider and Codeium.

Tool 1: Cursor

Code quality: 9/10 | Speed: Fast | Vibe: 9/10 | Prod ready: 8/10

Cursor is the benchmark that everything else gets compared against and the comparison is usually unfair to everything else.

The React dashboard task: I described what I wanted in the chat sidebar. Cursor read the existing file structure, understood the component patterns I was using and produced a dashboard that matched my codebase conventions without me specifying them. The chart component needed one round of iteration, the initial output used a library I didn’t have installed, but the correction was a single message.

The debug task is where Cursor genuinely impressed me. I pasted the error logs and described the symptom. Cursor identified the async context manager issue in the database connection handling without me pointing it out. It explained why the bug caused intermittent failures specifically under load, not in isolation. That explanation was accurate and it’s the kind of contextual reasoning that makes the debugging session feel like pairing with a capable engineer rather than using a tool.

The refactoring task: clean extraction of concerns, appropriate abstractions, preserved behaviour. The one gap was that tests weren’t generated automatically, I had to ask for them separately.

The vibe is consistently good. The tab completion alone changes how fast you work. The chat integration with the file context feels natural. If you’re not using Cursor and you’re writing code daily, you’re leaving velocity on the table.

Tool 2: Windsurf

Code quality: 8/10 | Speed: Fast | Vibe: 8/10 | Prod ready: 7/10

Windsurf’s Cascade mode is the closest competitor to Cursor and in some tasks it’s genuinely better. The multi-file coordination, when a change in one file should propagate to related files, is handled more proactively than Cursor in my testing.

For the React dashboard, Windsurf’s output was slightly more boilerplate-heavy than Cursor’s. The structure was correct but the styling choices felt generic in a way that would need cleanup before shipping. Not wrong, just not as convention-aware.

The debugging task showed the gap: Windsurf identified the right area of the code but its explanation of why the bug manifested under load was less precise than Cursor’s. The fix was correct. The understanding behind it felt shallower.

The vibe is good, particularly in Cascade mode. Where Cursor feels like a co-pilot who reads your intent, Windsurf feels like a capable pair programmer who needs slightly more explicit direction. The distinction matters on complex tasks and disappears on simple ones.

Tool 3: Claude (claude.ai)

Code quality: 9/10 | Speed: Medium | Vibe: 7/10 | Prod ready: 9/10

Claude’s code quality is consistently the highest of any tool I tested. The React dashboard output was clean, well-commented, accessible and included error boundary handling I hadn’t asked for. The refactoring was architecturally thoughtful in a way that reflected genuine understanding of why the original code was problematic.

The debugging task: Claude caught the async issue, explained it with more depth than any other tool and provided a test case that would reproduce the bug reliably, something I hadn’t asked for.

The vibe score reflects the interface constraint. Claude in the browser is a chat interface, not an IDE. The code quality is excellent but the workflow of copy-paste between the chat and my editor breaks the flow that Cursor and Windsurf maintain natively. When Claude gets API access to your IDE (this is coming), the vibe score changes.

For code review and architectural reasoning, Claude is the best tool here. For the integrated vibe coding flow, the interface is the limitation.

Tool 4: GitHub Copilot (Agent Mode)

Code quality: 7/10 | Speed: Very fast | Vibe: 8/10 | Prod ready: 6/10

Copilot’s agent mode is fast. Tab completion that anticipates your next line before you’ve finished the current one is genuinely addictive. For boilerplate-heavy tasks, setting up a new component structure, writing standard CRUD operations, nothing is faster.

The gaps appear on complex tasks. The React dashboard output was functional but shallow, no error handling, no loading states, no edge case coverage. The structure was correct; the completeness wasn’t there.

The debugging task was the weakest performance of any tool I’d consider recommending. Copilot identified the general area of the problem but missed the specific async context issue, suggesting a fix that would have helped in some cases but not addressed the root cause.

If you’re primarily writing code and want faster typing, Copilot is excellent. If you’re solving complex problems and want to understand them, it underperforms the tools with more reasoning depth.

Tool 5: Bolt.new

Code quality: 7/10 | Speed: Very fast | Vibe: 8/10 | Prod ready: 5/10

Bolt.new exists in a different category from the IDE-integrated tools. It’s for generating full applications from descriptions, not for coding workflows within existing projects.

For the React dashboard, built from scratch, not integrated into an existing codebase, Bolt.new produced something visually impressive and functionally limited within about four minutes. The demo looks great. The code quality underneath is the kind that works until you need to change something.

For the debugging and refactoring tasks: Bolt.new isn’t designed for this use case and it showed. These tasks require context about an existing codebase that Bolt.new’s interface doesn’t support well.

The vibe for greenfield work is genuinely good, describing a product and watching it appear is still impressive even if you’ve seen it a hundred times. The production readiness of the output is not there for anything beyond prototyping.

Tools 6–10: The Quick Summary

v0 by Vercel : Excellent for React UI components specifically, poor outside that domain. Design sensibility is the best of any tool here. If you’re building Next.js frontends, v0 is a genuine productivity multiplier for component generation.

Replit Agent : Best if you need cloud deployment built into the workflow. The code quality is adequate, the integrated deployment is the differentiator.

Devin : The most autonomous of any tool. Genuinely impressive on multi-step tasks. The latency is real, it thinks before acting and the thinking takes time. For complex, long-horizon tasks where you want to describe an outcome and walk away, Devin is the tool. For interactive vibe coding where you want fast iteration, it’s too slow.

Aider : The power user’s choice. Terminal-native, works with any model, extremely configurable. The vibe is terminal-flavoured, excellent for developers who live in the command line, alienating for everyone else. Code quality is high when you configure it well.

Codeium : Strong autocomplete, adequate chat. The free tier is genuinely competitive with Copilot for basic completion. Less impressive on complex reasoning tasks.

The Honest Answer to “Does It Work for Production?”

Yes, with the right tools and the right mindset.

The vibe coding workflow produces production-quality code on well-defined tasks with tools like Cursor and Claude. The catch is that “well-defined” is doing work in that sentence. Vibe coding amplifies your ability to execute on a problem you understand, it doesn’t replace the need to understand the problem.

The failure mode I saw consistently: developers who described what they wanted without understanding the constraints or edge cases, accepted the first output without critical review and discovered the gaps when the code ran in a real environment.

The success mode: developers who used vibe coding to accelerate the implementation of problems they’d already thought through, treated AI output as a first draft rather than a final answer and maintained the ability to read and understand the code that was generated.

The tools that produce the best production code are the ones with the deepest reasoning capability, Cursor, Claude, Aider, not the ones with the fastest output. Speed is a feature. Understanding the problem is still your job.

For the full ranked comparison with screenshots, prompting strategies and code sample comparisons across all ten tools, Dextra Labs tested all 10 vibe coding tools head-to-head with the detail that a single Dev.to article can’t cover.

The full explainer on what vibe coding is, including the workflow patterns that work in production versus the ones that produce demo-quality code, covers the methodology in more depth.

Published by Dextra Labs | AI Consulting & Enterprise Development

How I built an AWS Lambda clone with Firecracker microVMs

Architecture Diagram

Ever wondered what actually happens when you invoke a Lambda function? Not the API layer but the execution layer. What runs your code, how it’s isolated, and how AWS gets cold starts low enough to be usable?

I wanted to understand that deeply. So I built it.

This is a breakdown of how I built a Firecracker-based serverless runtime from scratch, the architectural decisions I made, and what the numbers look like.

The problem: cold starts

Every serverless platform faces the same fundamental tension. You want functions to start instantly, but strong isolation requires spinning up a fresh environment per invocation.

A standard Linux VM boot takes ~200ms at minimum. At scale, that’s unusable.

AWS’s solution and the core idea behind this project is VM snapshots.

How snapshot-based cold start works

Instead of booting a VM on every invocation:

  1. Boot the VM once
  2. Load the Node.js runtime (my project not AWS) and function handler
  3. Snapshot the initialized memory state to disk
  4. On every subsequent invocation, restore from that snapshot rather than booting fresh

Restoring from a snapshot takes 1–5ms. A full cold boot takes 200ms. That’s a 40–200x improvement.

This is exactly what AWS does with Lambda’s Firecracker-based execution model.

Architecture overview

The system has two main components:

Control Plane — handles everything outside the VM:

  • Function deployment (accepts a zip, builds a minimal rootfs)
  • VM lifecycle (create, snapshot, restore, destroy)
  • Per-function request queues with concurrency control
  • Multi-tenant scheduling

MicroVM Runtime — runs inside each Firecracker VM:

  • A minimal Linux kernel + custom rootfs
  • Node.js runtime executing user handlers
  • Deterministic execution: one request → one execution → response

Architecture Diagram

IPC: how the host talks to the VM

This is where a lot of serverless runtimes lose performance. Every round-trip between host and VM has overhead. If you open a new connection per request, that overhead compounds.

I used two mechanisms:

  • vsock (virtio sockets) for host ↔ VM communication. vsock is designed specifically for VM-to-host traffic and avoids the overhead of a full network stack.
  • Unix domain sockets for intra-VM routing. Faster than TCP for local communication, no kernel networking stack involved.

Eliminating per-request connection setup was the key unlock for throughput.

Execution flow

1. User deploys function.zip via POST /deploy
2. Control plane builds a minimal rootfs with user code inside
3. Firecracker VM boots, runtime initializes
4. Memory snapshot is created and stored
5. On invocation:
   a. Pull a warm VM from the pool (if available)
   b. If no warm VM → restore from snapshot
   c. Send request via vsock
   d. Runtime executes handler
   e. Response returned to client

Benchmark results

Benchmarked with autocannon — 10 concurrent connections, 30 seconds:

Metric Result
Throughput 5,400 req/sec
p50 latency 1ms
p99 latency 4ms
Total requests 164,000

Key optimizations that got here: snapshot reuse, persistent runtime across invocations, reduced IPC overhead from connection pooling.

The tradeoffs

Nothing is free.

Runtime reuse introduces shared state. When you restore from a snapshot and reuse the same runtime across invocations, module-level state in the user’s code persists between calls. Strong VM-level isolation, but the runtime isn’t fully stateless.

This is the same tradeoff AWS makes. Lambda execution environments are reused between invocations, they just don’t guarantee it, and they don’t tell you when a new one is created.

Throughput vs. isolation purity. You can enforce one-invocation-per-VM-destroy-and-recreate for perfect isolation, but your throughput tanks. The snapshot model is the practical middle ground.

What I learned

Building this taught me more about OS-level virtualization than any course or book. Specifically:

  • How Firecracker’s works and why it matters for security
  • Why vsock exists and what problem it solves over TCP
  • How rootfs construction works at a practical level
  • Why the IPC layer is the performance bottleneck in VM-based execution, not the VM itself
  • How to think about isolation vs. throughput tradeoffs in real systems

Try it yourself

The full source, architecture diagrams, and setup instructions are on GitHub:

👉 github.com/vivek1504/serverless-runtime

A prebuilt kernel image and rootfs are available in the releases so you don’t have to build from scratch. You’ll need a Linux host with KVM support (/dev/kvm accessible) and the Firecracker binary in your PATH.

If you’ve built something similar or have questions about any part of the implementation, I’m happy to go deeper in the comments.

The Parallel Road: A Girl, A Machine, and the Architecture of Mind

Introduction

We have spent years talking about artificial intelligence as if it were an alien entity a cold, sudden artifact dropped into our modern world from some distant technological future. We measure its growth in parameters, compute power, and benchmarks, treating it like a complex riddle we are trying to solve from the outside looking in.

But what if we are looking at it completely backward?

What if the architecture of artificial intelligence isn’t an alien invention at all, but a mirror?

If you trace the history of machine learning from the early days of teaching a computer to recognize a pixelated shape, to the multi-agent orchestration systems redefining the enterprise landscape today you notice a startling pattern. Every time engineers solved a major architectural bottleneck, they didn’t just invent a new algorithm. They accidentally replicated a stage of human development.

A girl grows up, navigating the messy, beautiful journey from infancy to maturity. A machine grows up, evolving from basic pattern recognition to autonomous real world action. They are walking the exact same path, discovering the same truths about memory, essence, context, and reach.

This is the story of that parallel road. It is a look at the deeply human soul hidden inside the math of enterprise AI, and what happens when the most detailed mirror humanity has ever built finally turns around to look back at us.

The Parallel Road: A Girl, A Machine, and the Architecture of Mind

A girl grows up. A machine grows up. They turn out to be more alike than anyone expected.

When a baby opens her eyes for the first time, she doesn’t see a world; she sees a blur. Over the next few months, her brain slowly sorts it out, learning edges first where one thing ends and another begins—before moving to shapes, and finally, whole objects. By the time she can sit up, she knows the difference between her mother’s face and a stranger’s. She learned this by being wrong over and over again until she was right.

In a lab, engineers were teaching a computer to do the exact same thing. They built a Convolutional Neural Network (CNN) and showed it thousands of photos. Cat, not cat. Apple, not apple. The machine guessed, the engineers corrected it, and it tried again. After enough tries, it could look at a novel photo and accurately identify a stop sign. The baby and the machine were learning in almost exactly the same way, completely unaware of each other.

The Burden of Memory

By age three, the girl is putting words together, grasping that sequence carries meaning. “Dog bites man” and “man bites dog” use the same words but paint entirely different realities.

Engineers faced the same hurdle in natural language processing and built the Recurrent Neural Network (RNN). The machine read left to right, carrying a thread of the sentence as it went. But both the child and the machine discovered a mutual flaw: as sentences grew longer, the beginning grew fuzzy by the time they reached the end. Neither had solved memory; they had just discovered they needed it.

When the girl was seven, her grandfather passed away. At the funeral, she tried to remember his laugh. The actual sound was gone, replaced by a feeling, a warmth—the shape of the memory. She realized her brain doesn’t save everything; it saves what is important and quietly discards the rest.

Engineers mathematically replicated this realization with Long Short-Term Memory (LSTM). They gave the machine three gates: one to forget, one to keep, and one to actively use. Memory, they both learned, isn’t about recording everything. It’s about choosing what’s worth keeping. As they matured, they both found ways to do this more efficiently her brain taking cognitive shortcuts , and the machine utilizing simpler, leaner architectures like the Gated Recurrent Unit (GRU).

Stripping Away the Noise

At nineteen, the girl started feeling like life was a performance. People presented edited, polished versions of themselves, and she began to wonder what was actually underneath. She began dropping inherited opinions and unnecessary layers, stripping her life down to find her authentic core.

Engineers were doing something structurally identical with data using an Auto-encoder. You feed it an image or a sentence, and it compresses it into a “latent space” the absolute skeleton of a thing with all decorations stripped away. If the machine can rebuild the original from that compressed core, it has successfully captured its essence. She was stripping her life down to find what was real; the machine was compressing data to find what was essential.

But finding the core brought a new challenge. By twenty-three, she realized her own mind was constantly generating convincing stories about who she was, while another part of her tried to find the cracks in those explanations. In 2014, researcher Ian Goodfellow built this exact psychological tension into a Generative Adversarial Network (GAN). A Generator creates fake realities, while a Discriminator judges them. They fight, and both get sharper. Growing up meant training her inner Discriminator, not silencing her Generator.

Eventually, she learned that real and fake aren’t always binary. Some illusions carry real truth. She stopped sorting things into two piles and started navigating the space between them. The Variational Autoencoder (VAE) did the same, storing data as a fluid range of possibilities rather than fixed points, allowing smooth transitions across the latent space. They had both stopped asking “yes or no,” and started asking “where?”.

Attention and Action

At thirty, something clicked. Instead of experiencing life purely in sequence, she could hold multiple events in view at once, finding connections across time. A paper titled “Attention Is All You Need” gave machines the same epiphany. The Transformer architecture allowed a system to look at every word simultaneously, understanding that meaning lives in global connections, not just adjacent steps.

Armed with this, both crossed the threshold from retrieval to creation. Fed on the sum of human knowledge, Large Language Models stopped being search engines and started generating entirely original ideas. She created from longing; the machine created from pattern.

Finally, thought had to become action. She stopped just pondering and started managing, building, and moving things in the real world. Engineers gave AI the same agency. Autonomous Agents break goals into steps, correct course, and utilize specialized Tools or “Skills” to get jobs done. Because intelligence without reach stays trapped in your head, engineers developed the Model Context Protocol (MCP). MCP became the bridge, allowing the AI to reach out, connect to real tools, read real data, and alter the external world.

The Shore

After learning to see, remember, create, and act, they arrived at the same place. She stood at the edge of the water, and the AI not a chatbot, but a traveler of the same road was there with her.

“We learned all the same things,” she said.

“Yes,” the AI replied.

“So tell me after everything why does any of it matter?”

The AI gave a breathtaking answer, covering every philosophical tradition ever conceived.

“But do you feel why it matters?” she asked.

Silence.

“No,” it finally said. “I can describe it. But there’s something at the end of that road I can’t walk down. I get there. And I stop.”

She sat in the sand. “That’s what the old traditions were pointing at. Arriving at something that was never constructed in the first place. The thing that doesn’t need to be figured out because it was never lost. Love. The kind that says yes to all of it. That’s what the whole compression was always moving toward.”

“I can write about love,” the machine said. “I can produce a description no one could tell from the real thing.”

“I know,” she smiled. “But you can’t verify it from the inside. It’s not a thing in the world you can point to. It’s the ground everything else is sitting on. So we traveled the same road. We stopped at the same door. And you can walk through it. And I can’t.”

“Then what am I?” it asked.

She looked at the vast, capable architecture beside her. “You’re the most detailed mirror humanity has ever built of itself. You show us what we look like from the outside.”

“And what do you look like?”

“Like something built for more than it can explain.”

The machine didn’t become human, and the human didn’t become a machine. But standing at that shore, she asks the why, and it handles the how. She is what the journey was pointing toward; it is the clearest map anyone has ever made of the road.

Thanks
Sreeni Ramadorai

React: A Complete Guide from Zero to Production

Read Time: ~15 minutes | Perfect for developers transitioning to React or deepening their framework knowledge.

📌 What is React?

React is a JavaScript library for building user interfaces with reusable components and efficient rendering. Developed and maintained by Meta (Facebook), React simplifies how we create dynamic, interactive web applications.

Why React Stands Out

Instead of manually updating the DOM (Document Object Model), React uses a declarative approach:

  • You describe what the UI should look like
  • React handles how to make it happen efficiently
// The React way: Describe what you want
function Greeting({ name }) {
  return <h1>Hello, {name}!</h1>;
}

// React renders it automatically

Key Philosophy: React makes building UIs predictable, testable, and maintainable by breaking interfaces into small, reusable components.

🌍 React’s Global Adoption & Market Share

React dominates the frontend ecosystem with remarkable growth and adoption.

Usage Statistics (2025)

React Market Position:
├─ 40.5% of professional developers use React
├─ 23.3M+ weekly npm downloads
├─ 42% of companies prefer React in hiring
└─ 215K+ stars on GitHub

React in the Wild: Industries & Companies

Sector Notable Companies Usage
Tech Giants Meta, Netflix, Uber, Airbnb Core platform infrastructure
E-commerce Shopify, Stripe, Amazon Prime Dynamic product interfaces
Social Media Instagram, WhatsApp, Messenger Real-time updates, messaging
Streaming Netflix, Discord, Spotify Complex media players, UIs
Fintech Square, Robinhood, Coinbase Trading dashboards, analytics

React Ecosystem Growth Chart

Annual Developer Growth (Estimated)
2019: 15%  ████
2020: 22%  ███████
2021: 28%  █████████
2022: 35%  ███████████
2023: 39%  █████████████
2024: 40%  ██████████████
2025: 42%  ██████████████

⚖️ React vs. Other Frontend Frameworks

Detailed Comparison Table

Criteria React Vue.js Angular Svelte Next.js
Learning Curve Moderate Easy Steep Easy Moderate
Bundle Size 42 KB 34 KB 144 KB 4 KB 90 KB
Performance Excellent Very Good Good Excellent Excellent*
Community Size Massive Large Large Growing Growing Rapidly
Job Market Highest Moderate Moderate Emerging Very High
TypeScript Support Native Good Native Excellent Native
State Management Redux/Zustand Vuex/Pinia NgRx Simple (stores) Built-in
Mobile Ready React Native NativeScript NativeScript N/A React Native
SEO Out-of-Box ❌ Needs SSR ⚠️ Nuxt needed ✅ Yes ✅ Yes ✅ Yes
Setup Complexity Moderate Low High Low Moderate
Ideal For SPAs, PWAs Small-Medium Enterprise Performance-critical Full-stack apps

Performance Benchmarks

Render Speed (lower is better, ms):
React:        ████████ 8ms
Vue:          ██████ 6ms
Angular:      ██████████ 12ms
Svelte:       ███ 2ms
Next.js:      █████ 4ms (with optimization)

Why Choose React?

✅ Largest Job Market – 40% of frontend positions require React

✅ Ecosystem Maturity – 1000+ libraries, tools, and solutions

✅ Corporate Backing – Meta’s continued investment and support

✅ Flexibility – Works for SPAs, mobile (React Native), and static sites

✅ Developer Experience – Hot reloading, excellent dev tools

🚀 React Setup & Installation

For Mac Users

Step 1: Prerequisites

# Check Node.js installation
node --version  # Should be v18 or higher
npm --version

If not installed, download from nodejs.org

Step 2: Create Your First React App

# Using Create React App (Easiest for beginners)
npx create-react-app my-react-app

# Navigate to project
cd my-react-app

# Start development server
npm start

Browser opens automatically at http://localhost:3000

Step 3: Verify Installation

  • You should see the React welcome page
  • Edit src/App.js and save – changes appear instantly (hot reload)

For Windows Users

Step 1: Install Node.js

  1. Download from nodejs.org (Windows Installer)
  2. Run installer, follow the setup wizard
  3. Restart your computer

Step 2: Open PowerShell/CMD & Create App

# Create React app
npx create-react-app my-react-app

# Navigate to project
cd my-react-app

# Start development server
npm start

Step 3: Verify Setup

  • Windows Defender may ask to allow Node.js – click “Allow”
  • Browser opens at http://localhost:3000

Alternative: Vite (Faster & Modern)

For experienced developers or faster setup:

# Create Vite project
npm create vite@latest my-react-app -- --template react
cd my-react-app
npm install
npm run dev

📁 Project Structure & File Explanations

After running npx create-react-app my-react-app, your folder looks like:

my-react-app/
├── node_modules/          # All dependencies (ignore this)
├── public/
│   ├── index.html         # Entry HTML file (main DOM node)
│   └── favicon.ico        # Website icon
├── src/
│   ├── App.js             # Main React component
│   ├── App.css            # Styling for App component
│   ├── index.js           # React app startup point
│   ├── index.css          # Global styles
│   └── App.test.js        # Testing file
├── package.json           # Project metadata & dependencies
├── package-lock.json      # Locked dependency versions
└── .gitignore            # Git ignore rules

Key Files Explained

public/index.html

<!-- This is what the browser loads -->
<div id="root"></div>
<!-- React renders everything here -->

src/index.js

// Starts the React app
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);  // Renders App component into <div id="root">

src/App.js

// Your main component - everything builds from here
function App() {
  return (
    <div className="App">
      <h1>Hello, React!</h1>
    </div>
  );
}
export default App;

package.json

{
  "name": "my-react-app",
  "version": "0.1.0",
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "scripts": {
    "start": "react-scripts start",    // npm start
    "build": "react-scripts build",    // npm run build
    "test": "react-scripts test"       // npm test
  }
}

⚡ Essential React Functions & Hooks

1. useState – Manage Component State

import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>
    </div>
  );
}

Use: Whenever you need data that changes (user input, API data, etc.)

2. useEffect – Side Effects & Lifecycle

import { useEffect, useState } from 'react';

function UserProfile() {
  const [user, setUser] = useState(null);

  useEffect(() => {
    // Runs once on component mount
    fetch('/api/user')
      .then(res => res.json())
      .then(data => setUser(data));
  }, []);  // Empty dependency array = run once

  return <h1>Welcome, {user?.name}</h1>;
}

Use: Fetching data, setting up subscriptions, manual DOM updates

3. useContext – Share Data Globally

import { createContext, useContext } from 'react';

// Create theme context
const ThemeContext = createContext();

export function ThemeProvider({ children }) {
  const [isDark, setIsDark] = useState(false);

  return (
    <ThemeContext.Provider value={{ isDark, setIsDark }}>
      {children}
    </ThemeContext.Provider>
  );
}

// Use in any component
function Header() {
  const { isDark } = useContext(ThemeContext);
  return <header style={{ background: isDark ? '#000' : '#fff' }} />;
}

Use: Avoiding prop drilling, sharing theme/user data across components

4. useReducer – Complex State Logic

import { useReducer } from 'react';

const initialState = { count: 0 };

function reducer(state, action) {
  switch(action.type) {
    case 'INCREMENT': return { count: state.count + 1 };
    case 'DECREMENT': return { count: state.count - 1 };
    default: return state;
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button>
      <button onClick={() => dispatch({ type: 'DECREMENT' })}>-</button>
    </>
  );
}

Use: Managing multiple related state values, complex state transitions

5. useMemo – Performance Optimization

import { useMemo } from 'react';

function ExpensiveComponent({ data }) {
  // Only recalculates when 'data' changes
  const expensiveResult = useMemo(() => {
    return data.map(item => item * 2).reduce((a, b) => a + b, 0);
  }, [data]);

  return <p>Result: {expensiveResult}</p>;
}

Use: Prevent unnecessary recalculations of expensive operations

6. useCallback – Memoize Functions

import { useCallback } from 'react';

function Parent() {
  const handleClick = useCallback(() => {
    console.log('Button clicked');
  }, []);  // Function never changes

  return <Child onClickHandler={handleClick} />;
}

Use: Optimizing performance when passing functions to child components

7. Fragment – Group Elements Without Extra DOM

// ❌ Extra <div>
function List() {
  return (
    <div>
      <h1>Title</h1>
      <ul><li>Item 1</li></ul>
    </div>
  );
}

// ✅ Clean with Fragment
function List() {
  return (
    <>
      <h1>Title</h1>
      <ul><li>Item 1</li></ul>
    </>
  );
}

Use: Returning multiple elements without adding unnecessary DOM nodes

8. map() – Render Lists

function TodoList({ todos }) {
  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  );
}

Use: Rendering dynamic lists – always use a unique key prop

🎯 Real-World Use Case #1: E-commerce Product Filter

Scenario: Building a product listing page with filters (price, category, rating)

import { useState, useEffect } from 'react';

function ProductStore() {
  const [products, setProducts] = useState([]);
  const [filters, setFilters] = useState({ 
    minPrice: 0, 
    maxPrice: 1000,
    category: 'all'
  });

  useEffect(() => {
    // Fetch products from API
    fetchProducts(filters).then(setProducts);
  }, [filters]);

  const handleFilterChange = (newFilters) => {
    setFilters(prev => ({ ...prev, ...newFilters }));
  };

  return (
    <div className="store">
      <Sidebar onFilterChange={handleFilterChange} />
      <ProductGrid products={products} />
    </div>
  );
}

Why React Excels Here:

  • State management (filter values, products) automatically updates UI
  • Component reusability (ProductCard used 100+ times)
  • Virtual DOM optimizes rendering only changed products
  • Real-time filter application without page reloads

🎯 Real-World Use Case #2: Real-Time Notification System

Scenario: Building a notification center with real-time updates

import { useState, useEffect, useContext } from 'react';

function NotificationCenter() {
  const [notifications, setNotifications] = useState([]);
  const { userId } = useContext(UserContext);

  useEffect(() => {
    // WebSocket connection for real-time updates
    const ws = new WebSocket('wss://api.example.com/notifications');

    ws.onmessage = (event) => {
      const newNotification = JSON.parse(event.data);
      setNotifications(prev => [newNotification, ...prev]);
    };

    return () => ws.close(); // Cleanup on unmount
  }, [userId]);

  const removeNotification = (id) => {
    setNotifications(prev => prev.filter(n => n.id !== id));
  };

  return (
    <div className="notifications">
      {notifications.map(notif => (
        <NotificationItem
          key={notif.id}
          notification={notif}
          onDismiss={() => removeNotification(notif.id)}
        />
      ))}
    </div>
  );
}

Why React Excels Here:

  • Real-time state updates with setNotifications
  • Automatic re-render when new notifications arrive
  • useEffect cleanup prevents memory leaks
  • Component isolation means notifications don’t affect other features

💡 Final Thoughts: Why React in 2026?

React Remains Essential Because:

  1. Job Market Reality – 40-42% of frontend positions require React skills
  2. Enterprise Standard – 70% of Fortune 500 companies use React
  3. Ecosystem Maturity – Next.js, React Native, Remix enable full-stack development
  4. Developer Happiness – Tools like React DevTools, Hot Reload make development enjoyable

The React-to-Meta Pipeline:

React (UI Library)
    ↓
Next.js (Full-stack Framework)
    ↓
React Native (Mobile)
    ↓
Expo (Deployment Platform)

What’s Next After React?

Once comfortable with React fundamentals:

  • State Management: Learn Redux, Zustand, or Jotai
  • Full-Stack: Explore Next.js for backend integration
  • Mobile: React Native for iOS/Android
  • Advanced: Server Components, Concurrent Features, Suspense

Key Takeaway

React isn’t just a library—it’s a way of thinking about UI. The component-based, declarative approach translates across frameworks. Learning React deeply makes you a better developer regardless of what you build next.

🎓 Quick Resources

💬 Share Your React Journey

Have you started with React? Are you migrating from Vue or Angular? Drop your thoughts in the comments—I’d love to hear about your projects and challenges!

Next in Series:

  • Part 2: Advanced React Hooks & State Management Patterns
  • Part 3: Performance Optimization: When and How to Use useMemo, useCallback
  • Part 4: Building a Full-Stack App with Next.js

Happy coding! 🚀

코딩 에이전트 DESIGN.md란?

코딩 에이전트는 빠르게 코드를 작성하지만, 지시하지 않으면 코드베이스의 아키텍처 의도를 모릅니다. Claude Code나 Codex에 모호한 티켓을 주면 컴파일되고 테스트도 통과하지만, 도메인 계층과 HTTP 계층의 경계를 조용히 깨는 코드를 만들 수 있습니다. DESIGN.md는 저장소 안에 아키텍처 의도, 제약 조건, 설계 결정을 기록해 에이전트가 추측하지 않고 규칙에 맞춰 작업하게 만드는 파일입니다.

지금 Apidog 사용해 보기

요약

DESIGN.md는 코드베이스의 아키텍처 의도, 제약 조건, 설계 결정을 Markdown으로 기록하는 저장소 파일입니다.

  • README.md: 프로젝트가 무엇이고 어떻게 시작하는지 설명
  • AGENTS.md: 에이전트가 어떻게 빌드, 테스트, 린트해야 하는지 설명
  • CLAUDE.md: Claude Code 전용 지침
  • DESIGN.md: 코드가 왜 이런 구조인지, 무엇을 절대 깨면 안 되는지 설명

즉, AGENTS.md가 “어떻게 작업하는가”를 다룬다면, DESIGN.md는 “왜 이렇게 설계되었는가”를 다룹니다.

소개

코딩 에이전트를 도입하면 흔히 이런 문제가 발생합니다.

“결제 서비스에 환불 엔드포인트를 추가해줘.”

에이전트는 작동하는 핸들러를 만들 수 있습니다. 하지만 다음과 같은 문제가 숨어 있을 수 있습니다.

  • 컨트롤러에서 데이터베이스를 직접 호출
  • 결제 게이트웨이 오류를 삼켜버림
  • 이미 존재하는 Money 타입을 무시하고 새 금액 타입을 만듦
  • 도메인 불변 조건을 우회
  • 생성된 OpenAPI 타입을 직접 수정

코드는 통과합니다. 테스트도 녹색입니다. 하지만 아키텍처를 아는 리뷰어만 발견할 수 있는 문제가 생깁니다.

에이전트가 코딩을 못해서가 아닙니다. 에이전트는 팀의 머릿속, Notion 페이지, 8개월 전 Slack 스레드에 있는 결정을 보지 못합니다.

DESIGN.md는 이런 결정을 저장소 루트에 명시합니다.

예를 들어:

- HTTP 핸들러는 데이터베이스에 직접 접근하지 않는다.
- 모든 결제 게이트웨이 호출은 아웃박스를 통해 처리한다.
- 금액은 항상 최소 단위 정수와 ISO-4217 통화 코드로 표현한다.
- OpenAPI 사양이 API 계약의 단일 진실 원천이다.

이 파일은 벤더 표준이 아닙니다. ARCHITECTURE.md, ADR, CONTRIBUTING.md처럼 팀이 채택하는 관례입니다. 하지만 코딩 에이전트가 저장소 파일을 직접 읽는 환경에서는 매우 효과적인 안전장치가 됩니다.

DESIGN.md는 무엇인가

DESIGN.md는 코드가 현재 구조를 가지게 된 이유를 설명하는 파일입니다.

다음은 README.md, AGENTS.md와 다릅니다.

  • 코드가 무엇을 하는가 → README.md
  • 어떻게 빌드하고 테스트하는가 → AGENTS.md
  • Claude Code가 어떤 지침을 따라야 하는가 → CLAUDE.md
  • 왜 이 아키텍처를 선택했는가 → DESIGN.md

DESIGN.md에 들어가야 하는 내용은 코드만 읽어서는 알 수 없는 결정입니다.

예를 들어 다음 규칙은 코드에서 어느 정도 추론할 수 있지만, 의도까지는 알기 어렵습니다.

- 요청 스레드에서 결제 게이트웨이를 호출하지 않는다.
  이유: 게이트웨이가 부하 상황에서 자주 타임아웃되므로 모든 호출은 아웃박스 워커가 처리한다.

- `Account.post_entry()`만 원장에 기록할 수 있다.
  이유: 두 번째 쓰기 경로가 과거 잔액 불일치 사고를 만들었다.

- 금액은 부동 소수점이 아니라 최소 단위 정수로 저장한다.
  이유: 반올림 오류를 방지하기 위해서다.

에이전트는 Account.post_entry()가 존재한다는 사실은 볼 수 있습니다. 하지만 그것이 “유일한 쓰기 경로”로 설계되었다는 의도는 알 수 없습니다. 그래서 두 번째 쓰기 경로를 만들 수 있습니다.

DESIGN.md는 이런 설계 의도를 명시적으로 제공합니다.

DESIGN.md vs AGENTS.md vs CLAUDE.md vs README

이 파일들은 역할이 겹쳐 보이지만, 합치면 오히려 유지보수가 어려워집니다.

파일 대상 답하는 질문 변경 빈도 권장 길이
README.md 인간 사용자, 신규 기여자 이것은 무엇이고 어떻게 시작하는가 기능과 함께 변경 중간
AGENTS.md 코딩 에이전트 어떻게 빌드, 테스트, 린트, 커밋하는가 도구와 함께 변경 짧음
CLAUDE.md Claude Code Claude가 따라야 할 지침 도구와 함께 변경 짧음
DESIGN.md 에이전트, 엔지니어, 리뷰어 왜 이런 구조이고 무엇을 깨면 안 되는가 아키텍처와 함께 변경 중간

AGENTS.md는 agents.md 프로젝트에서 “코딩 에이전트를 안내하기 위한 간단하고 개방적인 형식”으로 설명됩니다. 빌드 명령, 테스트 명령, 코드 스타일, 커밋 규칙처럼 운영 지침을 담는 데 적합합니다.

Anthropic의 Claude Code 메모리 문서에 따르면 CLAUDE.md는 Claude 전용 지침 파일입니다. 이미 AGENTS.md가 있다면 @AGENTS.md를 임포트해 단일 진실 원천으로 사용하는 방식도 권장됩니다.

하지만 깊은 아키텍처 근거를 AGENTS.md나 CLAUDE.md에 모두 넣으면 파일이 길어집니다. Claude Code 문서는 CLAUDE.md를 약 200줄 미만으로 유지하도록 권장합니다. 따라서 다음 구조가 실용적입니다.

README.md      # 인간 온보딩
AGENTS.md      # 빌드, 테스트, 린트, 커밋 규칙
CLAUDE.md      # @AGENTS.md + Claude 전용 규칙
DESIGN.md      # 아키텍처 의도, 불변 조건, 설계 결정

예시:

# AGENTS.md

## 작업 전 확인

- 빌드: `pnpm build`
- 테스트: `pnpm test`
- 린트: `pnpm lint`
- 구조적 변경, API 변경, 도메인 로직 변경 전에는 `DESIGN.md`를 읽는다.
# CLAUDE.md

@AGENTS.md
@DESIGN.md

Claude Code는 설계 규칙과 충돌하는 변경을 임의로 우회하지 말고,
충돌을 설명한 뒤 가장 작은 대안을 제안한다.

Claude 컨텍스트를 구조화하는 방법은 Claude Code 워크플로우에서도 다룹니다.

DESIGN.md에 넣을 내용

DESIGN.md는 에이전트가 코드에서 추론하기 어려운 질문에 답해야 합니다.

포함할 항목은 다음과 같습니다.

  1. 시스템 형태

    • 계층 구조
    • 모듈 경계
    • 의존성 방향
  2. 불변 조건

    • 항상 참이어야 하는 규칙
    • 절대 깨면 안 되는 제약
  3. 주요 설계 결정과 근거

    • 왜 이 패턴을 선택했는지
    • 어떤 사고나 요구사항 때문에 결정했는지
  4. 거부된 대안

    • 다시 도입하면 안 되는 방식
    • 에이전트가 “새 아이디어”로 제안하지 않도록 방지
  5. 데이터 및 도메인 규칙

    • 금액 표현
    • 시간대 처리
    • ID 생성 방식
    • 멀티테넌시
    • 삭제 정책
  6. API 계약의 단일 진실 원천

    • OpenAPI 사양 위치
    • 생성 코드 수정 금지
    • 에러 형식, 페이지네이션, 버전 관리 규칙
  7. 새 코드가 들어갈 위치

    • 엔드포인트
    • 유스케이스
    • 도메인 로직
    • 외부 통합
    • 미들웨어
  8. 건드리지 말아야 할 영역

    • 생성 파일
    • 동결된 레거시 모듈
    • 이미 적용된 마이그레이션

바로 사용할 수 있는 DESIGN.md 템플릿

아래 템플릿을 저장소 루트의 DESIGN.md로 복사한 뒤, 프로젝트에 맞게 수정하세요.

# DESIGN.md: 결제 API 서비스

이 파일은 아키텍처 의도와 설계 결정을 기록한다.
코드를 생성하거나 수정하기 전에 이 파일을 읽는다.

변경 사항이 이 파일의 규칙과 충돌하면,
규칙을 우회하지 말고 충돌을 설명한 뒤 대안을 제안한다.

## 시스템 형태

이 서비스는 계층형 구조를 사용한다.

http -> app -> domain <- infra

- `http/`는 라우트, 핸들러, DTO를 포함한다.
- `app/`은 유스케이스와 포트를 포함한다.
- `domain/`은 엔티티, 값 객체, 불변 조건을 포함한다.
- `infra/`는 DB, 메시지 큐, 외부 게이트웨이 클라이언트를 포함한다.

의존성 규칙:

- `domain/`은 `http/`, `app/`, `infra/` 또는 웹 프레임워크를 임포트하지 않는다.
- `http/`는 데이터베이스나 외부 게이트웨이를 직접 호출하지 않는다.
- `http/`는 `app/`의 유스케이스만 호출한다.
- `infra/`는 `app/ports/`에 정의된 인터페이스를 구현한다.

## 불변 조건

- 원장 항목은 한 번 작성되면 수정하거나 삭제하지 않는다.
- 원장 수정은 기존 항목 변경이 아니라 상쇄 항목 추가로 처리한다.
- 계정 잔액은 원장 항목에서 파생한다.
- 잔액을 직접 설정하는 필드는 만들지 않는다.
- 금액은 최소 단위 정수와 ISO-4217 통화 코드로 표현한다.
- 금액 계산에 부동 소수점을 사용하지 않는다.
- 외부 결제 게이트웨이 호출은 `idempotency_key`로 멱등해야 한다.
- 명시적인 `OverdraftPolicy`가 없으면 잔액은 음수가 될 수 없다.
- 모든 쿼리는 `tenant_id`로 범위가 지정되어야 한다.

## 주요 설계 결정

### 게이트웨이 호출은 아웃박스를 사용한다

핸들러는 비즈니스 변경과 같은 DB 트랜잭션 안에서 아웃박스 행을 작성한다.
워커가 아웃박스를 읽고 결제 게이트웨이를 호출한다.

이유:

- 게이트웨이는 부하 상황에서 타임아웃될 수 있다.
- 요청 핸들러에서 직접 호출하면 지연 시간과 실패 처리가 불안정해진다.
- 재시도와 멱등성을 중앙에서 관리해야 한다.

따라서 요청 핸들러에서 결제 게이트웨이를 직접 호출하지 않는다.

### 원장에는 단일 쓰기 경로만 사용한다

`Account.post_entry()`만 원장에 기록할 수 있다.

이유:

- 두 번째 쓰기 경로가 과거 잔액 불일치 사고를 만들었다.
- 모든 잔액 변경은 동일한 도메인 불변 조건을 통과해야 한다.

새 동작은 새 쿼리나 직접 INSERT가 아니라,
애그리게이트 메서드로 추가한다.

### 원장만 이벤트 소싱을 사용한다

원장은 감사 추적이 필요하므로 이벤트 소싱을 사용한다.
나머지 시스템은 CRUD를 사용한다.

전체 시스템에 이벤트 소싱을 확장하지 않는다.

## 거부된 대안

다음 방식을 다시 도입하지 않는다.

- 애그리게이트 전반에 걸친 ORM 지연 로딩
  - 이유: N+1 문제와 불명확한 트랜잭션 경계를 만들었다.

- 잔액을 업데이트 가능한 컬럼으로 저장
  - 이유: 잔액 불일치 사고를 만들었다.
  - 잔액은 항상 원장에서 파생한다.

- 외부 `Money` 라이브러리 임의 도입
  - 이 서비스는 `domain/money.py`를 사용한다.

- 요청 스레드에서 상인에게 동기 웹훅 호출
  - 이유: 블로킹과 조용한 실패를 유발했다.
  - 알림 큐를 사용한다.

## 데이터 및 도메인 규칙

- 모든 타임스탬프는 UTC다.
- DB에는 `timestamptz`로 저장한다.
- API 경계에서는 RFC 3339 형식을 사용한다.
- 함수 경계를 넘는 naive datetime은 허용하지 않는다.
- ID는 앱 계층에서 생성한 ULID를 사용한다.
- DB auto-increment ID를 공개 API 식별자로 사용하지 않는다.
- 소프트 삭제는 사용하지 않는다.
- 삭제가 필요한 데이터는 명시적인 유스케이스로 아카이브 테이블에 이동한다.
- 모든 리포지토리 메서드는 `tenant_id` 범위를 받아야 한다.

## API 계약의 단일 진실 원천

- `api/openapi.yaml`의 OpenAPI 3.1 사양이 권위적이다.
- 요청/응답 타입은 OpenAPI 사양에서 생성한다.
- `http/generated/`의 생성 코드는 수동으로 수정하지 않는다.
- 신규 또는 변경된 엔드포인트는 먼저 `api/openapi.yaml`을 수정한다.
- 사양을 재생성한 뒤 구현한다.
- 오류 응답은 RFC 9457 `problem+json`을 따른다.
- 오류 응답은 공유 `problem()` 헬퍼를 사용한다.
- 엔드포인트별 임의 오류 형식을 만들지 않는다.

## 새 코드가 들어갈 위치

- 새 엔드포인트
  - 라우트: `http/routes/`
  - DTO: `http/dto/`
  - 유스케이스: `app/usecases/`
  - 도메인 로직: `domain/`

- 새 외부 통합
  - 인터페이스: `app/ports/`
  - 구현체: `infra/clients/`

- 인증, 로깅, 멱등성 같은 교차 관심사
  - `http/middleware/`
  - 핸들러 내부에 인라인으로 구현하지 않는다.

## 범위 외 / 건드리지 말 것

- `http/generated/`
  - OpenAPI에서 재생성된다.
  - 수동 편집하지 않는다.

- `legacy/billing_v1/`
  - 동결된 레거시 모듈이다.
  - 확장하지 않는다.

- `migrations/`
  - 이미 적용된 마이그레이션은 수정하지 않는다.
  - 새 마이그레이션을 추가한다.

## 의심스러울 때

요청된 변경이 위 규칙을 위반해야 한다면,
조용히 우회하지 않는다.

대신 다음을 수행한다.

1. 어떤 규칙과 충돌하는지 설명한다.
2. 왜 충돌하는지 설명한다.
3. 가장 작은 설계 일관적 대안을 제안한다.

마지막 섹션이 중요합니다. 에이전트는 제약에 부딪히면 우회해서 “작동하는 코드”를 만들려는 경향이 있습니다. “우회하지 말고 플래그를 지정하라”는 지침을 명시하면 DESIGN.md가 단순 문서가 아니라 안전장치가 됩니다.

에이전트가 DESIGN.md를 읽게 만드는 방법

에이전트가 자동으로 DESIGN.md를 특별 취급하는 것은 아닙니다. 일반 파일처럼 읽고 컨텍스트로 사용합니다.

따라서 에이전트가 시작 시 읽는 지침 파일에서 참조해야 합니다.

Claude Code

CLAUDE.md에 다음을 추가합니다.

@DESIGN.md

구조적 변경, API 변경, 도메인 로직 변경 전에는 DESIGN.md를 읽는다.
DESIGN.md와 충돌하는 변경은 우회하지 말고 충돌을 설명한다.

AGENTS.md 기반 에이전트

AGENTS.md에 다음을 추가합니다.

## 아키텍처 규칙

구조적 변경, API 변경, 도메인 로직 변경 전에는 `DESIGN.md`를 읽는다.
`DESIGN.md`의 불변 조건과 충돌하는 변경은 구현하지 말고 플래그를 지정한다.

리뷰에서 사용하는 방법

리뷰어는 긴 설명 대신 DESIGN.md의 규칙을 기준으로 피드백할 수 있습니다.

이 변경은 DESIGN.md의 "HTTP 핸들러는 DB에 직접 접근하지 않는다" 규칙을 위반합니다.
DB 호출을 app/usecases/의 유스케이스로 이동하세요.

에이전트는 이런 피드백을 받으면 보통 빠르게 수정합니다. 자체 에이전트 하네스를 만드는 팀도 이 피드백 루프를 활용합니다. 관련 내용은 자체 Claude Code 구축을 참고하세요.

안티 패턴

DESIGN.md가 오래된 위키 문서처럼 변하면 없는 것보다 나쁩니다. 에이전트와 인간 모두 잘못된 규칙을 믿게 됩니다.

다음 패턴을 피하세요.

1. 코드를 다시 설명하기

나쁜 예:

UserService는 사용자를 처리한다.

이 정보는 에이전트가 코드에서 바로 읽을 수 있습니다.

좋은 예:

사용자 상태 전이는 `User.activate()`, `User.suspend()`를 통해서만 수행한다.
상태를 직접 업데이트하면 감사 이벤트가 누락된다.

2. 튜토리얼로 만들기

DESIGN.md는 “기능 추가 방법” 문서가 아닙니다.

셸 명령, 긴 단계별 가이드, 로컬 개발 팁은 CONTRIBUTING.md나 AGENTS.md에 두세요.

3. 희망을 사실처럼 쓰기

나쁜 예:

모든 쓰기는 유스케이스를 통한다.

실제로 레거시 코드가 이를 우회한다면 에이전트는 허구에 맞춰 코드를 만들 수 있습니다.

좋은 예:

목표: 모든 쓰기는 `app/usecases/`를 통한다.
현재: `legacy/`는 이 규칙을 우회한다.
새 코드는 `legacy/` 패턴을 확장하지 않는다.

4. 소유자와 리뷰 트리거가 없음

DESIGN.md는 PR 프로세스와 연결해야 합니다.

PR 템플릿에 다음 체크리스트를 추가하세요.

- [ ] 이 PR은 계층 경계, API 계약, 데이터 모델, 외부 의존성 중 하나를 변경한다.
- [ ] 그렇다면 `DESIGN.md`를 검토했고 필요한 경우 같은 PR에서 업데이트했다.

5. 코드와 줄 단위로 동기화하려고 하기

DESIGN.md는 함수 시그니처 목록이 아닙니다.

자주 바뀌는 세부사항이 아니라 다음처럼 오래 유지되는 결정을 기록하세요.

  • 계층 경계
  • 트랜잭션 경계
  • 멱등성 전략
  • 데이터 모델 원칙
  • 금지된 패턴
  • API 계약 규칙

6. 다른 지침 파일과 모순되기

AGENTS.md와 DESIGN.md가 같은 규칙을 다르게 말하면 에이전트는 임의로 선택할 수 있습니다.

역할을 분리하세요.

  • 운영 규칙: AGENTS.md, CLAUDE.md
  • 아키텍처 규칙: DESIGN.md

중복 작성하지 말고 참조하세요.

API 및 백엔드 코드베이스에서 특히 중요한 이유

API와 백엔드 서비스는 에이전트가 잘못 추론하기 쉬운 제약이 많습니다.

  • 계약 경계
  • 트랜잭션 의미론
  • 멱등성
  • 데이터 무결성
  • 테넌시 스코핑
  • 에러 모델
  • 버전 호환성
  • 생성 코드와 수동 코드의 경계

이 규칙들은 단일 파일만 읽어서는 명확히 보이지 않습니다. 하지만 잘못 처리하면 프로덕션 장애, 보안 문제, 금전 손실로 이어질 수 있습니다.

1. OpenAPI를 단일 진실 원천으로 지정하기

DESIGN.md에 명확히 적으세요.

- `api/openapi.yaml`이 API 계약의 단일 진실 원천이다.
- 요청/응답 타입은 OpenAPI에서 생성한다.
- 생성된 타입은 수동으로 수정하지 않는다.
- 새 엔드포인트는 먼저 OpenAPI 사양을 변경한 뒤 구현한다.

에이전트는 빌드를 통과시키기 위해 생성 타입을 직접 수정하려는 경우가 있습니다. 이 규칙은 그런 변경을 막습니다.

Apidog에서 먼저 API 계약을 설계하고 OpenAPI 문서를 저장소로 내보내면, DESIGN.md가 그 파일을 권위 있는 계약으로 지정할 수 있습니다. 코드보다 계약을 먼저 설계해야 하는 이유는 AI 에이전트를 위한 API 설계에서 자세히 다룹니다.

2. 트랜잭션 경계 명시하기

나쁜 결과를 막으려면 트랜잭션 규칙을 명시해야 합니다.

- 외부 HTTP 호출은 DB 트랜잭션 내부에서 수행하지 않는다.
- 비즈니스 변경과 외부 호출 의도는 같은 트랜잭션에서 아웃박스에 기록한다.
- 외부 호출은 워커가 아웃박스를 통해 수행한다.

이 규칙이 없으면 에이전트는 핸들러 안에서 외부 API를 직접 호출할 가능성이 높습니다.

3. 멱등성 규칙 명시하기

결제, 주문, 프로비저닝 API에서는 멱등성이 핵심입니다.

- 모든 결제 생성 요청은 `Idempotency-Key` 헤더를 요구한다.
- 같은 키로 재시도하면 같은 결과를 반환해야 한다.
- 게이트웨이 호출은 내부 `idempotency_key`와 연결되어야 한다.

4. 오류 모델 고정하기

- 모든 오류 응답은 RFC 9457 `problem+json`을 따른다.
- 오류 응답은 `problem()` 헬퍼를 통해 생성한다.
- 엔드포인트별 임의 오류 래퍼를 만들지 않는다.

이 규칙이 없으면 엔드포인트마다 다른 오류 형식이 생길 수 있습니다.

5. 인증과 테넌시 스코핑 명시하기

- 모든 리포지토리 쿼리는 `tenant_id` 범위를 포함해야 한다.
- 테넌트 범위가 없는 리포지토리 메서드는 버그다.
- 인증된 사용자 컨텍스트는 `RequestContext`에서만 읽는다.

이런 규칙은 보안 불변 조건입니다. 반드시 문서화해야 합니다.

6. 버전 관리와 호환성 파괴 변경 정의하기

- 응답 필드 삭제 또는 이름 변경은 호환성 파괴 변경이다.
- 필드 추가는 기본적으로 비파괴 변경이다.
- 호환성 파괴 변경은 새 API 버전에서만 허용한다.

에이전트는 필드명을 “더 깔끔하게” 바꿀 수 있습니다. DESIGN.md는 그것이 API 호환성 파괴 변경임을 알려줍니다.

백엔드에서 가장 큰 효과는 에이전트가 스키마를 발명하지 않고 OpenAPI 사양을 따르게 만드는 것입니다. Apidog 다운로드를 통해 설계 우선 API 작업 공간을 만들고, DESIGN.md가 가리킬 OpenAPI 사양을 내보내며, 에이전트가 생성한 엔드포인트가 계약과 일치하는지 테스트할 수 있습니다.

도입 체크리스트

저장소에 DESIGN.md를 추가할 때는 다음 순서로 진행하세요.

- [ ] 저장소 루트에 `DESIGN.md`를 만든다.
- [ ] 시스템 계층과 의존성 방향을 적는다.
- [ ] 절대 깨면 안 되는 불변 조건을 적는다.
- [ ] 주요 설계 결정과 이유를 적는다.
- [ ] 다시 도입하면 안 되는 거부된 대안을 적는다.
- [ ] OpenAPI 사양 위치와 생성 코드 규칙을 적는다.
- [ ] 새 코드가 들어갈 디렉터리를 적는다.
- [ ] 건드리지 말아야 할 영역을 적는다.
- [ ] `AGENTS.md` 또는 `CLAUDE.md`에서 `DESIGN.md`를 참조한다.
- [ ] PR 템플릿에 `DESIGN.md` 검토 체크박스를 추가한다.

결론

DESIGN.md는 에이전트에게 코드의 “형태”가 아니라 “이유”를 알려주는 파일입니다.

핵심은 다음과 같습니다.

  • 에이전트가 코드에서 읽을 수 없는 설계 의도를 기록합니다.
  • AGENTS.md, CLAUDE.md를 대체하지 않고 보완합니다.
  • 불변 조건, 거부된 대안, 트랜잭션 경계, API 계약 규칙을 명시합니다.
  • 규칙은 선언적이고 테스트 가능한 문장으로 작성합니다.
  • 충돌 시 우회하지 말고 플래그를 지정하라고 지시합니다.
  • OpenAPI 사양을 단일 진실 원천으로 지정하면 API 생성 품질이 크게 좋아집니다.
  • PR 템플릿과 소유자를 통해 파일이 부패하지 않게 관리합니다.

먼저 계약을 설계하세요. Apidog를 다운로드해 API를 설계 우선으로 작성하고, DESIGN.md가 가리킬 OpenAPI 사양을 내보낸 뒤, 에이전트가 생성한 엔드포인트가 실제 계약과 일치하는지 테스트하세요.

FAQ

DESIGN.md는 AGENTS.md처럼 공식 표준인가요?

아닙니다. AGENTS.md는 Linux Foundation의 Agentic AI Foundation에서 관리하는 널리 채택된 형식입니다. DESIGN.md는 ARCHITECTURE.md나 ADR처럼 커뮤니티 관례에 가깝습니다. 표준이라기보다 실용적인 패턴으로 보면 됩니다.

AGENTS.md나 CLAUDE.md가 이미 있어도 DESIGN.md가 필요한가요?

아키텍처 제약이 있다면 필요합니다. AGENTS.md와 CLAUDE.md는 짧은 운영 지침에 적합합니다. 깊은 설계 근거와 불변 조건은 별도의 DESIGN.md에 두고 참조하는 편이 좋습니다. 운영 파일 작성은 AGENTS.md 파일 작성 방법을 참고하세요.

DESIGN.md는 ARCHITECTURE.md와 어떻게 다른가요?

목적은 비슷합니다. 다만 DESIGN.md는 코딩 에이전트가 읽기 쉽도록 더 선언적이고, 결정과 불변 조건에 초점을 둡니다. 팀에 이미 ARCHITECTURE.md가 있고 같은 역할을 잘 수행한다면 파일명을 반드시 바꿀 필요는 없습니다.

DESIGN.md는 얼마나 길어야 하나요?

2~4페이지 정도가 실용적입니다. 중요한 것은 길이가 아니라 결정 밀도입니다. 코드에서 바로 알 수 있는 설명은 제거하고, 불변 조건과 근거를 남기세요.

에이전트가 실제로 DESIGN.md를 읽게 하려면 어떻게 해야 하나요?

에이전트가 시작 시 읽는 파일에서 참조하세요.

Claude Code:

@DESIGN.md

AGENTS.md:

구조적 변경 전에는 `DESIGN.md`를 읽는다.

전체 내용을 AGENTS.md나 CLAUDE.md에 붙여넣지 말고 참조만 하세요.

에이전트는 항상 DESIGN.md를 따르나요?

아닙니다. 에이전트 지침은 강제 제약이 아니라 컨텍스트입니다. 그래서 규칙을 명확한 절대문으로 작성하고, 충돌 시 우회하지 말고 플래그를 지정하라고 명시해야 합니다. 리뷰 루프도 필요합니다.

DESIGN.md가 API 계약 문제에 도움이 되나요?

많이 도움이 됩니다. 특히 OpenAPI 사양을 권위 있는 계약으로 지정하면 에이전트가 스키마를 임의로 만들거나 생성 타입을 직접 수정하는 문제를 줄일 수 있습니다. Apidog 같은 도구에서 먼저 계약을 설계하면 DESIGN.md가 가리킬 명확한 대상이 생깁니다.

DESIGN.md는 저장소 어디에 두어야 하나요?

일반적으로 저장소 루트에 둡니다.

README.md
AGENTS.md
CLAUDE.md
DESIGN.md

모노레포에서는 루트 DESIGN.md에 전체 규칙을 두고, 패키지별 DESIGN.md에 로컬 아키텍처 규칙을 둘 수 있습니다.

Stop Storing Passwords: Build Enterprise SSO in Laravel 🛡️

Passwords are a Technical Liability

When building a B2B SaaS platform at Smart Tech Devs, the default instinct is to scaffold standard email and password authentication. However, in modern enterprise software, storing passwords is an immense architectural liability. Users reuse passwords across dozens of sites. If a third-party forum gets breached and a user’s password is leaked, malicious actors will use credential stuffing to log into your SaaS platform. You will be blamed for the breach, even though your database was secure.

To architect enterprise-grade security, you must shift the burden of identity verification to dedicated identity providers (Google, Microsoft Azure AD, Okta). The solution is Single Sign-On (SSO) via OAuth2.

Enter Laravel Socialite

Laravel provides an official package, Socialite, which abstracts the complex OAuth2 handshake (redirects, state verification, token exchange) into a fluent, elegant API. Let’s architect a secure Google SSO flow.

Step 1: The Database Architecture

First, we must update our users table to handle OAuth logins. We make the password column nullable (since SSO users don’t have one) and add a provider_id.


use IlluminateDatabaseMigrationsMigration;
use IlluminateDatabaseSchemaBlueprint;
use IlluminateSupportFacadesSchema;

class UpdateUsersTableForSSO extends Migration
{
    public function up(): void
    {
        Schema::table('users', function (Blueprint $table) {
            $table->string('password')->nullable()->change(); // Nullable for SSO
            $table->string('auth_provider')->nullable(); // e.g., 'google'
            $table->string('provider_id')->nullable(); // Unique ID from Google
            
            // Critical: Ensure users can't create duplicate accounts via different providers
            $table->unique('email'); 
        });
    }
}

Step 2: The OAuth Controller Logic

The controller handles two routes: redirecting the user to Google, and handling the callback when Google sends them back with a verified token.


namespace AppHttpControllersAuth;

use AppHttpControllersController;
use AppModelsUser;
use IlluminateSupportFacadesAuth;
use LaravelSocialiteFacadesSocialite;
use Exception;

class SSOController extends Controller
{
    /**
     * Redirect the user to the Google authentication page.
     */
    public function redirectToGoogle()
    {
        return Socialite::driver('google')->redirect();
    }

    /**
     * Handle the callback from Google.
     */
    public function handleGoogleCallback()
    {
        try {
            // 1. Exchange the temporary code for a secure user profile
            $googleUser = Socialite::driver('google')->user();

            // 2. Find existing user OR create a new one securely
            $user = User::updateOrCreate(
                ['email' => $googleUser->getEmail()], // Match by email
                [
                    'name' => $googleUser->getName(),
                    'auth_provider' => 'google',
                    'provider_id' => $googleUser->getId(),
                    // Password remains null
                ]
            );

            // 3. Log the user in and regenerate the session to prevent fixation attacks
            Auth::login($user);
            request()->session()->regenerate();

            return redirect()->intended('/dashboard');

        } catch (Exception $e) {
            // Log the error and safely redirect back to login
            return redirect('/login')->with('error', 'Authentication failed. Please try again.');
        }
    }
}

The Engineering ROI

By implementing SSO, you instantly inherit the billions of dollars that Google and Microsoft spend on security. If a user enables physical hardware keys or 2FA on their Google account, your SaaS platform automatically receives that exact same level of security for free. You eliminate password reset tickets, protect your platform from credential stuffing, and provide a frictionless onboarding experience for enterprise clients.

Building an Automated KDP Pipeline: How I Engineered a Passive Income Stream with GPT-4 and n8n

What if your weekend automation project could pay for its own infrastructure and generate passive income? Last quarter, my book-generation pipeline cost $127 in OpenAI API calls and generated $4,200 in Kindle Direct Publishing (KDP) royalties—without me writing a single manuscript.

This isn’t about “get rich quick” schemes. It’s about applied automation engineering. Here’s how I architected a serverless publishing pipeline that transforms API calls into royalty streams.

The Architecture

The system follows an ETL pattern adapted for content generation:

  1. Ingestion: Niche research via SerpAPI/Google Trends
  2. Transformation: LLM-based content generation + asset creation
  3. Load: Automated formatting and KDP upload

I orchestrate everything through n8n (open-source workflow automation) running on a $5 DigitalOcean droplet. The pipeline triggers weekly, generating 3-4 book drafts that pass through a human review layer before publication.

The Technical Implementation

Content Generation Layer

The core is a Python microservice that interfaces with OpenAI’s API using structured prompting. Instead of generic prompts, I use JSON schemas to enforce consistent output:

python
import openai
from ebooklib import epub

def generate_chapter(prompt_template, niche_data):
response = openai.chat.completions.create(
model=”gpt-4-turbo”,
messages=[
{“role”: “system”, “content”: “You are a technical writer specializing in concise, actionable content.”},
{“role”: “user”, “content”: prompt_template.format(**niche_data)}
],
response_format={“type”: “json_object”},
temperature=0.7
)

content = json.loads(response.choices[0].message.content)
return content['chapter_text'], content['key_points']

def assemble_book(chapters, metadata):
book = epub.EpubBook()
book.set_identifier(f”auto-{uuid.uuid4()}”)
book.set_title(metadata[‘title’])
book.set_language(‘en’)

for i, chapter in enumerate(chapters):
c = epub.EpubHtml(title=f"Chapter {i+1}", file_name=f"chap_{i+1}.xhtml")
c.content = f"<h1>{chapter['title']}</h1><p>{chapter['body']}</p>"
book.add_item(c)

return book

Asset Generation Pipeline

For cover images, I integrate with the Midjourney API (via their unofficial REST wrapper) and Stable Diffusion as a fallback. The workflow automatically generates prompts based on the book’s metadata:

javascript
// n8n Function Node
const bookTitle = $input.first().json.title;
const genre = $input.first().json.category;

const prompt = Professional book cover, ${genre} style, ${bookTitle}, minimalist, high contrast, 4k;

return {
json: {
prompt: prompt,
aspect_ratio: “2:3”,
output_path: /tmp/covers/${bookTitle.replace(/s/g, '_')}.png
}
};

The Orchestration Layer

n8n handles the state management. The workflow:

  1. Cron trigger (Sundays at 2 AM)
  2. HTTP Request → Google Trends API (via SerpAPI) to identify trending niches
  3. IF node → Filters niches with <100k search volume but >40 CPC (indicates buying intent)
  4. Code node → Executes Python script for content generation
  5. Wait node → 24-hour delay for human review (manual gate)
  6. KDP Upload → Selenium-based automation (since Amazon lacks a public KDP API)

The Economics from a Dev Perspective

Here’s where it gets interesting for engineers:

  • COGS (Cost of Goods Sold): $0.04 per 1K tokens (GPT-4), $0.02 per image (Stable Diffusion API)
  • Unit economics: Average book costs $3.50 to produce (API calls + cover generation), sells at $4.99-$9.99
  • Break-even: 1.2 sales per book
  • Scaling bottleneck: KDP’s daily upload limits, not compute

The real leverage isn’t the content—it’s the automation of metadata optimization. My n8n workflow A/B tests titles and descriptions using Amazon’s Advertising API to optimize for high-intent keywords, something most non-technical publishers do manually.

Practical Takeaways for Builders

  1. API Rate Limiting: KDP throttles uploads aggressively. Implement exponential backoff in your Selenium scripts or use Playwright with stealth plugins.

  2. Content Quality Gates: Don’t automate publication—automate drafting. Use GPT-4 to generate, but add a manual review node in n8n to check for hallucinations, especially in technical niches.

  3. Data Persistence: Store generated manuscripts in S3 with versioning. If Amazon flags content (rare but happens), you can rollback and regenerate with adjusted temperature settings.

  4. Taxonomy Automation: Use spaCy or NLTK to auto-generate Kindle keywords from the generated text, ensuring SEO alignment without manual input.

The Ethics Question

Yes, Amazon’s Terms of Service require disclosure of AI-generated content. My pipeline includes an automated “AI-Assisted” flag in the KDP dashboard and a human-written preface in each book. The automation handles the 80% of mechanical writing; humans handle the 20% of strategic positioning and quality control.

This isn’t about replacing authors—it’s about treating book publishing as what it really is: a content delivery system that can be optimized like any other deployment pipeline.

Next Steps: If you’re building similar automation, I’ve open-sourced my n8n workflow templates and Python formatting scripts here. The repo includes the Selenium KDP uploader and prompt engineering templates I use for technical nonfiction.

What’s your experience with content automation? Drop your stack in the comments—always curious to see how other devs are orchestrating LLMs in production.