<?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: Jarod Stewart</title>
    <description>The latest articles on DEV Community by Jarod Stewart (@stewartjarod).</description>
    <link>https://dev.to/stewartjarod</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F83855%2F4e8ae00c-2a66-48f6-8758-827cbd153460.jpeg</url>
      <title>DEV Community: Jarod Stewart</title>
      <link>https://dev.to/stewartjarod</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/stewartjarod"/>
    <language>en</language>
    <item>
      <title>How I Build Features with Agent Swarms and TDD</title>
      <dc:creator>Jarod Stewart</dc:creator>
      <pubDate>Wed, 11 Mar 2026 18:35:26 +0000</pubDate>
      <link>https://dev.to/stewartjarod/how-i-build-features-with-agent-swarms-and-tdd-9gd</link>
      <guid>https://dev.to/stewartjarod/how-i-build-features-with-agent-swarms-and-tdd-9gd</guid>
      <description>&lt;p&gt;Most teams using AI coding assistants are doing it wrong. They open a chat, paste some context, say "build me X," and hope for the best. The output is inconsistent. The code doesn't follow existing patterns. Tests are an afterthought. Every feature feels like a coin flip.&lt;/p&gt;

&lt;p&gt;I had the same problem. So I built an entire software development lifecycle that runs on agents — not as a novelty, but because I was tired of rework.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bottleneck is context, not capability
&lt;/h2&gt;

&lt;p&gt;When you ask an AI agent to "build feature X," it starts coding immediately. No research. No pattern discovery. No test strategy. It invents new patterns instead of following existing ones. The output looks like code written by someone who joined the team today and skipped onboarding.&lt;/p&gt;

&lt;p&gt;So I built a system that front-loads context before any code is written. It's a single Claude Code skill called &lt;code&gt;/sdlc&lt;/code&gt; that orchestrates six phases:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/sdlc idea                    → 1-idea.md
/sdlc research &amp;lt;feature&amp;gt;      → 2-research.md
/sdlc plan &amp;lt;feature&amp;gt;          → 3-plan.md
/sdlc build &amp;lt;feature&amp;gt;         → 4-build.md
/sdlc review &amp;lt;feature&amp;gt;        → 5-review.md
/sdlc context &amp;lt;feature&amp;gt;       → 6-context.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The highlights
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Research swarms&lt;/strong&gt; — 4 independent agents investigate each feature in parallel: codebase analysis, library docs, market research, and a contrarian devil's advocate. They don't coordinate. Independent perspectives surface blind spots that groupthink misses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pattern baselines&lt;/strong&gt; — Before any code is written, the plan phase finds existing implementations that do the same kind of thing. Every file change in the plan references a specific "Pattern From" file and line number. The agent follows the template instead of inventing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TDD-driven builds&lt;/strong&gt; — Red, green, refactor for every unit. A regression gate (full test suite + lint + typecheck) runs after each unit goes green. No skipping.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Adversarial review&lt;/strong&gt; — Not a friendly once-over. Five dimensions: plan compliance, pattern compliance, security, correctness, and performance. The verdict is SHIP, FIX AND SHIP, or BLOCK.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Baseline enforcement&lt;/strong&gt; — A rule engine in CI that encodes every lesson into automated checks. Banned dependencies, architecture tests, and ratchets that force one-way progress on legacy patterns. The review phase feeds directly into baseline — creating a flywheel where each feature makes the next one harder to ship with bugs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it looks like in practice
&lt;/h2&gt;

&lt;p&gt;For a medium-sized feature, the full lifecycle takes about 45 minutes of my time:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Phase&lt;/th&gt;
&lt;th&gt;My Time&lt;/th&gt;
&lt;th&gt;Agent Time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Idea&lt;/td&gt;
&lt;td&gt;5 min&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Research&lt;/td&gt;
&lt;td&gt;4 min&lt;/td&gt;
&lt;td&gt;5–10 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plan&lt;/td&gt;
&lt;td&gt;10 min&lt;/td&gt;
&lt;td&gt;5 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build&lt;/td&gt;
&lt;td&gt;7 min&lt;/td&gt;
&lt;td&gt;15–30 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Review&lt;/td&gt;
&lt;td&gt;3 min&lt;/td&gt;
&lt;td&gt;5 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Context&lt;/td&gt;
&lt;td&gt;1 min&lt;/td&gt;
&lt;td&gt;2 min&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I'm a reviewer and decision-maker, not a typist. The agents do the reading, the coding, and the testing. I approve the plan and the ship decision.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://stewartjarod.com/articles/agent-sdlc" rel="noopener noreferrer"&gt;Read the full post →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The full article covers the coordinator pattern, how ratchets enforce one-way progress on tech debt, the TDD rules I learned the hard way, and how the review phase's lint rule extraction creates a self-enforcing codebase.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>tdd</category>
      <category>productivity</category>
    </item>
    <item>
      <title>A CI tool that stops AI from undoing your team's architectural decisions</title>
      <dc:creator>Jarod Stewart</dc:creator>
      <pubDate>Wed, 18 Feb 2026 16:10:31 +0000</pubDate>
      <link>https://dev.to/stewartjarod/a-ci-tool-that-stops-ai-from-undoing-your-teams-architectural-decisions-1e32</link>
      <guid>https://dev.to/stewartjarod/a-ci-tool-that-stops-ai-from-undoing-your-teams-architectural-decisions-1e32</guid>
      <description>&lt;p&gt;AI coding tools are fast. They're also confidently wrong about your team's conventions.&lt;/p&gt;

&lt;p&gt;You migrate off moment.js. Cursor adds it back. You build a repository layer. Copilot writes &lt;code&gt;db.select()&lt;/code&gt; directly in page.tsx. You ship a design system with a &lt;code&gt;&amp;lt;Button&amp;gt;&lt;/code&gt; component. Claude writes a custom &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt; with inline Tailwind. You ban axios. An AI assistant adds it to package.json.&lt;/p&gt;

&lt;p&gt;They generate from training data, not from your architecture. And the legacy patterns in your codebase are the ones they see most -- so they reinforce them.&lt;/p&gt;

&lt;p&gt;I kept seeing this across teams and decided to build something to fix it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Baseline
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/stewartjarod/baseline" rel="noopener noreferrer"&gt;Baseline&lt;/a&gt; is a Rust-based CLI that enforces team decisions in CI. You define rules in a single TOML file, and it catches violations before they land.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx code-baseline scan
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It covers a class of rules that ESLint can't express.&lt;/p&gt;

&lt;h2&gt;
  
  
  Banning patterns where they don't belong
&lt;/h2&gt;

&lt;p&gt;AI loves bypassing your abstractions. Scope bans to specific paths:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[[rule]]&lt;/span&gt;
&lt;span class="py"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"no-db-in-pages"&lt;/span&gt;
&lt;span class="py"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"banned-pattern"&lt;/span&gt;
&lt;span class="py"&gt;pattern&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"db."&lt;/span&gt;
&lt;span class="py"&gt;glob&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"app/**/page.tsx"&lt;/span&gt;
&lt;span class="py"&gt;severity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"error"&lt;/span&gt;
&lt;span class="py"&gt;message&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Use the repository layer, not direct DB access in pages"&lt;/span&gt;
&lt;span class="py"&gt;suggest&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Import from @/lib/repositories instead"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Banning imports and dependencies
&lt;/h2&gt;

&lt;p&gt;Deprecated packages are one autocomplete away from returning:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[[rule]]&lt;/span&gt;
&lt;span class="py"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"no-moment"&lt;/span&gt;
&lt;span class="py"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"banned-import"&lt;/span&gt;
&lt;span class="py"&gt;severity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"error"&lt;/span&gt;
&lt;span class="py"&gt;packages&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"moment"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"moment-timezone"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="py"&gt;message&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"moment.js is deprecated — use date-fns or Temporal API"&lt;/span&gt;

&lt;span class="nn"&gt;[[rule]]&lt;/span&gt;
&lt;span class="py"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"no-request"&lt;/span&gt;
&lt;span class="py"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"banned-dependency"&lt;/span&gt;
&lt;span class="py"&gt;severity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"error"&lt;/span&gt;
&lt;span class="py"&gt;packages&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"request"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"request-promise"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"axios"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="py"&gt;message&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Use native fetch or undici"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;banned-dependency&lt;/code&gt; rule parses package.json directly -- it catches packages added as dependencies even if no source file imports them yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ratcheting legacy code to zero
&lt;/h2&gt;

&lt;p&gt;This is the feature I'm most proud of. Say you have 200 calls to &lt;code&gt;legacyFetch()&lt;/code&gt; and want to migrate to &lt;code&gt;apiFetch()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[[rule]]&lt;/span&gt;
&lt;span class="py"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"ratchet-legacy-fetch"&lt;/span&gt;
&lt;span class="py"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"ratchet"&lt;/span&gt;
&lt;span class="py"&gt;severity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"error"&lt;/span&gt;
&lt;span class="py"&gt;pattern&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"legacyFetch("&lt;/span&gt;
&lt;span class="py"&gt;max_count&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;
&lt;span class="py"&gt;glob&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"src/**/*.ts"&lt;/span&gt;
&lt;span class="py"&gt;message&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Migrate to apiFetch"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set the ceiling at 200. Next sprint, migrate some, lower it to 180. The number only goes down. Any PR that adds new legacy calls fails CI.&lt;/p&gt;

&lt;p&gt;ESLint is pass/fail. It can't count occurrences across your codebase and enforce a decreasing ceiling over time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tailwind + shadcn enforcement
&lt;/h2&gt;

&lt;p&gt;If you use Tailwind with shadcn/ui, AI will write &lt;code&gt;bg-white&lt;/code&gt; and &lt;code&gt;text-gray-900&lt;/code&gt; everywhere. Your design system says &lt;code&gt;bg-background&lt;/code&gt; and &lt;code&gt;text-foreground&lt;/code&gt;. Dark mode breaks silently.&lt;/p&gt;

&lt;p&gt;Baseline ships two rules for this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dark mode enforcement&lt;/strong&gt; flags color classes missing a &lt;code&gt;dark:&lt;/code&gt; variant:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;7:21  error  Missing dark: variant for color class: 'bg-white'
  │ &amp;lt;div className="bg-white border border-gray-200 rounded-lg"&amp;gt;
  → Use 'bg-background' instead — it adapts to light/dark automatically
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Semantic token enforcement&lt;/strong&gt; bans raw color classes entirely and suggests your tokens. Ships with 130+ default mappings.&lt;/p&gt;

&lt;p&gt;Both rules understand &lt;code&gt;className&lt;/code&gt;, &lt;code&gt;class&lt;/code&gt;, &lt;code&gt;cn()&lt;/code&gt;, &lt;code&gt;clsx()&lt;/code&gt;, &lt;code&gt;cva()&lt;/code&gt;, and &lt;code&gt;twMerge()&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Proximity rules
&lt;/h2&gt;

&lt;p&gt;Enforce that related patterns appear near each other:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[[rule]]&lt;/span&gt;
&lt;span class="py"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"org-scoped-deletes"&lt;/span&gt;
&lt;span class="py"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"window-pattern"&lt;/span&gt;
&lt;span class="py"&gt;severity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"error"&lt;/span&gt;
&lt;span class="py"&gt;pattern&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"DELETE FROM"&lt;/span&gt;
&lt;span class="py"&gt;condition_pattern&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"organizationId"&lt;/span&gt;
&lt;span class="py"&gt;max_count&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;
&lt;span class="py"&gt;glob&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"src/**/*.ts"&lt;/span&gt;
&lt;span class="py"&gt;message&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"DELETE queries must include organizationId within 80 lines"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Presets
&lt;/h2&gt;

&lt;p&gt;Get started in one line instead of writing rules from scratch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[baseline]&lt;/span&gt;
&lt;span class="py"&gt;extends&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"ai-codegen"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"security"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"nextjs"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Preset&lt;/th&gt;
&lt;th&gt;What it catches&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ai-codegen&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;as any&lt;/code&gt;, empty catch, console.log, TODO, placeholder text, var, require in TS, and more (12 rules)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;security&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Hardcoded secrets, eval, dangerouslySetInnerHTML, .env files, http:// URLs (10 rules)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nextjs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Use next/image, next/link, next/font; no next/head or next/router in App Router (8 rules)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;shadcn-strict&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Dark mode enforcement, theme tokens, no inline styles, no CSS-in-JS (5 rules)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/utils/helpers.ts
  1:0  error  moment.js is deprecated — use date-fns  no-moment
    │ import moment from 'moment';
    → import { format } from 'date-fns'

src/components/BadCard.tsx
  7:21  error  Missing dark: variant for 'bg-white'  enforce-dark-mode
    │ &amp;lt;div className="bg-white border border-gray-200 rounded-lg"&amp;gt;
    → Use 'bg-background' instead

✗ 9 violations (7 error, 2 warning)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  CI integration
&lt;/h2&gt;

&lt;p&gt;Ships a GitHub Action that annotates violations inline on PR diffs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;stewartjarod/baseline@main&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;src'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On PRs it automatically scans only changed files. Outputs GitHub annotations, SARIF for Code Scanning, and markdown summaries.&lt;/p&gt;

&lt;h2&gt;
  
  
  The technical bits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Written in Rust. Single binary, no Node runtime.&lt;/li&gt;
&lt;li&gt;Tree-sitter for AST-aware rules (component size, nested components, useState/useEffect analysis)&lt;/li&gt;
&lt;li&gt;Parallel scanning via rayon&lt;/li&gt;
&lt;li&gt;Respects .gitignore automatically&lt;/li&gt;
&lt;li&gt;Also runs as an MCP server so AI tools can query your rules directly&lt;/li&gt;
&lt;li&gt;Available on crates.io and npm&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Run instantly&lt;/span&gt;
npx code-baseline scan

&lt;span class="c"&gt;# Or install&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; code-baseline
cargo &lt;span class="nb"&gt;install &lt;/span&gt;code-baseline
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Linters catch syntax. Formatters fix whitespace. &lt;strong&gt;Baseline enforces the decisions your team has already made&lt;/strong&gt; -- especially the ones AI keeps ignoring.&lt;/p&gt;

&lt;p&gt;MIT licensed. Feedback and contributions welcome.&lt;/p&gt;

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

</description>
      <category>opensource</category>
      <category>rust</category>
      <category>react</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Is this vibe coding?? I built an AI email template editor in 2 days</title>
      <dc:creator>Jarod Stewart</dc:creator>
      <pubDate>Thu, 11 Dec 2025 18:26:46 +0000</pubDate>
      <link>https://dev.to/stewartjarod/is-this-vibe-coding-i-built-an-ai-email-template-editor-in-2-days-2a4h</link>
      <guid>https://dev.to/stewartjarod/is-this-vibe-coding-i-built-an-ai-email-template-editor-in-2-days-2a4h</guid>
      <description>&lt;h2&gt;
  
  
  What I built
&lt;/h2&gt;

&lt;p&gt;An email template editor where you describe what you want and the AI builds it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Generate:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Create a welcome email for new users"&lt;/li&gt;
&lt;li&gt;Full template in 5 seconds — header, personalized greeting, CTA, footer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Iterate:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Make the button green"&lt;/li&gt;
&lt;li&gt;"Add a section about our mobile app"&lt;/li&gt;
&lt;li&gt;"Show a discount code only for new users"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Then edit visually or in code.&lt;/strong&gt; Chat → Visual → Code. Same template.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why this works (and most AI editors don't)
&lt;/h2&gt;

&lt;p&gt;Most AI-to-email tools generate raw HTML. You get a blob of code, maybe it works, probably it breaks in Outlook, and you can't easily edit it.&lt;/p&gt;

&lt;p&gt;This editor does something different: &lt;strong&gt;the AI generates editor structure, not HTML&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Specifically, it outputs TipTap JSON that maps to React Email components:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"doc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"emailSection"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"emailHeading"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"attrs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"level"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Welcome, "&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"variable"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"attrs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"firstName"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"fallback"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"there"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"!"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This structure is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Editable&lt;/strong&gt; — click any element, change it visually&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validated&lt;/strong&gt; — only known node types allowed, hallucinations get caught&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Portable&lt;/strong&gt; — serialize to React Email, HTML, or plain text&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  The AI architecture
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Constrained generation
&lt;/h3&gt;

&lt;p&gt;The system prompt tells Claude exactly which node types exist:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;VALID_NODES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;emailSection&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;emailHeading&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;emailText&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;emailButton&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;emailImage&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;emailDivider&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;emailSpacer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;emailRow&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;emailColumn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;variable&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;conditional&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each node has a spec: what props it accepts, what's required, examples of valid output.&lt;/p&gt;

&lt;p&gt;Claude can only use these nodes. If it tries to invent &lt;code&gt;emailCarousel&lt;/code&gt; or &lt;code&gt;fancyWidget&lt;/code&gt;, the validator rejects it before it hits the editor.&lt;/p&gt;

&lt;h3&gt;
  
  
  Iterative editing
&lt;/h3&gt;

&lt;p&gt;When you say "make the button green", the AI receives:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Your message&lt;/li&gt;
&lt;li&gt;The current template JSON&lt;/li&gt;
&lt;li&gt;Instruction to make targeted edits, not regenerate everything&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So it finds the button node, updates &lt;code&gt;style.backgroundColor&lt;/code&gt;, returns the modified JSON. Fast and predictable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Brand kit integration
&lt;/h3&gt;

&lt;p&gt;The system prompt includes the user's brand kit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Primary Color: #5046e5
Font Family: Inter, sans-serif
Button Style: rounded (4px radius)
Company Name: Acme Inc
Logo URL: https://...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Generated templates automatically use these defaults. Less manual fixing.&lt;/p&gt;




&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Editor:         TipTap (ProseMirror-based)
Email output:   React Email
AI:             Vercel AI SDK + Claude Sonnet
State:          Zustand
Framework:      Next.js 16
Database:       PostgreSQL + Drizzle
UI:             shadcn/ui + Tailwind
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why TipTap?
&lt;/h3&gt;

&lt;p&gt;TipTap lets you define custom nodes for your domain. Each React Email component (&lt;code&gt;&amp;lt;Button&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;Section&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;Heading&amp;gt;&lt;/code&gt;) gets a corresponding TipTap node with its own schema and rendering.&lt;/p&gt;

&lt;p&gt;The editor doesn't know it's building emails — it just knows it has nodes with certain attributes. The serializer handles the React Email conversion at export time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why React Email?
&lt;/h3&gt;

&lt;p&gt;It outputs HTML that actually works in email clients. Outlook, Gmail, Apple Mail — the hard stuff is handled. And the component model maps cleanly to editor nodes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Is this vibe coding?
&lt;/h2&gt;

&lt;p&gt;Here's what I had before writing any code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Database schema (templates, versions, blocks, brand kits, AI conversations)&lt;/li&gt;
&lt;li&gt;State management design&lt;/li&gt;
&lt;li&gt;API routes spec
&lt;/li&gt;
&lt;li&gt;Component architecture&lt;/li&gt;
&lt;li&gt;Full list of TipTap extensions needed&lt;/li&gt;
&lt;li&gt;AI system prompt and validation strategy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The AI didn't write any of that. I did — based on almost two years at SendGrid seeing what breaks in email infrastructure.&lt;/p&gt;

&lt;p&gt;AI helped me implement fast. Describe a component, get 80% of the code, fix edge cases, move on. But the architecture? The constraints that make the AI output actually usable? That's experience.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The unlock: build at the speed of your ideas.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you have the ideas, AI is a multiplier. If you don't, you get fast spaghetti.&lt;/p&gt;




&lt;h2&gt;
  
  
  What this is part of
&lt;/h2&gt;

&lt;p&gt;This editor is one piece of &lt;a href="https://wraps.dev" rel="noopener noreferrer"&gt;Wraps&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The problem:&lt;/strong&gt; AWS SES is cheap (~$1/10k emails) but painful. 90% of developers abandon during domain verification.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The alternative:&lt;/strong&gt; SaaS tools (Resend, Postmark) have great DX but cost 10-100x more and lock you in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wraps:&lt;/strong&gt; SaaS-quality developer experience, but deploys to YOUR AWS account. You own it. You pay AWS directly. If you stop paying us, your email keeps working.&lt;/p&gt;

&lt;p&gt;The template editor lets you build emails with AI, edit visually, and send tests through your own SES.&lt;/p&gt;

&lt;p&gt;More coming: SMS, background jobs, team dashboard, real-time collaboration.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://wraps.dev" rel="noopener noreferrer"&gt;wraps.dev&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Questions?
&lt;/h2&gt;

&lt;p&gt;Happy to go deeper on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The TipTap → React Email serialization&lt;/li&gt;
&lt;li&gt;How the AI validation works&lt;/li&gt;
&lt;li&gt;The conditional block implementation&lt;/li&gt;
&lt;li&gt;Anything else&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Roast the architecture in the comments.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>typescript</category>
      <category>aws</category>
    </item>
    <item>
      <title>GraphQL for Consumer IoT Applications</title>
      <dc:creator>Jarod Stewart</dc:creator>
      <pubDate>Fri, 06 Sep 2019 01:12:12 +0000</pubDate>
      <link>https://dev.to/stewartjarod/graphql-for-consumer-iot-applications-531a</link>
      <guid>https://dev.to/stewartjarod/graphql-for-consumer-iot-applications-531a</guid>
      <description>&lt;p&gt;At Yonomi, we have to satisfy a wide array of requirements from our customers. We are savvy in event-based architecture, pub/sub protocols, and we work within the limitations of hardware: memory, battery, and internet availability. We also have to iterate quickly and expand functionality across a myriad of devices.&lt;/p&gt;

&lt;p&gt;In the last year, we have learned a lot about GraphQL and have discovered some great features that may benefit the IoT industry and specifically, our customers in consumer IoT.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enter GraphQL
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Powerful Schema
&lt;/h3&gt;

&lt;p&gt;GraphQL has a powerful built-in schema and a strong type system that serves as basic documentation for the API. The schema is facilitated by the GraphQL Schema Definition Language (SDL), which allows us to define a device’s type. You can learn more about SDL here. Once defined, the client and the server have a contract on parameters and values. &lt;/p&gt;

&lt;p&gt;For example, a Light device type may need to define the device’s name that the end-user calls their light, whether the light is on or off, and what brightness the bulb is currently set to.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="no"&gt;Light&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="ss"&gt;id: &lt;/span&gt;&lt;span class="no"&gt;ID&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;
  &lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;
  &lt;span class="ss"&gt;power: &lt;/span&gt;&lt;span class="no"&gt;Boolean&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;
  &lt;span class="ss"&gt;brightness: &lt;/span&gt;&lt;span class="no"&gt;Int&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With custom schema directives, we can take this a step further and define which attributes are stateful for a device type. In such cases, we can make a mutation request to set the state of that attribute on the device. When the device accepts and acts on that request it can respond with the new state of that attribute.&lt;/p&gt;

&lt;p&gt;The custom schema directives let the client and server know which attributes are statefully managed by the device. Users can request to set the state of those attributes but, in the end, the device manages the actual state that it is in.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="no"&gt;Light&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="ss"&gt;id: &lt;/span&gt;&lt;span class="no"&gt;ID&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;
  &lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;
  &lt;span class="ss"&gt;power: &lt;/span&gt;&lt;span class="no"&gt;Boolean&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="vi"&gt;@state&lt;/span&gt;
  &lt;span class="ss"&gt;brightness: &lt;/span&gt;&lt;span class="no"&gt;Int&lt;/span&gt; &lt;span class="vi"&gt;@state&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Self-documenting APIs makes GraphQL ideal for consumer IoT where you need to control device’s attributes and states. The schema functions as a contract that both the client and server have to fulfill while exposing how the device functions. In the example of the Light type above, the Light’s name has to be a string, and the power and brightness attributes are stateful where only the device can report its state to the cloud. A user can tell the device to set a state but ultimately, the device is the source of truth on whether the request is accepted or not. &lt;/p&gt;

&lt;h3&gt;
  
  
  Data Relations
&lt;/h3&gt;

&lt;p&gt;We initially considered GraphQL when our customers wanted more control over the data they requested. They also wanted access to related data with as few requests as possible for mobile applications. GraphQL is a natural choice for this because of its ability to refer to related data through the type definitions that define the schema of the object/model. &lt;/p&gt;

&lt;p&gt;A great use case for this would be associating devices to locations. In consumer IoT, we associate devices to rooms within a house for products that are wanted in many rooms, like light bulbs. Our mobile app, in this case, would list devices that are in rooms all associated with a single house location. When a user wants to make sure the lights are off while on vacation, they can check the status of all the lights throughout the home and only make one API request.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Query&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="no"&gt;Location&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;id: &lt;/span&gt;&lt;span class="s2"&gt;"my_house"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;devices&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nb"&gt;id&lt;/span&gt;
      &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="n"&gt;on&lt;/span&gt; &lt;span class="no"&gt;Light&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;power&lt;/span&gt;
        &lt;span class="n"&gt;brightness&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This problem is commonly referred to as the over/under-fetching problem. In a RESTful architecture, you would normally make at least two requests to be able to obtain all of this data (under-fetching) and would have obtained more information than what you really needed (over-fetching). Over-fetching data could be extremely detrimental to a device that cannot handle unknown attributes or is limited in memory. By over-fetching data, a device is susceptible to buffer overflow, which overruns the buffer's boundary and overwrites adjacent memory locations. Letting devices control which data they want to work with and not having to worry about receiving more data than what was requested alleviates this problem entirely.&lt;/p&gt;

&lt;p&gt;Mobile developers have also become innately aware of data consumption and strive to use as little as possible when they can. GraphQL enables minimizing data use through data relations that are defined in the schema and allowing the clients to be selective in their queries.&lt;/p&gt;

&lt;h3&gt;
  
  
  Subscriptions
&lt;/h3&gt;

&lt;p&gt;As a cherry on top, Apollo server supports subscriptions out of the box.  Subscriptions are GraphQL operations that watch events emitted from Apollo Server. The native Apollo Server supports GraphQL subscriptions without additional configuration.&lt;/p&gt;

&lt;p&gt;GraphQL clients can get near real-time information on device’s states as they change. No need to poll for the device’s current state, just subscribe and listen for those changes as they happen. This works really well for app-clients that want to show device status changes as soon as they are available.&lt;/p&gt;

&lt;h2&gt;
  
  
  GraphQL Gotchas
&lt;/h2&gt;

&lt;p&gt;Although these features of GraphQL are nice, there are some caveats when selecting relatively new software for production environments. &lt;/p&gt;

&lt;h3&gt;
  
  
  Tooling
&lt;/h3&gt;

&lt;p&gt;Apollo and Prisma have been pumping out an impressive amount of tooling for GraphQL and things are only getting better with community support. Yet, there are a few things that are still hard to do, and everyone is doing things differently. Namely, acceptance testing.&lt;/p&gt;

&lt;p&gt;At Yonomi, we are working on our own acceptance test tooling and one day we hope to share these with the community.&lt;/p&gt;

&lt;h3&gt;
  
  
  200 OK
&lt;/h3&gt;

&lt;p&gt;In GraphQL everything is 200 OK. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2F146wewlafjhfuj2qvft2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2F146wewlafjhfuj2qvft2.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Did you make a bad Query or Mutation? 200 OK.&lt;/p&gt;

&lt;p&gt;Internal Server Error? 200 OK.&lt;/p&gt;

&lt;p&gt;Not permitted to access that data? 200 OK.&lt;/p&gt;

&lt;p&gt;As long as you hit the GraphQL server you will receive a 200 OK. As cloud experts, we have learned to rely on HTTP statuses to provide helpful and standard information. GraphQL has decidedly been remiss on some of the golden standards of HTTP.&lt;/p&gt;

&lt;h3&gt;
  
  
  Nullability
&lt;/h3&gt;

&lt;p&gt;In GraphQL, every field is nullable by default. There are many ways for things to go wrong when interacting with n-number of databases, services, etc. to perform a single GraphQL request. So, if any of them fail, GraphQL will simply return null for that attribute or parent attribute. Although this is a design choice, it is something worth noting when working with GraphQL. If your clients rely on null values to mean more than the value simply wasn’t returned by your API, you might run into problems. The wonderful people at Apollo offer sound advice on this &lt;a href="https://blog.apollographql.com/using-nullability-in-graphql-2254f84c4ed7?gi=c0b4fd9f132e" rel="noopener noreferrer"&gt;here&lt;/a&gt;. Be consistent and thoughtful and use non-nullables as diligently as possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Many benefits of GraphQL for Consumer IoT
&lt;/h2&gt;

&lt;p&gt;GraphQL has the ability to self-document with its schema, can simplify API interactions to only one request, allows requests to specific fields, and it provides simple to use subscriptions to access real-time information. Although it has some weakness when considering available tooling and thinks everything is 200 “OK”, GraphQL provides a lot of power for use-cases specifically required by the internet of things. At Yonomi, we are continuing to learn and explore more on how GraphQL can better suit the needs of our customers now and in the future.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published @Yonomi &lt;a href="https://www.yonomi.co/blog/2018/12/7/graphql-for-consumer-iot-applications" rel="noopener noreferrer"&gt;GraphQL for Consumer IoT Applications&lt;br&gt;
&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>graphql</category>
      <category>iot</category>
    </item>
  </channel>
</rss>
