<?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>Forem: Spencer Pauly</title>
    <description>The latest articles on Forem by Spencer Pauly (@spencerpauly).</description>
    <link>https://forem.com/spencerpauly</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%2F444771%2Fa1d992d5-857e-4bc6-8ce1-d2981b1cd3f9.jpeg</url>
      <title>Forem: Spencer Pauly</title>
      <link>https://forem.com/spencerpauly</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/spencerpauly"/>
    <language>en</language>
    <item>
      <title>Why is Anthropic's archived Postgres MCP server still getting 312k installs a month?</title>
      <dc:creator>Spencer Pauly</dc:creator>
      <pubDate>Mon, 04 May 2026 19:45:13 +0000</pubDate>
      <link>https://forem.com/spencerpauly/why-is-anthropics-archived-postgres-mcp-server-still-getting-312k-installs-a-month-3oeh</link>
      <guid>https://forem.com/spencerpauly/why-is-anthropics-archived-postgres-mcp-server-still-getting-312k-installs-a-month-3oeh</guid>
      <description>&lt;p&gt;Last month, &lt;a href="https://www.npmjs.com/package/@modelcontextprotocol/server-postgres" rel="noopener noreferrer"&gt;&lt;code&gt;@modelcontextprotocol/server-postgres&lt;/code&gt;&lt;/a&gt; was downloaded 312,391 times. The month before that, 301,440. November 2025 it was 100,059. Installs have tripled in five months and they're still climbing.&lt;/p&gt;

&lt;p&gt;The package is archived.&lt;/p&gt;

&lt;p&gt;Anthropic moved it into a repo called &lt;a href="https://github.com/modelcontextprotocol/servers-archived" rel="noopener noreferrer"&gt;&lt;code&gt;servers-archived&lt;/code&gt;&lt;/a&gt; and told you, in big letters, not to use it in production. The npm command still works. The dev community keeps reaching for it.&lt;/p&gt;

&lt;p&gt;So who's installing it, and why?&lt;/p&gt;

&lt;h2&gt;
  
  
  What the server is
&lt;/h2&gt;

&lt;p&gt;If you've never used it: &lt;code&gt;@modelcontextprotocol/server-postgres&lt;/code&gt; is a small Model Context Protocol server that lets a model (Claude, Cursor, Windsurf, anything that speaks MCP) read your Postgres database. You give it a connection string. It exposes one tool called &lt;code&gt;query&lt;/code&gt; that runs read-only SQL, plus a resource for each table's schema so the model knows what columns exist.&lt;/p&gt;

&lt;p&gt;It's the official one. Anthropic published it. It's the first hit on Google for "postgres mcp server" and the first answer ChatGPT will give you if you ask. Wire it into your &lt;code&gt;claude_desktop_config.json&lt;/code&gt; and you can ask Claude "what were our top 10 customers by revenue last month" and get an actual answer pulled from your actual database.&lt;/p&gt;

&lt;p&gt;The use case is obvious. Devs have spent the last year hooking agents up to everything else they own: Slack, GitHub, Linear, Datadog. The database is the most useful thing left to connect, and this is the ready-made way to do it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it got archived
&lt;/h2&gt;

&lt;p&gt;In early 2025, Anthropic split their reference server repo. The actively maintained &lt;a href="https://github.com/modelcontextprotocol/servers" rel="noopener noreferrer"&gt;&lt;code&gt;servers&lt;/code&gt;&lt;/a&gt; repo now contains seven servers: &lt;code&gt;everything&lt;/code&gt;, &lt;code&gt;fetch&lt;/code&gt;, &lt;code&gt;filesystem&lt;/code&gt;, &lt;code&gt;git&lt;/code&gt;, &lt;code&gt;memory&lt;/code&gt;, &lt;code&gt;sequentialthinking&lt;/code&gt;, &lt;code&gt;time&lt;/code&gt;. Every one of them is either a protocol demo or a local-only utility.&lt;/p&gt;

&lt;p&gt;The other 14 servers — Postgres, GitHub, GitLab, Slack, Sentry, Google Drive, Redis, Puppeteer, and the rest — got moved into &lt;code&gt;servers-archived&lt;/code&gt; with a single notice at the top:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NO SECURITY GUARANTEES ARE PROVIDED FOR THESE ARCHIVED SERVERS.&lt;/strong&gt;&lt;br&gt;
These servers are no longer maintained. No security updates or bug fixes will be provided. Use at your own risk.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There's no specific "we archived Postgres because X" statement. But the pattern is clear when you compare the two lists. Every server that survived is something with no real security surface — it talks to your local filesystem or just demos a protocol feature. Every server that got archived is something that talks to a third-party system with credentials.&lt;/p&gt;

&lt;p&gt;The honest read: if a reference server has a real security surface, the team would rather not own it. That's a defensible call. The awkward part is that Anthropic told users to "refer to the actively maintained servers repository" for production use, and the actively maintained repo has nothing that replaces the archived ones. The official recommendation is "use the maintained version." There is no maintained version. The implicit answer is "find something in the community."&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it's still getting installed
&lt;/h2&gt;

&lt;p&gt;Three reasons, all reasonable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Connecting an agent to your database is critical.&lt;/strong&gt; Devs are wiring AI agents to their databases right now. Every team has the same workflow problem: the model is in a chat thread, you need data to debug something, you tab over to your DB client, copy a row, paste it back. After the third time you wire up anything to skip that step, and the path of least resistance is the official-looking package on npm.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The disclaimer is in the wrong place.&lt;/strong&gt; When you &lt;code&gt;npx @modelcontextprotocol/server-postgres&lt;/code&gt;, the command works. The npm page doesn't say "archived." It says "PostgreSQL." You have to navigate to the GitHub repo to find the warning, and the repo is &lt;code&gt;modelcontextprotocol/servers-archived&lt;/code&gt; — a different name and a different URL than the one most tutorials linked to a year ago. If you came in via a YouTube walkthrough or an LLM's suggestion, you may never visit the source at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It works.&lt;/strong&gt; For demos, prototypes, "let me see if this is even useful" experiments, the server does what it says. Read-only queries against a small dev database are fine. The trouble starts when "fine for a demo" turns into "this is now in front of a production database," which is exactly the bridge a lot of teams quietly cross.&lt;/p&gt;

&lt;p&gt;So 312k installs a month isn't 312k people making a security mistake. It's a mix of demos, CI runs, fresh installs in new projects, and a slowly growing group of teams running it against something real because they didn't know there was a better option.&lt;/p&gt;

&lt;h2&gt;
  
  
  A look under the hood
&lt;/h2&gt;

&lt;p&gt;The whole thing is &lt;a href="https://github.com/modelcontextprotocol/servers-archived/blob/main/src/postgres/index.ts" rel="noopener noreferrer"&gt;130 lines of TypeScript&lt;/a&gt;. The &lt;code&gt;query&lt;/code&gt; handler looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;BEGIN TRANSACTION READ ONLY&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ROLLBACK&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the security model. A &lt;code&gt;READ ONLY&lt;/code&gt; Postgres transaction stops writes, so the agent can't &lt;code&gt;DROP TABLE&lt;/code&gt;. It doesn't stop anything else. There's no statement timeout, so a query like &lt;code&gt;SELECT pg_sleep(3600)&lt;/code&gt; ties up a connection for an hour. No row cap either, so &lt;code&gt;SELECT * FROM events&lt;/code&gt; returns the whole table as a JSON blob and tries to fit it into the model's context window. And no allow-list of tables or block-list of columns, so if your &lt;code&gt;users&lt;/code&gt; table has an &lt;code&gt;api_key&lt;/code&gt; or a &lt;code&gt;stripe_secret&lt;/code&gt;, the model can read it and pass it on to whatever third-party model provider it's running against.&lt;/p&gt;

&lt;p&gt;Schema introspection is two columns: &lt;code&gt;column_name&lt;/code&gt; and &lt;code&gt;data_type&lt;/code&gt;. No primary keys, no foreign keys, no indexes, no enums, no &lt;code&gt;COMMENT ON COLUMN&lt;/code&gt; text. The &lt;code&gt;information_schema&lt;/code&gt; query that fetches columns also forgets to filter on &lt;code&gt;table_schema&lt;/code&gt;, so if you have a table called &lt;code&gt;users&lt;/code&gt; in two schemas, the model gets the columns from both merged together. The discovery query hard-codes &lt;code&gt;WHERE table_schema = 'public'&lt;/code&gt;, so any table outside &lt;code&gt;public&lt;/code&gt; is invisible.&lt;/p&gt;

&lt;p&gt;Errors are thrown out of the handler instead of returned as tool results, so the model sees an opaque JSON-RPC error rather than the Postgres message it could use to fix its own query. The &lt;code&gt;inputSchema&lt;/code&gt; doesn't mark &lt;code&gt;sql&lt;/code&gt; as required, so the agent can call &lt;code&gt;query({})&lt;/code&gt; and crash the server. The connection string is passed as a command-line argument, so anyone who can run &lt;code&gt;ps&lt;/code&gt; on the host can read the password.&lt;/p&gt;

&lt;p&gt;None of this is interesting on its own. Together it tells you what the file is: a demonstration of how to build an MCP server. Not a production database gateway. Anthropic was right to treat it that way.&lt;/p&gt;

&lt;h2&gt;
  
  
  The alternative: QueryBear
&lt;/h2&gt;

&lt;p&gt;I'm not a neutral observer here, because I spent the last year building &lt;a href="https://querybear.com" rel="noopener noreferrer"&gt;QueryBear&lt;/a&gt; for this exact gap. It's the read-only SQL gateway I wanted to exist when I first hooked an agent to my own database, and it ships with the things the official server doesn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Real read-only enforcement.&lt;/strong&gt; Every query is parsed before it runs. Anything that isn't a &lt;code&gt;SELECT&lt;/code&gt; (or a &lt;code&gt;WITH&lt;/code&gt; resolving to one) is rejected, multi-statement input included. The transaction wrapper is still there as a second line of defense; the parser is the primary one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Table allow-lists and column block-lists.&lt;/strong&gt; You decide which tables an agent can read and which columns are off-limits. &lt;code&gt;users.password_hash&lt;/code&gt; and &lt;code&gt;customers.stripe_secret&lt;/code&gt; stay invisible unless you opt them in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Statement timeouts and row caps.&lt;/strong&gt; Configurable per connection. A bad query fails fast and returns a bounded result, so no more accidental full-table scans landing in the model's context window.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Schema introspection that includes the real schema.&lt;/strong&gt; Foreign keys, indexes, enums, column comments, multi-schema. The model writes better SQL when it has more to work with, and your &lt;code&gt;COMMENT ON COLUMN&lt;/code&gt; documentation is finally useful to something.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Errors as tool results.&lt;/strong&gt; When a query fails, the error message comes back to the model in the tool response. The agent can fix its query and try again. The loop actually closes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Audit log.&lt;/strong&gt; Every query the agent ran, with timestamp, agent identity, and connection. If something goes sideways you can answer "what ran?" instead of guessing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Encrypted credentials at rest.&lt;/strong&gt; Connection strings are encrypted in storage, not sitting in &lt;code&gt;process.argv&lt;/code&gt; and not visible to &lt;code&gt;ps&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Drop-in MCP server.&lt;/strong&gt; QueryBear runs as an MCP server, so it slots into the same &lt;code&gt;claude_desktop_config.json&lt;/code&gt; spot the official one occupies. Same shape, different security model.&lt;/p&gt;

&lt;p&gt;That's the trade. The official server is a 130-line demo. QueryBear is a service built to do this job for real.&lt;/p&gt;

&lt;h2&gt;
  
  
  Back to the question
&lt;/h2&gt;

&lt;p&gt;So why does an archived MCP server keep growing? Because the need is real, the disclaimer is invisible, and the official-looking thing on npm works well enough to feel fine. None of that is anyone's fault. It's just the default when a reference implementation gets adopted as production infrastructure.&lt;/p&gt;

&lt;p&gt;If you're using &lt;code&gt;@modelcontextprotocol/server-postgres&lt;/code&gt; against your dev database for tinkering, carry on. If you're using it against anything that holds real customer data, you should know what you're using — and that you don't have to.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Introducing Skills Over MCP – the better way to share and distribute skills</title>
      <dc:creator>Spencer Pauly</dc:creator>
      <pubDate>Mon, 04 May 2026 16:36:07 +0000</pubDate>
      <link>https://forem.com/spencerpauly/introducing-skills-over-mcp-the-better-way-to-share-and-distribute-skills-bb</link>
      <guid>https://forem.com/spencerpauly/introducing-skills-over-mcp-the-better-way-to-share-and-distribute-skills-bb</guid>
      <description>&lt;h2&gt;
  
  
  A new Skills charter is rolling out. Sharing them is still a mess.
&lt;/h2&gt;

&lt;p&gt;Anthropic's new Skills charter is the best packaging format Claude has had so&lt;br&gt;
far. A folder, a &lt;code&gt;SKILL.md&lt;/code&gt;, maybe some supporting files, a client that knows&lt;br&gt;
how to load them. Clean.&lt;/p&gt;

&lt;p&gt;The thing nobody's solved yet: how do you actually share a skill with someone?&lt;/p&gt;

&lt;p&gt;Today the answer is, you don't. You copy folders around, paste &lt;code&gt;.claude/skills/&lt;/code&gt;&lt;br&gt;
between projects, fork someone's repo and forget which version you forked. I've&lt;br&gt;
been doing the copy-paste dance for months. It's dumb.&lt;/p&gt;

&lt;p&gt;So I built the distribution layer I wanted.&lt;/p&gt;

&lt;h2&gt;
  
  
  Skills Over MCP
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://skillsovermcp.com" rel="noopener noreferrer"&gt;Skills Over MCP&lt;/a&gt; takes a public GitHub repo full of&lt;br&gt;
&lt;code&gt;SKILL.md&lt;/code&gt; files and serves it as a remote MCP server. You get a personal URL.&lt;br&gt;
Anyone you hand it to can mount your whole pack with one line of config.&lt;/p&gt;

&lt;p&gt;No hosting, no build step. The repo is the source of truth. Push to GitHub, the&lt;br&gt;
server updates.&lt;/p&gt;

&lt;p&gt;It works in Claude Code, Cursor, and Claude Desktop today, anything that speaks&lt;br&gt;
MCP. You don't have to wait for the charter to land in your client. When it&lt;br&gt;
does, the &lt;code&gt;SKILL.md&lt;/code&gt; files you already shipped keep working.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup, about 30 seconds
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Public GitHub repo. One folder per skill, each with a &lt;code&gt;SKILL.md&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Connect it at skillsovermc&lt;/li&gt;
&lt;li&gt;Drop it into your client config:
&lt;/li&gt;
&lt;/ol&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;"mcpServers"&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;"my-skills"&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;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://skillsovermcp.com/your-handle/skills"&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;Skills show up as skill_&amp;lt;nameserver picks it up.&lt;/p&gt;

&lt;p&gt;What's already up there&lt;/p&gt;

&lt;p&gt;I seeded the catalog with my&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;50+ marketing skills (SEO a, email sequences, customerresearch)&lt;/li&gt;
&lt;li&gt;A few dev utilities I use d_grill_me (interview your ownplan until it falls over), skill_triage_bug, skill_write_like_a_human&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fork any of them, edit, point your MCP URL at the fork. Done.&lt;/p&gt;

&lt;p&gt;Why publish a pack this week&lt;/p&gt;

&lt;p&gt;The packs that go up early get copied. That's how awesome-* repos worked in&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;If you've got a workflowant, the curve is
cheapest right now. Less competition, more discovery. Wait six months and it's
the opposite.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Site: skillsovermcp.com&lt;br&gt;
Example repo: github.com/spencerpauly/skills-repo&lt;/p&gt;

&lt;p&gt;If you ship one, drop the link in the comments. I'm keeping a list of the good&lt;br&gt;
ones.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Giving AI Agents Database Access Is Way Harder Than It Looks</title>
      <dc:creator>Spencer Pauly</dc:creator>
      <pubDate>Fri, 24 Apr 2026 22:51:42 +0000</pubDate>
      <link>https://forem.com/spencerpauly/giving-ai-agents-database-access-is-way-harder-than-it-looks-5akg</link>
      <guid>https://forem.com/spencerpauly/giving-ai-agents-database-access-is-way-harder-than-it-looks-5akg</guid>
      <description>&lt;p&gt;Imagine the world 12 months from now.&lt;/p&gt;

&lt;p&gt;AI agents are everywhere and they are genuinely powerful. They write code, ship features, debug production issues, and quietly do half of the boring work nobody wanted to do anyway.&lt;/p&gt;

&lt;p&gt;But there's a catch. Agents are only as smart as the world you give them. They can't reason about a system they can't see, and they can't act on data they don't have access to.&lt;/p&gt;

&lt;p&gt;The model is the brain. You're the one who has to build the body and the room it lives in.&lt;/p&gt;

&lt;p&gt;Which means the highest-leverage thing you can do right now is not picking the smartest model. It's building a sandbox where your agents can roam free and never break anything.&lt;/p&gt;

&lt;p&gt;One of the most important parts of that sandbox is access to your databases. And it turns out doing that safely is extremely hard. This post is about why, and how I think about solving it.&lt;/p&gt;

&lt;h2&gt;
  
  
  So what could actually go wrong?
&lt;/h2&gt;

&lt;p&gt;The first time I let an AI agent talk to a real database, I gave it a read-only user and called it a day. That felt safe for about two days.&lt;/p&gt;

&lt;p&gt;Then I sat down and actually wrote out everything that could go wrong. The list was a lot longer than I expected.&lt;/p&gt;

&lt;h3&gt;
  
  
  The model writes something it shouldn't
&lt;/h3&gt;

&lt;p&gt;A model can absolutely write a &lt;code&gt;DELETE&lt;/code&gt;, wrap it in a comment trick, and slip it past a sloppy regex check. "Read-only" enforced in app code is one bad string match away from being not-read-only.&lt;/p&gt;

&lt;p&gt;And once you start trusting the model's output, that gap is hard to see.&lt;/p&gt;

&lt;h3&gt;
  
  
  The model writes something "valid" that nukes your database
&lt;/h3&gt;

&lt;p&gt;A "read-only" Postgres user can still fire off &lt;code&gt;SELECT pg_sleep(3600)&lt;/code&gt; and quietly hold a connection open. Do that a few times and your pool dies, and your real app starts 500'ing for everyone.&lt;/p&gt;

&lt;p&gt;Or the agent writes a 12-table cartesian join that returns half a billion rows. Nothing about that is technically illegal, but it'll OOM the box and take the database down for everyone sharing it.&lt;/p&gt;

&lt;h3&gt;
  
  
  The model reads something it shouldn't
&lt;/h3&gt;

&lt;p&gt;A perfectly innocent-looking join across &lt;code&gt;users&lt;/code&gt; and &lt;code&gt;oauth_tokens&lt;/code&gt; can yank credentials your app never intended to expose. The query is valid SQL. The user has permission. The result is a security incident.&lt;/p&gt;

&lt;p&gt;This one keeps me up at night because there's no syntax check that catches it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prompt injection from your own data
&lt;/h3&gt;

&lt;p&gt;My personal favorite. A single row in your &lt;code&gt;support_messages&lt;/code&gt; table contains a string that convinces the agent to query a different connection entirely.&lt;/p&gt;

&lt;p&gt;The attacker isn't your user. The attacker is text someone wrote three months ago that's now sitting in your database.&lt;/p&gt;

&lt;h3&gt;
  
  
  The ones I didn't think of
&lt;/h3&gt;

&lt;p&gt;And those are just the failures I thought of. The scary ones are always the ones I didn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why one big check doesn't fix it
&lt;/h2&gt;

&lt;p&gt;The deeper problem isn't any single missing check. It's that every layer you'd lean on can be defeated on its own.&lt;/p&gt;

&lt;p&gt;Static SQL parsing can't catch dynamic patterns it doesn't know about. A read-only Postgres role won't stop a query from melting your CPU.&lt;/p&gt;

&lt;p&gt;App-level timeouts don't fire if the network call hangs. Rate limits don't help if the one query that does make it through reads everything.&lt;/p&gt;

&lt;p&gt;Any one of those alone is a paper wall. And you can't fix that by making one wall slightly thicker. You fix it by adding more walls.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix is an onion
&lt;/h2&gt;

&lt;p&gt;The mental model that finally clicked for me was an onion.&lt;/p&gt;

&lt;p&gt;&lt;a href="/static/blog/architecture-of-querybear.png" class="article-body-image-wrapper"&gt;&lt;img src="/static/blog/architecture-of-querybear.png" alt="A simple line drawing of an onion peeled back to reveal a database server sitting inside the innermost layer."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Start with "nothing works"
&lt;/h3&gt;

&lt;p&gt;You start as restrictive as you possibly can. The default is no tables, no columns, no writes, no nothing.&lt;/p&gt;

&lt;p&gt;Then you peel back exactly the capabilities the agent actually needs, and nothing else. Default-deny is honestly doing 90% of the work.&lt;/p&gt;

&lt;h3&gt;
  
  
  Add a layer for every realistic failure mode
&lt;/h3&gt;

&lt;p&gt;The parser is a layer. The database-level read-only transaction is a layer. The statement timeout, the row limit, the column allowlist, the pre-execution cost estimator, the audit log.&lt;/p&gt;

&lt;p&gt;Each one exists specifically because the layer in front of it can fail. That's the point. You're not building one perfect check. You're stacking imperfect ones on purpose.&lt;/p&gt;

&lt;h3&gt;
  
  
  Test it like someone is trying to break it
&lt;/h3&gt;

&lt;p&gt;Not "happy path passes." Adversarial test cases. Prompt injection payloads pulled from real-world lists. Multi-statement attacks. Queries that look fine and aren't. Queries that look bad but are actually fine.&lt;/p&gt;

&lt;p&gt;The whole point of an onion is to assume every layer will eventually be defeated, and to make sure the layer underneath it catches the fall.&lt;/p&gt;

&lt;p&gt;If you only build the first layer, you've built a wall. If you build all of them, you've built a building.&lt;/p&gt;

&lt;h2&gt;
  
  
  Our onion
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://querybear.com" rel="noopener noreferrer"&gt;QueryBear&lt;/a&gt;'s stack is built around exactly this idea. I won't go into every gory detail, but the layers we run today include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A SQL parser that strictly only allows what we expect.&lt;/li&gt;
&lt;li&gt;An allowlist of tables and columns the agent is actually permitted to see.&lt;/li&gt;
&lt;li&gt;AST-level rewriting so row limits and timeouts can't be bypassed by the agent.&lt;/li&gt;
&lt;li&gt;A pre-execution cost check that rejects queries that would scan too much.&lt;/li&gt;
&lt;li&gt;Database-level read-only transactions and statement timeouts as the hard backstop.&lt;/li&gt;
&lt;li&gt;A full audit log so we (and you) can replay everything later.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are a few pieces I'm not putting in a blog post. Some of the secret sauce is in how the layers interact, and in how we test against attacks that haven't been invented yet.&lt;/p&gt;

&lt;p&gt;This is the part of the product I'm most paranoid about, and honestly the part I'm most proud of.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you take one thing from this
&lt;/h2&gt;

&lt;p&gt;Don't trust a single guardrail. Stack them. Default-deny everything, and test it like someone is actively trying to break it.&lt;/p&gt;

&lt;p&gt;Because eventually, someone will be.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>architecture</category>
      <category>database</category>
    </item>
    <item>
      <title>Introducing awesome-cursor-skills: A list of awesome skills for Cursor!!</title>
      <dc:creator>Spencer Pauly</dc:creator>
      <pubDate>Fri, 10 Apr 2026 16:57:41 +0000</pubDate>
      <link>https://forem.com/spencerpauly/introducing-awesome-cursor-skills-a-list-of-awesome-skills-for-cursor-1a8e</link>
      <guid>https://forem.com/spencerpauly/introducing-awesome-cursor-skills-a-list-of-awesome-skills-for-cursor-1a8e</guid>
      <description>&lt;p&gt;awesome-cursor-skills: &lt;a href="https://github.com/spencerpauly/awesome-cursor-skills" rel="noopener noreferrer"&gt;https://github.com/spencerpauly/awesome-cursor-skills&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Been using many of these cursor skills for a while now. Thought I would bring together in one central place others! Some of my favorites:&lt;/p&gt;

&lt;p&gt;suggesting-cursor-rules - If I get frustrated or suggest the same changes repeatedly, suggest a cursor rule for it.&lt;/p&gt;

&lt;p&gt;screenshotting-changelog - Generate visual before/after PR descriptions by screenshotting UI changes across branches.&lt;/p&gt;

&lt;p&gt;parallel-test-fixing - When multiple tests fail, assign each to a separate subagent that fixes it independently in parallel.&lt;/p&gt;

&lt;p&gt;Enjoy! And please add your own skills I'd appreciate it!&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>showdev</category>
      <category>tooling</category>
    </item>
    <item>
      <title>I Gave Claude Code Access to My Prod Database with MCP</title>
      <dc:creator>Spencer Pauly</dc:creator>
      <pubDate>Sun, 29 Mar 2026 20:09:46 +0000</pubDate>
      <link>https://forem.com/spencerpauly/i-gave-my-ai-access-to-my-prod-database-with-mcp-15c0</link>
      <guid>https://forem.com/spencerpauly/i-gave-my-ai-access-to-my-prod-database-with-mcp-15c0</guid>
      <description>&lt;p&gt;Last week I did something that would've made me uncomfortable six months ago. I opened my Claude Desktop config, added an MCP server URL pointing at my production Postgres database, and told Claude to go look at real customer data.&lt;/p&gt;

&lt;p&gt;Nothing caught fire.&lt;/p&gt;

&lt;p&gt;I've been building QueryBear for a while now, and I'd always been careful to test against staging data, demo databases, seed data. Production was the thing I protected.&lt;/p&gt;

&lt;p&gt;But I kept hitting the same wall. I'd be deep in a debugging thread with Claude, connected to Linear and my codebase, and I'd get 90% of the way to understanding a customer issue. Then I'd tab over to my database client, look up the user, write a couple joins, squint at the results, copy them back into chat. Every single time.&lt;/p&gt;

&lt;p&gt;It's not hard. It's just friction. After doing it fifty times in a week I started thinking: why am I the bottleneck here?&lt;/p&gt;

&lt;p&gt;So I built an MCP server that sits between the AI and my database. The AI doesn't get a connection string. It doesn't get credentials. It gets tool calls, and the server decides what happens.&lt;/p&gt;

&lt;p&gt;The config I added to Claude Desktop was two fields:&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;"mcpServers"&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;"querybear"&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;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://mcp.querybear.com/mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"headers"&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;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bearer qb_live_xxxxx"&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;I restarted Claude and typed: "How many users signed up in the last 7 days?"&lt;/p&gt;

&lt;p&gt;Three seconds later I had the answer. From prod. Correct.&lt;/p&gt;

&lt;p&gt;I asked it to break signups down by day. Then by referral source. Then I asked it to cross-reference with which users created their first query. Each answer came back fast, and each one was right.&lt;/p&gt;

&lt;p&gt;What surprised me wasn't that it worked. I built the thing. What surprised me was how natural it felt. I wasn't context-switching anymore. I was just asking questions inside the same thread where I was already working.&lt;/p&gt;

&lt;p&gt;The reason I wasn't scared: every query passes through a security pipeline before it touches the database. SQL gets parsed into an AST, only SELECTs pass. Tables get checked against an allowlist. Sensitive columns get stripped. Row limits get enforced by rewriting the query. Timeouts kill anything that runs too long, server-side. The whole thing runs inside a read-only transaction. And everything gets logged.&lt;/p&gt;

&lt;p&gt;The worst case is the AI gets a "query rejected" response. That's a much better failure mode than "oops, that was production."&lt;/p&gt;

&lt;p&gt;Before this, debugging a customer issue meant reading the ticket in my AI thread, tabbing to the database, writing queries, copying results back. Now I paste the ticket and say "look up this user's account and tell me what's going on." Steps 2 through 5 just disappeared.&lt;/p&gt;

&lt;p&gt;I've also started using it for things I wouldn't have bothered querying before. Quick sanity checks during development. "Did that migration actually backfill the new column?" Instead of writing a throwaway query, I just ask.&lt;/p&gt;

&lt;p&gt;I'm still iterating on this. But the core loop of "ask your AI a question about your data and get a real answer" already works, and it's already changed how I work day to day.&lt;/p&gt;

&lt;p&gt;If you want to try it, you can set it up at querybear.com in a couple minutes. And if you've already connected AI to your database some other way, I'm curious how you handled the security side. Still figuring out where the line should be.&lt;/p&gt;

</description>
      <category>llm</category>
      <category>mcp</category>
      <category>postgres</category>
      <category>productivity</category>
    </item>
    <item>
      <title>So, I gave my coding agent direct database access...</title>
      <dc:creator>Spencer Pauly</dc:creator>
      <pubDate>Sun, 22 Mar 2026 19:37:07 +0000</pubDate>
      <link>https://forem.com/spencerpauly/so-i-gave-my-coding-agent-direct-database-access-33ed</link>
      <guid>https://forem.com/spencerpauly/so-i-gave-my-coding-agent-direct-database-access-33ed</guid>
      <description>&lt;p&gt;I've been connecting my coding agent to everything: Datadog logs, Linear, Slack. But, still get bottlenecked at the database.&lt;/p&gt;

&lt;p&gt;I'll be debugging. The LLM can read the stack trace, make a ticket, scan the codebase, but can't introspect the database. So I can't prove what happened in the data.&lt;/p&gt;

&lt;p&gt;At some point I hacked together a repo on my laptop. It generated SQL and talked to the database for me. And it worked better than I expected.&lt;/p&gt;

&lt;p&gt;But, It also made me nervous. &lt;/p&gt;

&lt;p&gt;Credentials sitting around, no real story for who could run what, no audit trail I could point at if something went sideways. I kept using it for a week and felt worse about it each day.&lt;/p&gt;

&lt;p&gt;I wanted the same speed without the part where I pretend that's fine.&lt;/p&gt;

&lt;p&gt;So I ended up with something I think is pretty cool. I call it querybear. It's a wrapper around my databse to make it AI agent friendly. It adds read-only access, row-level permissions, timeout enforcement, rate limiting, audit trails, schema introspection, and memory with long-living context. &lt;/p&gt;

&lt;p&gt;And it's amazing! I can tell my agent to dive into anything and it can go digging around my data with no risk of misuse.&lt;br&gt;
I know it's a weird pattern but I truly think it's the future. &lt;/p&gt;

&lt;p&gt;Anyone else done similar?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I'm building the future of automated testing</title>
      <dc:creator>Spencer Pauly</dc:creator>
      <pubDate>Fri, 28 Feb 2025 18:55:51 +0000</pubDate>
      <link>https://forem.com/spencerpauly/im-building-the-future-of-automated-testing-2f2d</link>
      <guid>https://forem.com/spencerpauly/im-building-the-future-of-automated-testing-2f2d</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Edit: Thanks to all the early support, I've built and launched my product! You can find it at &lt;a href="https://www.testingbee.io/" rel="noopener noreferrer"&gt;testingbee.io&lt;/a&gt;!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Currently, the way to prevent products from breaking is by writing automated tests. These tests define the steps needed to do a task in your product, and run automatically when you deploy changes.&lt;/p&gt;

&lt;p&gt;For example, an automated test that verifies an "Edit profile" form is working might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@playwright/test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;allows user to edit profile and save changes&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://yourapp.com/profile/edit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;input[name="name"]&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;John Doe&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;input[name="email"]&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;john.doe@example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button:has-text("Save Changes")&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;successMessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text=Profile updated successfully&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;successMessage&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&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;Apps can have hundreds or even thousands of these tests and they all get run before every new update is deployed. Awesome!&lt;/p&gt;

&lt;p&gt;But wait... Do you notice something with the test above? What if we update our app and rename the "Save Changes" button? Or add a 3rd input to the edit profile form? Or move the edit profile page elsewhere?&lt;/p&gt;

&lt;p&gt;This is the root problem with automated testing. They will test your functionality, but they're extremely brittle to changes. This makes it difficult to have complete test coverage while also pushing new features as fast as a startup needs to.&lt;/p&gt;

&lt;p&gt;The other problem with automated testing is they are time consuming to write. This is a simple example, but commonly your tests are much more complicated. And again, multiply this work 100 times over.&lt;/p&gt;

&lt;p&gt;Essentially, automated testing code follows an efficiency graph like so:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn43al0vmg0t95wvnk7k6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn43al0vmg0t95wvnk7k6.png" alt="Testing efficiency" width="800" height="394"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As you can see, adding more tests increases your confidence that your app works, but also hinders your development speed at the same time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;For most companies – the fact they have to write and maintain automated tests isn't a problem. The value of testing outways the cost towards productivity. That's because typically a new feature is never as valuable as your existing ones.&lt;/p&gt;

&lt;p&gt;But, for startups this typically isn't true.&lt;/p&gt;

&lt;p&gt;A startup exists to solve problems for their customers. A product is the proposed solution. Tests ensure the solution keeps functioning as intended. But here’s the bigger question: what if your product isn’t the right solution to begin with?&lt;/p&gt;

&lt;p&gt;Some argue that you should always test your product rigorously, but I believe testing should come with a purpose. That purpose being to ensure your product keeps solving the problem for your customer.&lt;/p&gt;

&lt;p&gt;If the goal of a test is to ensure your product continues solving the problem, shouldn’t the first priority be validating that your product is solving the problem in the first place?&lt;/p&gt;

&lt;p&gt;Shouldn't you ensure that it’s a viable business before focusing on its technical reliability with testing?&lt;/p&gt;

&lt;p&gt;Most startup founders would agree with me, therefore, you hear this common phrase around the startup world a lot:&lt;/p&gt;

&lt;p&gt;"Move fast and break things" - Mark Zuckerberg&lt;br&gt;
But, I believe we can do even better than this mantra.&lt;/p&gt;

&lt;h2&gt;
  
  
  Move fast and don't break things
&lt;/h2&gt;

&lt;p&gt;And that brings us to the solution I've been building for the past month. I built a tool that automatically tests if your product is fully functional, and does it without the overhead of traditional automated tests.&lt;/p&gt;

&lt;p&gt;My tool handles the entire job of automated testing for you, without any micromanagement needed on your part. Instead of writing tests, you simply define broad goals that you want a user to be able to accomplish.&lt;/p&gt;

&lt;p&gt;You don't do this through code, but through simple explanations of end-user experiences. So your testing suite will look something like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqcb0fpvs8c0k7o3fnij7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqcb0fpvs8c0k7o3fnij7.png" alt="Testingbee" width="800" height="351"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Through these broad goals – AI will walk through your site and generate the specific tests you need automatically. Essentially writing your entire automated testing suite for you.&lt;/p&gt;

&lt;p&gt;Then, on any code change, we will run those tests against your product to ensure your code is still functioning exactly as it was before.&lt;/p&gt;

&lt;h2&gt;
  
  
  But, what if a test fails?
&lt;/h2&gt;

&lt;p&gt;Unlike traditional tests that only have specific instructions, our approach includes both the specific instructions and a broader goal for each test. This means a test might fail on a specific detail, but still meet the broader goal.&lt;/p&gt;

&lt;p&gt;For example, if the goal is “a user should be able to edit their profile,” deploying a code change that renames the “Save Changes” button to “Save” would cause a specific test to fail. However, from the user’s perspective, this product is still functioning perfectly.&lt;/p&gt;

&lt;p&gt;And truly, this test shouldn't fail. You want your tests to ensure that your product is functional. But, in the real world tests almost always fail from a product change that doesn't break the user experience.&lt;/p&gt;

&lt;p&gt;Here’s where we shine: we can determine if your product is broken or if it's just a code change that means you need to update your test. And if it's the latter, we'll heal your test automatically with just the press of a button.&lt;/p&gt;

&lt;p&gt;So, for the example above, we'll update the specific instructions to check for a "Save" button instead of a "Save changes" button, re-run the test, then save it to be updated moving forwards.&lt;/p&gt;

&lt;p&gt;No more pushing code changes simply to update your testing code.&lt;/p&gt;

&lt;p&gt;Another advantage of this is that we remove the false alerts that you would otherwise get frequently. Since we can identify when a test is a product breakage and not just a product change, those high priority alerts mean something!&lt;/p&gt;

&lt;p&gt;I'll be launching soon, there's still functionality to add but I have the core working and I'm excited to share it!&lt;/p&gt;

&lt;p&gt;Anyways that's all, thanks!&lt;/p&gt;

</description>
      <category>programming</category>
      <category>testing</category>
      <category>career</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Why does testing suck so much in 2025?</title>
      <dc:creator>Spencer Pauly</dc:creator>
      <pubDate>Fri, 21 Feb 2025 18:19:07 +0000</pubDate>
      <link>https://forem.com/spencerpauly/why-does-testing-suck-so-much-1i2e</link>
      <guid>https://forem.com/spencerpauly/why-does-testing-suck-so-much-1i2e</guid>
      <description>&lt;p&gt;&lt;em&gt;Why are tests so time consuming to write?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;And why do they break so easily?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;And why do they offer no guidance on what went wrong when they do break?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This past month I evaluated some of the most common end-to-end (E2E) testing frameworks: Cypress, Playwright, Selenium — and none of them offered a solution to these problems.&lt;/p&gt;




&lt;p&gt;My solution would be something like this: &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Write automated tests from an end user's perspective.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here is an example of what your test would look like:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;"Click the searchbar"&lt;/li&gt;
&lt;li&gt;Type "Docs site"&lt;/li&gt;
&lt;li&gt;Hit enter&lt;/li&gt;
&lt;li&gt;You should now be on "docs.ourapp.com"&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Take these commands and let AI execute them as a human would.&lt;/p&gt;

&lt;p&gt;It fixes all the problems above.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Time consuming to write?&lt;/strong&gt;&lt;br&gt;
Not anymore. Just write the steps in pure english.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Break easily?&lt;/strong&gt; &lt;br&gt;
 Not anymore. Because if the end user flow keeps working then the test will keep working even if the underlying code changes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No guidance to fix tests?&lt;/strong&gt;&lt;br&gt;
Get a direct printout from the AI saying exactly which step failed and &lt;strong&gt;why&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Edit: Thanks to early demand I've built this product! Find it at &lt;a href="//testingbee.io"&gt;testingbee.io&lt;/a&gt;!&lt;/p&gt;

</description>
      <category>testing</category>
      <category>programming</category>
      <category>playwright</category>
      <category>selenium</category>
    </item>
    <item>
      <title>I built an AI Tool to write medium posts 10x faster.</title>
      <dc:creator>Spencer Pauly</dc:creator>
      <pubDate>Wed, 06 Jul 2022 15:52:06 +0000</pubDate>
      <link>https://forem.com/spencerpauly/i-built-an-ai-tool-to-write-medium-posts-10x-faster-3b5i</link>
      <guid>https://forem.com/spencerpauly/i-built-an-ai-tool-to-write-medium-posts-10x-faster-3b5i</guid>
      <description>&lt;p&gt;In 2020, I fell in love with blogging here on Medium. Articles are fun to write, provide value for others, and even make a little money. It's a perfect combination!&lt;/p&gt;

&lt;p&gt;But, there's one really, really annoying issue with writing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It's. So. Time-consuming.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Even a simple post like this one takes me 4 or 5 hours to write. This heavily limits my output. If I'm going to write a new post, I have to mentally commit to giving up a day of my life to do it. And that shouldn't be the case.&lt;/p&gt;

&lt;p&gt;So, I decided to analyze how I spend my time while crafting a new article. This was the breakdown.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Femi95y9pltrle14u2t3y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Femi95y9pltrle14u2t3y.png" alt="time spent writing medium blog post" width="800" height="432"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The most shocking finding: I spend 50% of my time editing.&lt;/p&gt;

&lt;p&gt;But, thinking back, that makes sense. I have many unpublished rough drafts that I simply haven't finished because of the time commitment of editing. So, I set out to solve this problem.&lt;/p&gt;

&lt;p&gt;I found some tools that would help me write faster, but they usually added another layer of workflow complexity. Either I'd need to write in a completely separate editor, or get only half the functionality I wanted.&lt;/p&gt;

&lt;p&gt;So, I decided to set out and build my own tool to write medium posts fast.&lt;/p&gt;

&lt;p&gt;Introducing, my AI writing tool for medium.&lt;/p&gt;

&lt;p&gt;I'll note, this was just an experiment. I made a local version to test, but after finding how much time it's saved me I thought it would be cool to share.&lt;/p&gt;

&lt;p&gt;It looks something like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fom91uuvs2syvmx6ej8e6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fom91uuvs2syvmx6ej8e6.png" alt="AI tool for medium writers" width="800" height="481"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This general gist is that the tool combines AI and traditional grammar-checking to make editing a breeze. Idea generation can be done on-the-fly as the AI creates sentence suggestions. Trimming down long, confusing, or badly worded sentences is easy as-well because the suggestions are available right in the medium editor.&lt;/p&gt;

&lt;p&gt;Plus, it's just a chrome extension. Everything is still done in the medium editor so my workflow isn't compromised.&lt;/p&gt;

&lt;p&gt;Here's a taste of some of the functionality I found to be most useful:&lt;/p&gt;

&lt;h3&gt;
  
  
  💡 AI Brainstorming - Title &amp;amp; subtitle suggestions using AI
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhyow1p0l7wq3ggqkqtmf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhyow1p0l7wq3ggqkqtmf.png" alt="AI-powered title and subtitle brainstorming for medium articles" width="800" height="484"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is huge. The AI hooks into a database of millions of blog posts to give title suggestions based on patterns seen elsewhere. Since I usually don't go with my first title, this feature makes brainstorming easy.&lt;/p&gt;

&lt;p&gt;Plus, the AI uses the content of your post to guide its suggestions. As I add more content to a post, I get new and different title ideas that I might not've thought of before.&lt;/p&gt;

&lt;h3&gt;
  
  
  ✅ Grammar checking - directly in the medium editor
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6p378ediw1aa4rjd73fd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6p378ediw1aa4rjd73fd.png" alt="AI powered medium grammar checking" width="800" height="279"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Usually this is the most time-consuming part of writing. It's hard to write perfect sentences the first time, so a grammar checking tool is an absolute must for me.&lt;/p&gt;

&lt;p&gt;Usually, I offload this work to another tool. But copying the content back and forth can become a pain. I added this functionality to my tool so it can all be done on Medium's editor. No more time-wasting.&lt;/p&gt;

&lt;h3&gt;
  
  
  ✨ Sentence suggestions - rewrite &amp;amp; brainstorm with AI
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl9b534o8wrpqx5k8i3yk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl9b534o8wrpqx5k8i3yk.png" alt="AI powered sentence writing for Medium" width="800" height="215"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is one of my favorite features. As I write, the AI will offer suggestions for ways to rewrite a sentence or continue a train of thought. I don't always use these suggestions, but they keep me thinking about how to flow my thoughts together and I find that &lt;em&gt;invaluable&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;I built this tool as a simple local chrome extension for myself. I've thought of providing it publicly, but it would be a lot of work to take it over the finish line.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Is this something people would be interested in?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Like I said, it would take a lot of effort to deliver it publicly, but if this is something people would use then I might give it a shot. I've even thought of ideas to make it more powerful.&lt;/p&gt;

&lt;p&gt;Things like hooking into other tools to generate images on-demand. But again, I mostly built it to work for my use-case so I'd be interested to hear how others would find this helpful.&lt;/p&gt;

</description>
      <category>writing</category>
      <category>ai</category>
      <category>tooling</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Users preferred the less straightforward UX – and I finally understand why</title>
      <dc:creator>Spencer Pauly</dc:creator>
      <pubDate>Wed, 22 Jun 2022 23:50:32 +0000</pubDate>
      <link>https://forem.com/spencerpauly/users-preferred-the-less-straightforward-ux-and-i-finally-understand-why-2lf6</link>
      <guid>https://forem.com/spencerpauly/users-preferred-the-less-straightforward-ux-and-i-finally-understand-why-2lf6</guid>
      <description>&lt;p&gt;Two years ago, I learned a valuable lesson about creating good UX:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;It might not seem intuitive, but sometimes users prefer the less straightforward UX&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;At this time, I was building the first version of a new cross-platform app called &lt;a href="https://skiwise-app.com" rel="noopener noreferrer"&gt;Skiwise&lt;/a&gt;. I thought I was creating the best experience possible, but as I later learned, straightforward doesn't always equal intuitive.&lt;/p&gt;

&lt;p&gt;This is the story of how I learned this lesson, and how these learnings can help you build better software.&lt;/p&gt;

&lt;h2&gt;
  
  
  It's confusing?
&lt;/h2&gt;

&lt;p&gt;Skiwise is an app that lets cross-country skiers access crowd-sourced reports on the trail conditions of various ski trails. This lets them decide where to ski on a given day and what skis/wax/clothes to bring with them. You can think of this as essentially "the weather app for cross-country ski trails".&lt;/p&gt;

&lt;p&gt;With the goal clear, the initial version of the UI looked something like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feqqjeqboue9t11nnnfdj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feqqjeqboue9t11nnnfdj.png" alt="skiwise initial release" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As you can see, the main page contains a list of trails you follow and displays the most recent trail report from each trail. "This is great", I thought at the time, "The info is instantaneous! You can browse the newest trail reports in 2 seconds! There's no friction! People will love this!".&lt;/p&gt;

&lt;p&gt;And the result? People didn't love it.&lt;/p&gt;

&lt;p&gt;So, I went back to the basics. And there's nothing more basic than watching users interact with your app. But, it wasn't until I put the app in the hands of my Mother that I discovered the problem.&lt;/p&gt;

&lt;p&gt;She loaded the app, got to the home screen, scrolled a bit, stopped, scrolled some more, stopped again, then looked at me and said "what now?".&lt;/p&gt;

&lt;p&gt;And that's when I had the "aha moment".&lt;/p&gt;

&lt;p&gt;See, I'd made the information so readily available that it left the user no desire to dive deeper. It took away all the exploration. It's like being handed a solved Rubix cube. There's no clear direction, no problem to solve, and no task to be accomplished.&lt;/p&gt;

&lt;p&gt;And, the way this information was presented didn't line up with a user's mental model. It was hard for people to conceptualize that the feed was showing the most recent report for favorite trails in order of report date.&lt;/p&gt;

&lt;p&gt;This is similar to having a list of the most popular book by each artist sorted by the book's release date. It's just a complicated set of relationships to think about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Simplifying things, by adding complexity
&lt;/h2&gt;

&lt;p&gt;So, I changed the user experience. Instead of delivering all the information on the home screen, users now had to hunt on a map for it. And this dramatically changed the experience.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzgldwocysf27sklhf3l2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzgldwocysf27sklhf3l2.png" alt="skiwise current release" width="800" height="531"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, instead of the info being discoverable instantly, it takes about 30 seconds. But, it lines up better with a user's mental model. When surveyed, 76% of users reported that they only skied at trails within 30 minutes of their home. This was a huge breakthrough.&lt;/p&gt;

&lt;p&gt;It indicates that people place the highest priority on nearby trails, and don't care about trails far away from home. This points to a clear data structure – a map.&lt;/p&gt;

&lt;p&gt;And, there's a certain satisfaction when you click on a map, find a trail, see a recent trail report, and get your answers.&lt;/p&gt;

&lt;p&gt;So, despite it taking objectively longer for users to get their answers, they loved it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How you can apply this?
&lt;/h2&gt;

&lt;p&gt;The key is in mental models. If you think about it, every app comes with a set of mental models and interaction patterns that users will intuitively follow.&lt;/p&gt;

&lt;p&gt;For example, when you use Instagram you quickly learn that posts are attributed to users, with no exceptions. This becomes a core mental model of the app. It helps everything else become intuitive as well. You learn that comments are attributed to users. And posts can be clicked on to see a user's profile. And your feed is just a series of posts.&lt;/p&gt;

&lt;p&gt;So, say you have a software-as-a-service app. Write down the core mental models your users are using and use that to determine if your UI is intuitive. Does a user own multiple projects in their dashboard? Do settings belong to projects or users? Where should the settings button be located to indicate that?&lt;/p&gt;

&lt;p&gt;From there, use something like &lt;a href="https://hotjar.com" rel="noopener noreferrer"&gt;Hotjar&lt;/a&gt; or &lt;a href="https://clarity.microsoft.com" rel="noopener noreferrer"&gt;Microsoft Clarity&lt;/a&gt; to watch your users interacting with your app. Do they see the mental models the same way you do?&lt;/p&gt;

&lt;p&gt;Want some more tips? I wrote another article with quick fixes for &lt;a href="https://blog.pwego.com/7-common-saas-dashboard-mistakes-how-to-fix/" rel="noopener noreferrer"&gt;7 common SaaS dashboard mistakes&lt;/a&gt;. These can give you good hints on low hanging fruit that might be causing friction for users of your app.&lt;/p&gt;

&lt;p&gt;Also, feel free to get in contact with me directly. I run a consultancy called &lt;a href="https://pwego.com" rel="noopener noreferrer"&gt;Pwego&lt;/a&gt; where I help businesses write better software and create measurable business outcomes as a result. If this is a pain point for your business, let's see if I can help!&lt;/p&gt;

</description>
      <category>ux</category>
      <category>design</category>
      <category>startup</category>
      <category>todayilearned</category>
    </item>
    <item>
      <title>The #1 Best Design Pattern for Managing Forms in React</title>
      <dc:creator>Spencer Pauly</dc:creator>
      <pubDate>Fri, 12 Nov 2021 19:47:13 +0000</pubDate>
      <link>https://forem.com/spencerpauly/the-1-best-design-pattern-for-managing-forms-in-react-4215</link>
      <guid>https://forem.com/spencerpauly/the-1-best-design-pattern-for-managing-forms-in-react-4215</guid>
      <description>&lt;p&gt;Ughh… why does form code in React always get so messy?&lt;/p&gt;

&lt;p&gt;It starts out simple: a &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt; component, a couple input fields, and a submit button. But soon things get a little more complicated.&lt;/p&gt;

&lt;p&gt;You think, "hmmm.. I want some more validation for this zip code field". So you add a custom workaround that validates the data in the input field.&lt;/p&gt;

&lt;p&gt;Then, you think "I want to disable the submit button when the form is submitting". So you create another custom workaround that keeps track of what's submitting, and when things are complete, etc.&lt;/p&gt;

&lt;p&gt;Then, you think "I want better error handling". So you add yet another workaround.&lt;/p&gt;

&lt;p&gt;And over time that simple form balloons into a 400-line long super-component with multiple useEffects, useStates, and custom logic to handle all the edge cases.&lt;/p&gt;

&lt;p&gt;Sound familiar?&lt;/p&gt;

&lt;p&gt;I've had this trouble more times than I'd like to admit. So 6 months ago, I decided to double down and find the solution. I wanted to know:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What's the absolute BEST way to manage forms in React so they're organized, performant, and easy to debug?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's what I'm going to share here today.&lt;/p&gt;

&lt;h1&gt;
  
  
  A Form Library - Do I need one?
&lt;/h1&gt;

&lt;p&gt;I've come at this crossroads before. As a project is small the answer usually starts as "&lt;strong&gt;nah&lt;/strong&gt;", then over time it inevitably sways towards "&lt;strong&gt;please, please yes&lt;/strong&gt;".&lt;/p&gt;

&lt;p&gt;So now, I advocate for form management libraries no matter what scale of project. Form libraries usually have a relatively small bundle size and make a world of difference for code organization.&lt;/p&gt;

&lt;p&gt;But, I should note: I've also seen custom form management work in the past.&lt;/p&gt;

&lt;p&gt;The issue is that it's really difficult. It's possible, but even if you're successful you'll usually end up building a similar version of another form library except without all the great documentation.&lt;/p&gt;

&lt;p&gt;That's why I recommend starting your project with a good form library from the get-go. So that brings us to the next question.&lt;/p&gt;

&lt;h1&gt;
  
  
  What's the best form library?
&lt;/h1&gt;

&lt;p&gt;This decision making process could be a whole other article in itself. But, I want to focus on concrete design patterns today, so I'm only going to give a high-level overview of the landscape.&lt;/p&gt;

&lt;h4&gt;
  
  
  The Plethora of Form Management Libraries
&lt;/h4&gt;

&lt;p&gt;The landscape for form management libraries in React is huge. But, luckily it's concentrated among only a few popular libraries. Some of the most popular are: react-hook-form, formik, redux form, and react-final-form.&lt;/p&gt;

&lt;p&gt;Here's a breakdown of their popularity, with Formik as the most popular and react-hook-form chasing close on their heals.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fji94ofhnz6dg548byp7t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fji94ofhnz6dg548byp7t.png" alt=" " width="800" height="272"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As I already mentioned, I'm not going to be deeply comparing these solutions in this article. But, if you want a great article comparing these, &lt;a href="https://dev.tocheck%20out%20this%20great%20post%20comparing%20these%20libraries%20from%20Retool"&gt;https://retool.com/blog/choosing-a-react-form-library/&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;With that said, the two form libraries that I consider to be an excellent choice are &lt;em&gt;Formik&lt;/em&gt; and &lt;em&gt;React-Hook-Form&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Both provide hook-centric form management and have great documentation, active devs, and a healthy user base.&lt;/p&gt;

&lt;p&gt;However, between these two, I tend to lean towards React-Hook-Form and I'll explain why below.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why React-Hook-Form?
&lt;/h3&gt;

&lt;p&gt;React-hook-form (RHF) is great because it prioritizes hooks to manage your form state (hence the name). This makes it fast, flexible, and a breeze to work with if you're already using hooks.&lt;/p&gt;

&lt;p&gt;Among it's various benefits, one advantage over Formik is that react-hook-form was created exclusively for hooks. This means, although react-hook-form can't support class components, their docs and best practices are more focused. If you look up articles online, you won't find a lot of outdated guides with old design patterns. I find this extremely valuable when trying to learn a new library.&lt;/p&gt;

&lt;p&gt;They also have numerous other small performance, bundle, and flexibility advantages over the other libraries. Here's just some examples:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu742ck7vvxnxenag7t38.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu742ck7vvxnxenag7t38.png" alt=" " width="800" height="401"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;That's why I chose React-Hook-Form. However, if your codebase uses a lot of class components you might be better off going with Formik as it'll be easier to integrate into your components.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I'm going to touch on more advanced design patterns in this article, however if you're confused at any time here's some great resources for understanding React-Hook-Form: &lt;a href="https://react-hook-form.com/get-started" rel="noopener noreferrer"&gt;The official getting started guide&lt;/a&gt;, &lt;a href="https://blog.logrocket.com/using-material-ui-with-react-hook-form/" rel="noopener noreferrer"&gt;Using RHF with material UI&lt;/a&gt;, and &lt;a href="https://travis.media/react-hook-form-controller-examples/" rel="noopener noreferrer"&gt;RHF with 5 different UI library examples&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h1&gt;
  
  
  The 3 Layer Approach
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkdxxnn5jfquyzfvjmxbs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkdxxnn5jfquyzfvjmxbs.png" alt=" " width="800" height="576"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;The basic premise of the 3 layer approach is to take a complicated form component and split it into three parts.&lt;/p&gt;

&lt;p&gt;Each part will be its own react component and will focus on one responsibility of the form (see: SOLID). Each part will also be named with a suffix (Apollo, Logic or View), which will make it easier to find.&lt;/p&gt;

&lt;p&gt;Here's an overview of what each component does:&lt;/p&gt;

&lt;h4&gt;
  
  
  Apollo Component
&lt;/h4&gt;

&lt;p&gt;This component handles strictly the network requests for your form (aka. fetching the initial data for the form, and submitting the final data to your backend). It's named "Apollo" because I typically use Apollo to talk to my GraphQL backend. Feel free to use a more relevant suffix such as: "API", "Network", or "Fetch" if you prefer.&lt;/p&gt;

&lt;h4&gt;
  
  
  Logic Component
&lt;/h4&gt;

&lt;p&gt;This handles the logic for the form. This is the component where you'll define the shape of the form, default values, and validation.&lt;/p&gt;

&lt;h4&gt;
  
  
  View Component
&lt;/h4&gt;

&lt;p&gt;This component renders the view of the form. It's meant to be a stateless component. However, I usually allow view-related state in this component such as an isOpen toggle for an expandable section of the form or something similar.&lt;/p&gt;

&lt;h1&gt;
  
  
  The 3 Layer Pattern Further Explained
&lt;/h1&gt;

&lt;p&gt;This chart shows how the data will flow between these three layers to create an organized form structure. Start at the Apollo.tsx file and follow the arrows to read how the data will flow through the components.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8ihi5qyrp4sc6qxn92q2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8ihi5qyrp4sc6qxn92q2.png" alt=" " width="800" height="576"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let's dive a little further into each of these components. I use TypeScript in this example, because it will help give a good look into the different types of data being passed around.&lt;/p&gt;

&lt;p&gt;Also, &lt;a href="https://codesandbox.io/s/react-hook-form-3-layer-design-pattern-d4rup?from-embed" rel="noopener noreferrer"&gt;here is the finished codebase&lt;/a&gt;. If you're a hands-on learner feel free to play around yourself as you read.&lt;/p&gt;

&lt;h2&gt;
  
  
  CreateUserApollo.tsx Explained
&lt;/h2&gt;

&lt;p&gt;The Apollo component is responsible for fetching form data over the wire. Here's what it looks like.&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;p&gt;There's a couple things I want to point out about this component.&lt;/p&gt;

&lt;p&gt;First of all, notice how the data fetched from the database is transformed before being passed down into the default values of . This is important, because in general it's a good idea not to trust data fetched from over the wire. If you don't do this it can go wrong in one of three ways.&lt;/p&gt;

&lt;p&gt;(a) You can end up fetching too many fields from the API. This means your form will have more defaults than it needs. This can add clutter to your form and problems when we get to validation.&lt;/p&gt;

&lt;p&gt;(b) This also safeguards against bad defaults (ex. undefined). Instead of trusting the backend, it's a good idea to provide sensible defaults, such as the empty string, just in-case.&lt;/p&gt;

&lt;p&gt;(c) It's more robust. Notice how the user field from the API is transformed into the username field before being passed down to the form? This is useful for other fields too. Ex. parsing a string timestamp from the backend into a Date object for the form.&lt;/p&gt;

&lt;p&gt;The second thing I want to point out is regarding the handleSubmit function. This function takes the submitted form data, transforms it into JSON for the API, and returns an async function for updating the database with the result.&lt;/p&gt;

&lt;p&gt;Returning the async function is important. You'll see this a bit later, but essentially it allows you to await the API response in your CreateUserLogic component which means you can know what the submission status of the form currently is.&lt;/p&gt;

&lt;h2&gt;
  
  
  CreateUserLogic.tsx Explained
&lt;/h2&gt;

&lt;p&gt;The goal of this component is simple: set up the form with the default values, pass the form down to the view layer, then handle submitting the form to the parent component when the submit button is pressed.&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;p&gt;The main thing I want to point out here is the handleSubmit function. You'll remember that the Apollo component had a handleSubmit function too. Why do you need two of them?&lt;/p&gt;

&lt;p&gt;The reason is to keep our three layers modular. The handleSubmit in this component lets you make state changes after a successful submission of the form. It doesn't care how that data is submitted, it just cares about when it completes.&lt;/p&gt;

&lt;p&gt;Trust me, I've tried doing it other ways and eventually you'll realize this way is the cleanest. It lets you keep each layer from needing to care about what's happening in the other layers and instead simply focusing on what they care about.&lt;/p&gt;

&lt;p&gt;In this example, we reset the form after submitting. But, you can just as easily use this to route to a different page, show a success toast, close a modal, etc. This design pattern leaves it up in the air, which is good.&lt;/p&gt;

&lt;p&gt;Also, it's important that you either await or return the onSubmit(data) function. If you don't, everything will still work but react-hook-form won't know when you've completed the submission process and won't properly handle the isSubmitting state of the form.&lt;/p&gt;

&lt;h2&gt;
  
  
  CreateUserView.tsx Explained
&lt;/h2&gt;

&lt;p&gt;Finally we have the simplest component. This one simply renders out your form fields. Since you've done all the hard work in the layers above this component can be pretty simple.&lt;/p&gt;

&lt;p&gt;This is great because in a large form this will usually be your biggest component. Additionally, this component only handles the "look" of the form and won't deal with any logic. This is great because now you can easily hand this file off to a designer and the designer won't need to care about how the form works, they only have to worry about how it &lt;em&gt;looks&lt;/em&gt;. Which is great!&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;h1&gt;
  
  
  The benefits of this pattern
&lt;/h1&gt;

&lt;p&gt;Ok, so I mentioned at the beginning of the article all my pain points when building forms. Not only does this structure solve all of those, it also has some other inherit benefits as-well.&lt;/p&gt;

&lt;h4&gt;
  
  
  ✅ Built in type checking and validation for every step of your form
&lt;/h4&gt;

&lt;p&gt;If you noticed, the logic component contains per-field validation, and every step of this process has strong typescript typings. This makes it very hard to mess up and much easier to debug.&lt;/p&gt;

&lt;h4&gt;
  
  
  🔍 Easy to find where things happens
&lt;/h4&gt;

&lt;p&gt;Do you have an issue submitting data to the backend? It's likely in the Apollo component. Issue with the default value of a field? Logic component. Issue with the "look" your form? View component. Super easy!&lt;/p&gt;

&lt;h4&gt;
  
  
  💨 Automated testing is a breeze
&lt;/h4&gt;

&lt;p&gt;This is a commonly under-looked benefit of this pattern. But, if you notice, you can test the functionality of a form by passing props to the Logic components directly. There is no need to mock your backend at all since you can test all the functionality by bypassing the Apollo component entirely.&lt;/p&gt;

&lt;h4&gt;
  
  
  🎁 Forms become much more composable
&lt;/h4&gt;

&lt;p&gt;This means you can mix and match different layers to have the form behave differently. You can have different Apollo components submit form data in a different way (ex. editing vs. creating a document). Or vice versa, you can reuse an Apollo component for different forms to submit different data to the same backend services. Really cool!&lt;/p&gt;

&lt;h4&gt;
  
  
  👥 Easy to Divide-And-Conquer for Teams
&lt;/h4&gt;

&lt;p&gt;This structure lends itself well to working with a team. Your designer can work on the View layer, while the backend person can work on the Apollo component. Then, you can easily meet in the middle at the Logic component and get your new feature launched twice as fast!&lt;/p&gt;

&lt;h1&gt;
  
  
  And that's the design pattern!
&lt;/h1&gt;

&lt;p&gt;As you can see, by combining a good form library with a good design pattern can make messy form code a thing of the past. It allows for easier collaboration, cleaner development, and faster debugging. What's not to like?&lt;/p&gt;

&lt;p&gt;If you have any further questions or improvements, leave a comment!&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>react</category>
      <category>webdev</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Here's Actually Why Deno Flopped</title>
      <dc:creator>Spencer Pauly</dc:creator>
      <pubDate>Mon, 28 Sep 2020 13:54:15 +0000</pubDate>
      <link>https://forem.com/spencerpauly/here-s-actually-why-deno-flopped-3k1a</link>
      <guid>https://forem.com/spencerpauly/here-s-actually-why-deno-flopped-3k1a</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Brought to you by &lt;a href="https://engine.so" rel="noopener noreferrer"&gt;engine.so&lt;/a&gt; - a tool to instantly create a public self-service knowledge base for your customers with Notion.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Deno is a Javascript / TypeScript runtime looking to take the place of Node.js as the status quo. It boasts a wide slew of features and has a lot of hype around the project with almost 68,000 stars on Github:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fi8vv1fjooy4020yqm54z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fi8vv1fjooy4020yqm54z.png" alt="Alt Text" width="338" height="128"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With so many great features, the question to ask is:&lt;br&gt;
Why didn't Deno take off when it released it's official version 1.0?&lt;/p&gt;

&lt;p&gt;This article looks to dive into that question…&lt;/p&gt;




&lt;h1&gt;
  
  
  So, what's Deno?
&lt;/h1&gt;

&lt;p&gt;Deno is a secure JavaScript and TypeScript runtime created by Ryan Dahl (who's also the original creator of Node.js). It was created to fix some of the oversights made when first designing Node.js back in 2009. In my opinion this motivation makes a lot of sense because I'm sure every programmer would love to get a chance to rewrite their 10 year old code.&lt;br&gt;
In this case, Deno boasts quite a few features over Node.js:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deno is secure by default. Access to the file system, network, or environment has to be opt-in&lt;/li&gt;
&lt;li&gt;Deno was built for TypeScript out of the box
External files are explicitly referenced by a URL. No package.json.&lt;/li&gt;
&lt;li&gt;Import statements include file extensions (.ts,.tsx,.js,.json)&lt;/li&gt;
&lt;li&gt;Built-in dependency inspector and file formatter utilities
And more…&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And with these features in its arsenal combined with an enormous amount of developer hype, Deno had its official 1.0 release in May 2020.&lt;/p&gt;

&lt;p&gt;And then…&lt;/p&gt;

&lt;p&gt;Crickets.&lt;/p&gt;




&lt;h1&gt;
  
  
  Why Didn't Deno Take Off?
&lt;/h1&gt;

&lt;p&gt;Deno looked like it had all the ingredients for success. It had a massive following, a solid batch of features, an experienced creator, and more, but it didn't really have the growth everyone expected. Why is that?&lt;/p&gt;

&lt;p&gt;I think it's best to look at it from a business perspective. See, most of us forget that building open source software is really no different than building software for users. The standard economic principles of supply and demand still play a large role.&lt;/p&gt;

&lt;p&gt;When someone is creating a new open source project, they're typically going to be competing with something that already exists. With that in mind, you have to consider not only how good your new project is, but also what it looks like compared to what's already available.&lt;/p&gt;

&lt;p&gt;In Deno's case, what was already available was Node.js, and while Node.js might have its flaws, it's still very capable of doing its job. Now, if Deno came out with a blowaway feature that Node.js would never be able to replicate, that might change the game. But it didn't.&lt;/p&gt;

&lt;p&gt;Deno only really sported "minor features" from a users perspective. It had a cleaner codebase, used up-to-date best-practices, and had better security, but those things are really only "features" to a user, not a product in themselves. You could make an email client exactly like Gmail except it has better security and a 50% speed improvement, but users still wouldn't switch to it because even the tiny amount of time it takes to create a new bookmark wouldn't be worth it.&lt;/p&gt;

&lt;p&gt;So that's strike 1 against Deno: It has quite a few nice-to-have features, but there's nothing standout that inspires users to switch away from Node.js.&lt;/p&gt;

&lt;p&gt;The other major strike against Deno is that it doesn't support NPM packages. If Deno were able to support NPM packages, that would change the game for them. Deno supporting NPM packages would make them much less of a "separate email client", and more like a better wrapper around the current client.&lt;/p&gt;

&lt;p&gt;Supporting NPM packages would greatly reduce the barrier to entry. It would act as a good stepping stone for users to migrate their projects and libraries towards Deno.&lt;/p&gt;

&lt;p&gt;Think of it as similar to TypeScript's "strict-mode". For users with a huge codebase of JavaScript, jumping directly into TypeScript would cripple your productivity for weeks while you sort through all the error messages. Because TypeScript has the ability to disable strict mode, it can act as a stepping stone for users to slowly migrate over to pure TypeScript. This gives them a much lower barrier to entry, and in turn has helped TypeScript rip away market share from JavaScript in recent years.&lt;/p&gt;




&lt;h1&gt;
  
  
  What's the Takeaway?
&lt;/h1&gt;

&lt;p&gt;I think this an interesting case-study that exemplifies a larger methodology in business. The takeaway is that if you're going to release a new product into the market, you have to make sure it's something where the upside is so great that it overcomes the resistance from people switching from the status quo.&lt;/p&gt;

&lt;p&gt;In Deno's case they had the initial allure, but when it came down to it Deno was really only offering a collection of small "fixes" at the price of losing the whole NPM ecosystem that Node.js had cultivated and this tipped the scales for them.&lt;/p&gt;




&lt;h1&gt;
  
  
  Where does Deno go from here?
&lt;/h1&gt;

&lt;p&gt;Well they have a decision to make. They can either work on adding backwards compatibility to Node.js libraries, or they can increase their offering to make the compulsion to switch just that much more enticing. I personally think backwards compatibility is the way to go, and I think if that was added it would drastically alter the future of the project.&lt;/p&gt;




&lt;p&gt;Either way, best of luck to the deno team and I hope the best technology wins in the end. I hope you enjoyed the article, thanks.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>node</category>
      <category>deno</category>
      <category>typescript</category>
    </item>
  </channel>
</rss>
