<?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: Marc Gil</title>
    <description>The latest articles on DEV Community by Marc Gil (@marcgil_dev).</description>
    <link>https://dev.to/marcgil_dev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4023059%2Fb82f7082-460e-4ed3-9b7a-79c7e98dc76c.png</url>
      <title>DEV Community: Marc Gil</title>
      <link>https://dev.to/marcgil_dev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/marcgil_dev"/>
    <language>en</language>
    <item>
      <title>I spent a week on OAuth plumbing for an MCP server before writing a single line of actual product logic.</title>
      <dc:creator>Marc Gil</dc:creator>
      <pubDate>Tue, 14 Jul 2026 15:40:01 +0000</pubDate>
      <link>https://dev.to/marcgil_dev/i-spent-a-week-on-oauth-plumbing-for-an-mcp-server-before-writing-a-single-line-of-actual-product-3ik4</link>
      <guid>https://dev.to/marcgil_dev/i-spent-a-week-on-oauth-plumbing-for-an-mcp-server-before-writing-a-single-line-of-actual-product-3ik4</guid>
      <description>&lt;p&gt;I spent a week on OAuth plumbing for an MCP server before writing &lt;br&gt;
a single line of actual product logic.&lt;/p&gt;

&lt;p&gt;Four hours in I was reading RFC 9728. Not one line of that code &lt;br&gt;
made the server smarter or more useful. I shipped with a static &lt;br&gt;
API key and moved on.&lt;/p&gt;

&lt;p&gt;Eventually I went back and built it properly. And the part I &lt;br&gt;
thought would be easy — Stripe billing — turned out to have three &lt;br&gt;
edge cases that nobody documents anywhere.&lt;/p&gt;

&lt;p&gt;This is about those three cases.&lt;/p&gt;


&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;You've got an MCP server. Users call it. You want to charge per call.&lt;br&gt;
The obvious path: POST to Stripe's API after each tool call, done.&lt;/p&gt;

&lt;p&gt;Except it's not done.&lt;/p&gt;


&lt;h2&gt;
  
  
  Problem 1: Retries
&lt;/h2&gt;

&lt;p&gt;A client makes a tool call. Your server processes it. Before the &lt;br&gt;
response arrives, the client's network drops. The client retries.&lt;/p&gt;

&lt;p&gt;You now have two requests for what is logically one call. Without &lt;br&gt;
protection, you bill twice.&lt;/p&gt;

&lt;p&gt;The fix is an idempotency key — a stable identifier for the &lt;br&gt;
logical request, not the HTTP request. The client generates it &lt;br&gt;
once and reuses it across retries. Your server enforces uniqueness &lt;br&gt;
at the database level:&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="c1"&gt;// POST /api/usage&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;existing&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;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;usageEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findUnique&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;idempotencyKey&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;deduplicated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&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;The key detail: uniqueness enforced by a DB constraint, not a &lt;br&gt;
SELECT-before-INSERT. The race condition between two simultaneous &lt;br&gt;
retries hitting the server at the same instant is closed by the &lt;br&gt;
constraint, not by application logic.&lt;/p&gt;

&lt;p&gt;The client side 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="c1"&gt;// In your MCP server — generate once per logical call&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;idempotencyKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;mcpRequestId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-search_documents`&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;APP_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/api/usage`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;search_documents&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;units&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;completed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Problem 2: Interrupted streaming responses
&lt;/h2&gt;

&lt;p&gt;You're streaming a response. 3,000 tokens in, the user closes &lt;br&gt;
the tab.&lt;/p&gt;

&lt;p&gt;Do you bill for those 3,000 tokens? Or nothing?&lt;/p&gt;

&lt;p&gt;There's no objectively correct answer — it depends on your cost &lt;br&gt;
structure and what you've promised users. But you have to make &lt;br&gt;
the decision explicitly, and you have to make it before you ship.&lt;/p&gt;

&lt;p&gt;The approach I took: register usage once, when the stream finishes. &lt;br&gt;
If it doesn't finish, send a &lt;code&gt;partial&lt;/code&gt; status instead of skipping &lt;br&gt;
the call entirely.&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="c1"&gt;// At stream completion&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;reportUsage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;generate_text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;units&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;totalTokensStreamed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;streamCompleted&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;completed&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;partial&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;partial&lt;/code&gt; events are always recorded in Postgres for auditing. &lt;br&gt;
Whether they're reported to Stripe is configurable:&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="c1"&gt;// Default: only 'completed' goes to Stripe&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;BILLABLE_STATUSES&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;completed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;// If you want to bill partial calls too:&lt;/span&gt;
&lt;span class="c1"&gt;// const BILLABLE_STATUSES = ['completed', 'partial']&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important thing isn't which option you pick — it's that you &lt;br&gt;
pick one explicitly and document it, because your users will ask.&lt;/p&gt;


&lt;h2&gt;
  
  
  Problem 3: What "failed" means
&lt;/h2&gt;

&lt;p&gt;Not every failed call should be treated the same way.&lt;/p&gt;

&lt;p&gt;A tool call that fails because your server threw an unhandled &lt;br&gt;
exception: probably shouldn't be billed.&lt;/p&gt;

&lt;p&gt;A tool call that fails because the user's input was invalid &lt;br&gt;
(and you processed it, returned a 400, and spent compute doing &lt;br&gt;
so): maybe should be billed.&lt;/p&gt;

&lt;p&gt;The status field handles 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="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;UsageStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;completed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;partial&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;failed&lt;/code&gt; events are always written to Postgres — you want the &lt;br&gt;
audit trail — but never reported to Stripe by default. If you &lt;br&gt;
need to bill for certain failure modes, add them to &lt;br&gt;
&lt;code&gt;BILLABLE_STATUSES&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The failed events in Postgres are also useful for debugging. &lt;br&gt;
If a specific endpoint has a high failure rate, you'll see it &lt;br&gt;
in the usage table before it becomes a support ticket.&lt;/p&gt;




&lt;h2&gt;
  
  
  The thing Stripe's docs don't say
&lt;/h2&gt;

&lt;p&gt;Stripe's usage-based billing documentation covers the happy path &lt;br&gt;
well. It doesn't cover what to do when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The same event arrives twice (retries)&lt;/li&gt;
&lt;li&gt;The event never fully completes (streaming, disconnects)
&lt;/li&gt;
&lt;li&gt;The event completes but represents a failure (errors)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These aren't edge cases in practice — they're normal operating &lt;br&gt;
conditions for any MCP server with real users.&lt;/p&gt;

&lt;p&gt;I ended up building all of this into a boilerplate &lt;br&gt;
(&lt;a href="https://mcp-billing.com" rel="noopener noreferrer"&gt;MCP-Billing&lt;/a&gt;) along with OAuth 2.1, &lt;br&gt;
API key management, and rate limiting. But the decisions above &lt;br&gt;
are framework-agnostic — you can apply them to any Stripe &lt;br&gt;
usage-based integration.&lt;/p&gt;

&lt;p&gt;The main takeaway: make the decisions explicit, document them in &lt;br&gt;
your README or changelog, and make &lt;code&gt;BILLABLE_STATUSES&lt;/code&gt; configurable &lt;br&gt;
from day one. You will change your mind about what's billable &lt;br&gt;
as you learn more about your users' behavior.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>typescript</category>
      <category>mcp</category>
      <category>stripe</category>
    </item>
    <item>
      <title>Why Most AI Prompts for Developers Don't Work (and How to Fix Them)</title>
      <dc:creator>Marc Gil</dc:creator>
      <pubDate>Thu, 09 Jul 2026 17:38:51 +0000</pubDate>
      <link>https://dev.to/marcgil_dev/why-most-ai-prompts-for-developers-dont-work-and-how-to-fix-them-5a9</link>
      <guid>https://dev.to/marcgil_dev/why-most-ai-prompts-for-developers-dont-work-and-how-to-fix-them-5a9</guid>
      <description>&lt;p&gt;You've seen the lists. "100 ChatGPT Prompts for Developers." "Ultimate Prompt Pack for Coders." You try one, get a generic response that doesn't understand your stack, your codebase, or your actual problem — and go back to writing the prompt yourself from scratch.&lt;/p&gt;

&lt;p&gt;The issue isn't the AI. It's that most prompts shared online were written by people who don't actually write code. They're optimized for looking comprehensive, not for producing useful output in a real codebase.&lt;/p&gt;

&lt;p&gt;Here's what's actually going wrong, and how to fix it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The three failure modes of generic dev prompts
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. No context anchoring&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A prompt like "review my code and suggest improvements" gives Claude or GPT-4 nothing to work with. The model doesn't know your language, your framework, whether you care about performance or readability, or what "improvement" means in your context. It produces a generic response because it received a generic input.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. No output structure&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When the AI doesn't know what format you need, it invents one — and it's usually a wall of text that mixes critical issues with stylistic suggestions with refactoring ideas. You end up reading the whole thing to find the one insight you actually needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. No variables to customize&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A prompt that works for a Python FastAPI backend won't work unchanged for a TypeScript monorepo. Generic prompts treat all codebases as equivalent. They're not.&lt;/p&gt;




&lt;h2&gt;
  
  
  What a prompt that actually works looks like
&lt;/h2&gt;

&lt;p&gt;Here's a concrete example. This is the code review prompt I use in my daily workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;You are a senior software engineer conducting a thorough code review.

Context:
&lt;span class="p"&gt;-&lt;/span&gt; Language/Framework: [LANGUAGE_AND_FRAMEWORK]
&lt;span class="p"&gt;-&lt;/span&gt; Project type: [PROJECT_TYPE]
&lt;span class="p"&gt;-&lt;/span&gt; Review focus: [FOCUS: security / performance / readability / all]

Code to review:
[PASTE_CODE_HERE]

Additional context (optional):
[CONTEXT: recent changes, known constraints, team conventions]

Provide:
&lt;span class="p"&gt;1.&lt;/span&gt; A severity-labeled issue list (Critical / Major / Minor / Suggestion)
&lt;span class="p"&gt;2.&lt;/span&gt; For each Critical and Major issue: explain why it's a problem and provide corrected code
&lt;span class="p"&gt;3.&lt;/span&gt; A summary score (1–10) with one-sentence justification
&lt;span class="p"&gt;4.&lt;/span&gt; Top 3 actionable improvements for the next iteration
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The differences from a generic prompt:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;[LANGUAGE_AND_FRAMEWORK]&lt;/code&gt; forces you to specify context before pasting anything — which also forces the model to apply language-specific knowledge&lt;/li&gt;
&lt;li&gt;The severity labels structure the output before it's generated, so you get a scannable list instead of paragraphs&lt;/li&gt;
&lt;li&gt;"For each Critical and Major issue: provide corrected code" means you get working code, not a description of what to change&lt;/li&gt;
&lt;li&gt;The summary score gives you a baseline to track improvement across PRs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's what this produces with a real piece of code — a Python FastAPI endpoint with a SQL injection vulnerability:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CRITICAL [SQL Injection] — Line 12
The query uses direct string interpolation with user input:
query = f"SELECT * FROM users WHERE email = '{email}'"
This allows arbitrary SQL execution. Fix:
result = db.execute(
    text("SELECT * FROM users WHERE email = :email"),
    {"email": email}
)

MAJOR [Missing authentication] — Line 8
The endpoint has no authentication decorator. Any unauthenticated request 
can access user data. Add @require_auth or equivalent middleware.

MINOR [No input validation] — Line 6
Email input is not validated before use. Add:
if not re.match(r"[^@]+@[^@]+\.[^@]+", email):
    raise HTTPException(status_code=422, detail="Invalid email format")

Summary score: 4/10 — Critical security vulnerability makes this 
unshippable in current state.

Top 3 next steps:
1. Migrate all raw queries to parameterized statements
2. Add authentication middleware at router level
3. Implement Pydantic input validation models
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's directly actionable. The "review my code" prompt wouldn't get you there.&lt;/p&gt;




&lt;h2&gt;
  
  
  The pattern that makes prompts reusable
&lt;/h2&gt;

&lt;p&gt;Every prompt that actually holds up in daily use has three components:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Role + context&lt;/strong&gt; — Tell the model who it is and what it's looking at. "You are a senior software engineer" isn't fluff — it activates a different response distribution than "help me with this code." Adding framework and project type narrows it further.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Structured variables&lt;/strong&gt; — Anything that changes between uses goes in &lt;code&gt;[BRACKETS]&lt;/code&gt;. This turns a one-time prompt into a reusable template. You fill in the variables, paste your code, and get consistent output structure every time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output specification&lt;/strong&gt; — Define the format before the model generates it. Numbered lists, severity labels, score, corrected code — specify exactly what you need. If you don't, the model optimizes for completeness, not usability.&lt;/p&gt;




&lt;h2&gt;
  
  
  A second example: debugging with stack trace context
&lt;/h2&gt;

&lt;p&gt;This one saves the most time in practice. Instead of "why is this failing," you give the model everything it needs to diagnose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;You are debugging a production issue in a [LANGUAGE] application.

Error and stack trace:
[PASTE_FULL_STACK_TRACE]

Relevant code section:
[PASTE_CODE]

Environment context:
&lt;span class="p"&gt;-&lt;/span&gt; Runtime version: [VERSION]
&lt;span class="p"&gt;-&lt;/span&gt; Recent changes: [WHAT_CHANGED_RECENTLY]
&lt;span class="p"&gt;-&lt;/span&gt; Frequency: [always / intermittent / under specific conditions]

Provide:
&lt;span class="p"&gt;1.&lt;/span&gt; Most likely root cause (with confidence: high / medium / low)
&lt;span class="p"&gt;2.&lt;/span&gt; Why this error occurs at this specific point in the stack
&lt;span class="p"&gt;3.&lt;/span&gt; Fix with code, not just description
&lt;span class="p"&gt;4.&lt;/span&gt; One test to verify the fix works
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The "recent changes" and "frequency" fields are what make this work. Intermittent errors that only happen under load have different root causes than errors that happen every time. Without that context, the model guesses. With it, the diagnosis is usually right on the first try.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why this matters more now than six months ago
&lt;/h2&gt;

&lt;p&gt;Models have gotten significantly better at following structured instructions. GPT-4o and Claude 3.5+ will reliably produce the exact output format you specify if your prompt is precise enough. The ceiling on prompt quality has gone up — which means the gap between a well-structured prompt and a generic one is now larger, not smaller.&lt;/p&gt;

&lt;p&gt;Developers who invest in building reusable, structured prompts for their specific workflows are getting qualitatively better AI assistance than those using prompts from generic lists.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where to go from here
&lt;/h2&gt;

&lt;p&gt;The two prompts above are from a larger set I built for my own dev workflow: 38 prompts covering code review, debugging, documentation, git workflows, architecture decisions, and testing — each with the same structure: role, variables, output spec, and a real output example generated from actual code.&lt;/p&gt;

&lt;p&gt;If you want the full set: &lt;a href="https://gilmarc4.gumroad.com/l/devprompt-pro" rel="noopener noreferrer"&gt;DevPrompt Pro on Gumroad&lt;/a&gt; — $19.&lt;/p&gt;

&lt;p&gt;If you want to build your own, the pattern in this article is the starting point. The most important habit: every time you write a prompt that produces a genuinely useful output, turn it into a template before you close the tab.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built this for my own use as a developer. Happy to discuss specific prompt structures in the comments — if you have a workflow that's hard to prompt for, drop it below.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>productivity</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
