<?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: Virginia Nyambura  Mwega</title>
    <description>The latest articles on DEV Community by Virginia Nyambura  Mwega (@virginiamwega2svg).</description>
    <link>https://dev.to/virginiamwega2svg</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%2F4005611%2Faf72765f-e3ee-4aff-8c58-be369191fedb.jpeg</url>
      <title>DEV Community: Virginia Nyambura  Mwega</title>
      <link>https://dev.to/virginiamwega2svg</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/virginiamwega2svg"/>
    <language>en</language>
    <item>
      <title>How to Cancel an OpenAI API Request with AbortSignal in Node.js</title>
      <dc:creator>Virginia Nyambura  Mwega</dc:creator>
      <pubDate>Wed, 28 Oct 2026 12:15:24 +0000</pubDate>
      <link>https://dev.to/virginiamwega2svg/how-to-cancel-an-openai-api-request-with-abortsignal-in-nodejs-3h0i</link>
      <guid>https://dev.to/virginiamwega2svg/how-to-cancel-an-openai-api-request-with-abortsignal-in-nodejs-3h0i</guid>
      <description>&lt;p&gt;A timeout and a cancellation signal are related, but they are not exactly the same thing.&lt;/p&gt;

&lt;p&gt;A timeout is a limit on how long an operation should wait. Cancellation is a way to tell an operation that its result is no longer needed.&lt;/p&gt;

&lt;p&gt;For example, an interactive application may abandon a request because a user navigated away, cancelled a task, or no longer needs the generated response. A background process may also have a total deadline that should apply to the whole operation rather than just one network attempt.&lt;/p&gt;

&lt;p&gt;The OpenAI JavaScript SDK supports passing an &lt;code&gt;AbortSignal&lt;/code&gt; through the request options. Here is a practical example.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Create an abort controller
&lt;/h2&gt;

&lt;p&gt;Node.js provides &lt;code&gt;AbortController&lt;/code&gt;, which exposes a signal that can be used to cancel compatible asynchronous operations.&lt;/p&gt;

&lt;p&gt;The OpenAI JavaScript SDK accepts the signal through the request options object.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;OpenAI&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;openai&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;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="p"&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;controller&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AbortController&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;deadline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&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="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;try&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;response&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="nx"&gt;responses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;YOUR_MODEL_ID&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Explain why request cancellation matters in API clients.&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;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;output_text&lt;/span&gt;&lt;span class="p"&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;span class="nx"&gt;error&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;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aborted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Request cancelled or overall deadline reached.&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="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;deadline&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;Replace &lt;code&gt;YOUR_MODEL_ID&lt;/code&gt; with a model available to your project. The example also requires a valid API key in &lt;code&gt;OPENAI_API_KEY&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;finally&lt;/code&gt; block clears the timer whether the request succeeds, fails, or is cancelled.&lt;/p&gt;

&lt;p&gt;SDK reference: &lt;a href="https://github.com/openai/openai-node/blob/main/docs/configuration.md" rel="noopener noreferrer"&gt;https://github.com/openai/openai-node/blob/main/docs/configuration.md&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Understand what the deadline means
&lt;/h2&gt;

&lt;p&gt;The example starts a 15-second timer that aborts the operation.&lt;/p&gt;

&lt;p&gt;This is useful when an application needs a total elapsed-time limit. The SDK also offers its own request &lt;code&gt;timeout&lt;/code&gt; setting.&lt;/p&gt;

&lt;p&gt;Those settings should not be confused.&lt;/p&gt;

&lt;p&gt;A configured SDK timeout governs an individual request attempt. Since the SDK can retry certain failures, the total elapsed time can exceed a single attempt's timeout.&lt;/p&gt;

&lt;p&gt;An application-level &lt;code&gt;AbortSignal&lt;/code&gt; can provide a cancellation boundary around the operation when the overall deadline matters.&lt;/p&gt;

&lt;p&gt;Choose the approach according to your needs. You may want an SDK timeout, an external cancellation signal, or both.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Distinguish cancellation from other errors
&lt;/h2&gt;

&lt;p&gt;The catch block checks whether the controller was aborted.&lt;/p&gt;

&lt;p&gt;If it was, the application can report that the operation was cancelled or its deadline was reached. Otherwise, the error is propagated.&lt;/p&gt;

&lt;p&gt;In a real application, you may want to distinguish user cancellation from deadline expiry by recording which event triggered the abort.&lt;/p&gt;

&lt;p&gt;Avoid silently treating every exception as cancellation. Authentication failures, invalid requests, rate limits, and provider errors still need appropriate handling.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Use cancellation with streaming too
&lt;/h2&gt;

&lt;p&gt;For a streamed request, pass the signal through the same request-options mechanism:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AbortController&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;stream&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="nx"&gt;responses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;YOUR_MODEL_ID&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Explain asynchronous iteration.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;stream&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;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;await &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;event&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;stream&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;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;response.output_text.delta&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="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the application needs a deadline or user-cancellation action, it can call &lt;code&gt;controller.abort()&lt;/code&gt; while the stream is being consumed. The consuming code should handle the resulting cancellation or error path.&lt;/p&gt;

&lt;p&gt;A web application should also connect cancellation to its own lifecycle—for example, stopping work when a result is no longer needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Common mistakes to avoid
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Leaving timers running:&lt;/strong&gt; Always clean up a manually created timer in a &lt;code&gt;finally&lt;/code&gt; block.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Treating every exception as cancellation:&lt;/strong&gt; Check the cancellation signal and preserve other errors for appropriate handling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Assuming the timeout covers every retry automatically:&lt;/strong&gt; Understand the difference between an attempt-level timeout and an overall deadline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Forgetting the UI or caller:&lt;/strong&gt; Cancelling the network request is only part of the feature. The application must also stop waiting, update its state, and prevent stale results from replacing a newer result.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Assuming cancellation proves that no work occurred:&lt;/strong&gt; A request may already have reached the server before the client cancels it. Cancellation should not be treated as proof that no processing or cost occurred.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Cancellation is an important part of an API client's contract. It lets applications stop waiting for work whose result is no longer useful and gives them a practical way to enforce operation deadlines.&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;AbortSignal&lt;/code&gt; deliberately, clean up timers and application state, and test the cancellation path separately from ordinary success and failure behavior.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reference:&lt;/strong&gt; &lt;a href="https://github.com/openai/openai-node/blob/main/docs/configuration.md" rel="noopener noreferrer"&gt;OpenAI Node.js SDK configuration&lt;/a&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>node</category>
      <category>ai</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Your AI Evaluation Can Pass Every Test and Still Mislead You</title>
      <dc:creator>Virginia Nyambura  Mwega</dc:creator>
      <pubDate>Fri, 23 Oct 2026 16:27:54 +0000</pubDate>
      <link>https://dev.to/virginiamwega2svg/your-ai-evaluation-can-pass-every-test-and-still-mislead-you-114b</link>
      <guid>https://dev.to/virginiamwega2svg/your-ai-evaluation-can-pass-every-test-and-still-mislead-you-114b</guid>
      <description>&lt;p&gt;A passing test suite can tell you that your evaluation code works.&lt;/p&gt;

&lt;p&gt;It cannot, by itself, tell you that your AI model produces reliable answers.&lt;/p&gt;

&lt;p&gt;That distinction has become important in my work on an OpenAI JavaScript SDK documentation practice project. I have been building an evaluation workflow that includes a rule-based baseline, pairwise comparisons, and an optional live-model judge runner.&lt;/p&gt;

&lt;p&gt;The experience has made one thing clear:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Testing the evaluation pipeline is not the same as validating the model.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here are four lessons I am taking from the process.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Separate pipeline correctness from answer quality
&lt;/h2&gt;

&lt;p&gt;An evaluation pipeline has its own software behavior to verify.&lt;/p&gt;

&lt;p&gt;You can test whether it processes inputs, applies comparison rules, produces results, and handles failures as expected.&lt;/p&gt;

&lt;p&gt;Those tests establish useful evidence about the implementation.&lt;/p&gt;

&lt;p&gt;But they do not prove that the underlying evaluation criteria are good measures of AI quality.&lt;/p&gt;

&lt;p&gt;For example, a rule that checks whether a response contains a specific phrase might work perfectly in every test while still accepting an incorrect answer or rejecting a correct one written differently.&lt;/p&gt;

&lt;p&gt;A passing test means the code met the condition being tested. It does not mean the condition captures every important aspect of answer quality.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Know what your evaluation actually measures
&lt;/h2&gt;

&lt;p&gt;A rule-based baseline is useful when the expected behavior is narrow and predictable.&lt;/p&gt;

&lt;p&gt;It becomes less informative when answers can be correct in several different ways, or when correctness depends on context.&lt;/p&gt;

&lt;p&gt;Before trusting an evaluation result, ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What behavior is being measured?&lt;/li&gt;
&lt;li&gt;What behavior is not being measured?&lt;/li&gt;
&lt;li&gt;Could a bad response pass?&lt;/li&gt;
&lt;li&gt;Could a good response fail?&lt;/li&gt;
&lt;li&gt;Do the examples represent the cases users will encounter?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These questions help expose the difference between a convenient metric and meaningful evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Label simulated and live results accurately
&lt;/h2&gt;

&lt;p&gt;My project includes a pairwise evaluation workflow and an optional live-judge runner.&lt;/p&gt;

&lt;p&gt;These serve different purposes.&lt;/p&gt;

&lt;p&gt;A simulated comparison can help test whether the evaluation workflow processes comparisons as expected. It is useful for developing and checking the software without relying on live model calls.&lt;/p&gt;

&lt;p&gt;However, simulated results do not establish how a real model judge will perform.&lt;/p&gt;

&lt;p&gt;If the live evaluation has not been run and validated, the correct report is that live-model validation remains unverified—not that the system has passed a live benchmark.&lt;/p&gt;

&lt;p&gt;This distinction makes technical documentation more trustworthy because readers can see what was actually tested.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Treat scores as evidence, not verdicts
&lt;/h2&gt;

&lt;p&gt;Pairwise evaluation can identify which of two responses performs better under a given criterion.&lt;/p&gt;

&lt;p&gt;But a relative winner is not automatically correct, safe, or production-ready.&lt;/p&gt;

&lt;p&gt;The conclusion depends on the criterion, the examples, the comparison method, and the reliability of the judge.&lt;/p&gt;

&lt;p&gt;A score without that context can create confidence that the underlying evidence does not justify.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical checklist for AI evaluation
&lt;/h2&gt;

&lt;p&gt;When documenting an evaluation workflow, I now find it useful to separate these questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt; Have I tested that the evaluation software behaves correctly?&lt;/li&gt;
&lt;li&gt; Have I explained what the evaluation criteria measure?&lt;/li&gt;
&lt;li&gt; Have I identified whether results are rule-based, simulated, or generated by a live model?&lt;/li&gt;
&lt;li&gt; Have I stated what the results do not prove?&lt;/li&gt;
&lt;li&gt; Have I avoided presenting unverified work as completed validation?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This checklist does not make an evaluation perfect. It makes the claims around it more precise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thought
&lt;/h2&gt;

&lt;p&gt;AI evaluation is not just about getting a score. It is about understanding how that score was produced and what conclusions it can reasonably support.&lt;/p&gt;

&lt;p&gt;A green test suite is useful evidence about software behavior. A benchmark can provide evidence about model behavior. We should not confuse one for the other.&lt;/p&gt;

&lt;p&gt;I am continuing to document the implementation and its limitations in my &lt;a href="https://github.com/virginiamwega2-svg/openai-sdk-docs-practice" rel="noopener noreferrer"&gt;OpenAI SDK Documentation Practice repository&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What checks do you use to decide whether an AI evaluation result is trustworthy?&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>testing</category>
      <category>machinelearning</category>
      <category>javascript</category>
    </item>
    <item>
      <title>How I Tested and Fixed Sensitive Exception Data in OpenTelemetry</title>
      <dc:creator>Virginia Nyambura  Mwega</dc:creator>
      <pubDate>Wed, 21 Oct 2026 15:24:31 +0000</pubDate>
      <link>https://dev.to/virginiamwega2svg/how-i-tested-and-fixed-sensitive-exception-data-in-opentelemetry-1g4f</link>
      <guid>https://dev.to/virginiamwega2svg/how-i-tested-and-fixed-sensitive-exception-data-in-opentelemetry-1g4f</guid>
      <description>&lt;p&gt;I'm building an OpenAI Node.js SDK practice project to explore API integration, testing, documentation, and LLM observability.&lt;/p&gt;

&lt;p&gt;One issue I investigated was how my observability wrapper recorded exceptions. The original code called:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;recordException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In my local tracing demonstration, that resulted in an exported exception event containing a synthetic marker embedded in the error message. It wasn't production data, but it gave me a reproducible example of a privacy risk.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproduce before fixing
&lt;/h2&gt;

&lt;p&gt;I added a regression test with a fake marker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sensitiveMarker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PRIVATE_PROMPT_DO_NOT_LOG&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;expectedError&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s2"&gt;`Provider failure; prompt=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;sensitiveMarker&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The test executes the failing operation, flushes the test exporter, and checks the exported span attributes and events. It also confirms that the original error object is still rethrown.&lt;/p&gt;

&lt;p&gt;Before the fix, the assertion failed because the marker was present in the exported telemetry.&lt;/p&gt;

&lt;h2&gt;
  
  
  Change the event, not the failure behaviour
&lt;/h2&gt;

&lt;p&gt;I replaced the raw exception recording with a generic event:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;exception&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;exception.type&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="s2"&gt;Error&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;p&gt;The wrapper still sets the span's status to error and rethrows the original exception. This removes the original message and stack trace from the exception event created by this code path while keeping a generic failure signal.&lt;/p&gt;

&lt;p&gt;The trade-off is reduced diagnostic detail. In this practice implementation, the error type is deliberately generic. A more mature system could investigate safe, allowlisted error categories, while ensuring that user input and sensitive data are not embedded in those categories.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify the change
&lt;/h2&gt;

&lt;p&gt;The privacy regression test passed after the fix, and the full local test suite passed 57 tests with no failures. The project also runs its automated tests, an OpenTelemetry smoke test, and a documentation-link checker in GitHub Actions.&lt;/p&gt;

&lt;p&gt;The tracing example uses mocked SDK responses and a console exporter. It does not prove production telemetry safety or live API behaviour. The fix is not general-purpose redaction: other logging paths and exporters need separate review.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code and evidence:&lt;/strong&gt; &lt;a href="https://github.com/virginiamwega2-svg/openai-sdk-docs-practice" rel="noopener noreferrer"&gt;openai-sdk-docs-practice on GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For background, OpenTelemetry's exception conventions note that exception messages can contain sensitive information: &lt;a href="https://opentelemetry.io/docs/specs/semconv/exceptions/exceptions-spans/" rel="noopener noreferrer"&gt;OpenTelemetry exception conventions&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;How do you decide which exception details to keep in telemetry by default?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>javascript</category>
      <category>observability</category>
      <category>testing</category>
    </item>
    <item>
      <title>My First OpenAI Streaming Example: The Code, the Tests, and the Edge Cases</title>
      <dc:creator>Virginia Nyambura  Mwega</dc:creator>
      <pubDate>Thu, 15 Oct 2026 14:45:02 +0000</pubDate>
      <link>https://dev.to/virginiamwega2svg/my-first-openai-streaming-example-the-code-the-tests-and-the-edge-cases-281c</link>
      <guid>https://dev.to/virginiamwega2svg/my-first-openai-streaming-example-the-code-the-tests-and-the-edge-cases-281c</guid>
      <description>&lt;p&gt;I wanted to understand AI streaming by building it—not just reading about it.&lt;/p&gt;

&lt;p&gt;So I added a streaming command to my OpenAI SDK practice project using JavaScript and the OpenAI Responses API.&lt;/p&gt;

&lt;p&gt;The goal was simple: instead of waiting for the entire AI response, display text fragments as they arrive.&lt;/p&gt;

&lt;p&gt;Along the way, I got to practise something I care about just as much as the implementation: making an example understandable, testable, and useful to the next developer.&lt;/p&gt;

&lt;p&gt;Here is what I built and what I learned.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Project:&lt;/strong&gt; &lt;a href="https://github.com/virginiamwega2-svg/openai-sdk-docs-practice" rel="noopener noreferrer"&gt;OpenAI SDK Docs Practice on GitHub&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The streaming function
&lt;/h2&gt;

&lt;p&gt;The core implementation is a small asynchronous function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;streamText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onText&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="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Prompt is required.&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;onText&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;function&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="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;A text callback is required.&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stream&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="nx"&gt;responses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MODEL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;stream&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;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;await &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;event&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;stream&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;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;response.output_text.delta&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="nf"&gt;onText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;MODEL&lt;/code&gt; is defined elsewhere in the project.&lt;/p&gt;

&lt;p&gt;A few things are happening here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I validate the prompt before making the request.&lt;/li&gt;
&lt;li&gt;I validate the callback so the caller has a way to receive text.&lt;/li&gt;
&lt;li&gt;I enable streaming in the API request.&lt;/li&gt;
&lt;li&gt;I consume the asynchronous event stream.&lt;/li&gt;
&lt;li&gt;I forward only text-delta events to the callback.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the command-line interface, the callback uses &lt;code&gt;process.stdout.write()&lt;/code&gt; to display each fragment as it arrives.&lt;/p&gt;

&lt;p&gt;To run the example with a valid API key configured locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;index.js&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Explain how an API works"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The project uses &lt;code&gt;OPENAI_API_KEY&lt;/code&gt; from a local &lt;code&gt;.env&lt;/code&gt; file. Never commit a real API key to your repository.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. What I decided to test
&lt;/h2&gt;

&lt;p&gt;I didn't want to stop at checking that the function returned without throwing.&lt;/p&gt;

&lt;p&gt;Because streaming is event-driven, I wanted to check what happened as different events arrived.&lt;/p&gt;

&lt;p&gt;I created a mocked client that yields simulated events, including two text fragments and a completion event.&lt;/p&gt;

&lt;p&gt;The test verifies that the received fragments are:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Hello&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="s2"&gt;, developer!&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Joining them produces:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Hello, developer!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The completion event isn't forwarded to the text callback.&lt;/p&gt;

&lt;p&gt;I also added coverage for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ignoring non-text events&lt;/li&gt;
&lt;li&gt;Rejecting an empty prompt&lt;/li&gt;
&lt;li&gt;Rejecting a missing callback&lt;/li&gt;
&lt;li&gt;Propagating an SDK error when stream startup fails&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This makes the tests repeatable without depending on a network connection or consuming API usage.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The result—and an important limitation
&lt;/h2&gt;

&lt;p&gt;The latest reported run of the project's automated test suite was:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;19 passed, 0 failed.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That is a useful milestone, but I want to be precise about what it means.&lt;/p&gt;

&lt;p&gt;The streaming tests use mocked events. I haven't tested a live API stream because I don't currently have an API key.&lt;/p&gt;

&lt;p&gt;The tests verify my application's behavior with the events and failures I simulate. They don't prove that a real provider request succeeds or that the implementation handles every possible interruption.&lt;/p&gt;

&lt;p&gt;My next improvements would include testing a stream interrupted after some text has already arrived, defining how partial output should be reported, and verifying a live integration when credentials are available.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The documentation is part of the feature
&lt;/h2&gt;

&lt;p&gt;I also documented the implementation in &lt;code&gt;STREAMING-COOKBOOK.md&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It covers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prerequisites&lt;/li&gt;
&lt;li&gt;How the streaming function works&lt;/li&gt;
&lt;li&gt;The command to run&lt;/li&gt;
&lt;li&gt;How to execute the automated tests&lt;/li&gt;
&lt;li&gt;Common setup and troubleshooting issues&lt;/li&gt;
&lt;li&gt;What has and hasn't been verified&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This exercise reminded me that a code example is much more useful when someone else can follow it without guessing.&lt;/p&gt;

&lt;p&gt;A working snippet alone doesn't explain how to configure it, test it, or diagnose a failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. What I learned about Developer Experience
&lt;/h2&gt;

&lt;p&gt;The biggest lesson wasn't simply how to use &lt;code&gt;stream: true&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It was how much of the developer experience lives around the API call:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Validate → Request → Consume events → Handle errors → Test → Document&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every step gives the next developer more confidence and fewer surprises.&lt;/p&gt;

&lt;p&gt;I'm building these examples as part of my journey into AI Developer Documentation and Developer Experience. I want to be able to build the integration, understand its limitations, and explain it clearly—not just write a tutorial around code I haven't tested.&lt;/p&gt;

&lt;p&gt;If you'd like to explore the project, here's the repository:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/virginiamwega2-svg/openai-sdk-docs-practice" rel="noopener noreferrer"&gt;OpenAI SDK Docs Practice&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  I'd love to learn from other developers
&lt;/h3&gt;

&lt;p&gt;When you're implementing a streaming API, which edge case do you make sure to test first?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A failure before the stream starts&lt;/li&gt;
&lt;li&gt;A connection dropping halfway through&lt;/li&gt;
&lt;li&gt;Cancellation when the user leaves the page&lt;/li&gt;
&lt;li&gt;Handling partial output after a failure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'd be interested to hear how you approach it.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Verification note:&lt;/strong&gt; Automated tests passed in the latest reported run, using mocked streaming events. Live API streaming has not yet been verified.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>ai</category>
      <category>beginners</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Your API Can Work Perfectly and Still Deliver a Bad Developer Experience</title>
      <dc:creator>Virginia Nyambura  Mwega</dc:creator>
      <pubDate>Thu, 08 Oct 2026 16:31:42 +0000</pubDate>
      <link>https://dev.to/virginiamwega2svg/your-api-can-work-perfectly-and-still-deliver-a-bad-developer-experience-3ilp</link>
      <guid>https://dev.to/virginiamwega2svg/your-api-can-work-perfectly-and-still-deliver-a-bad-developer-experience-3ilp</guid>
      <description>&lt;p&gt;Your API Can Work Perfectly and Still Deliver a Bad Developer Experience&lt;/p&gt;

&lt;p&gt;I thought I was testing an API. I was actually testing everything around it.&lt;/p&gt;

&lt;p&gt;An API can work perfectly and still frustrate developers.&lt;/p&gt;

&lt;p&gt;The server starts.&lt;/p&gt;

&lt;p&gt;The endpoint responds.&lt;/p&gt;

&lt;p&gt;The tests pass.&lt;/p&gt;

&lt;p&gt;And somehow, someone still gets stuck.&lt;/p&gt;

&lt;p&gt;I discovered this while building a small Express API for a Developer Experience experiment.&lt;/p&gt;

&lt;p&gt;Instead of stopping when the API worked, I decided to use it like a developer discovering the project for the first time.&lt;/p&gt;

&lt;p&gt;I didn't rely on what I &lt;em&gt;knew&lt;/em&gt; about the project.&lt;/p&gt;

&lt;p&gt;I followed what a new developer would actually see.&lt;/p&gt;

&lt;p&gt;I read the README.&lt;/p&gt;

&lt;p&gt;I ran the commands.&lt;/p&gt;

&lt;p&gt;I copied the examples.&lt;/p&gt;

&lt;p&gt;I tested successful requests.&lt;/p&gt;

&lt;p&gt;I sent invalid requests.&lt;/p&gt;

&lt;p&gt;I read the errors.&lt;/p&gt;

&lt;p&gt;And I compared the documentation against the actual application.&lt;/p&gt;

&lt;p&gt;That's when I started finding friction.&lt;/p&gt;




&lt;p&gt;The API Was Simple&lt;/p&gt;

&lt;p&gt;The API only had two endpoints:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;```text id="a7u1m2"&lt;br&gt;
GET  /hello&lt;br&gt;
POST /hello&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;


A request to `GET /hello` returns:



```json id="h6p9r3"
{
  "message": "Hello, Developer!"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The POST endpoint accepts:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;```json id="v2k8q4"&lt;br&gt;
{&lt;br&gt;
  "name": "Virginia"&lt;br&gt;
}&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;


and returns:



```json id="j5s1d8"
{
  "message": "Hello, Virginia!"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;For an invalid request, it returns:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;```json id="n3x7b5"&lt;br&gt;
{&lt;br&gt;
  "error": "Name is required"&lt;br&gt;
}&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;


It's a deliberately small API.

But that made it easier to focus on something I was becoming more interested in:

What does it actually feel like to use this API?

---

 First Problem: The README Told Me to Run a Command That Didn't Exist

The README instructed developers to run:



```bash id="q4w6e2"
npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Except there was no &lt;code&gt;dev&lt;/code&gt; script.&lt;/p&gt;

&lt;p&gt;That meant the documented onboarding path failed immediately.&lt;/p&gt;

&lt;p&gt;This was interesting because the documentation itself looked reasonable.&lt;/p&gt;

&lt;p&gt;The problem only became obvious when I actually followed it.&lt;/p&gt;

&lt;p&gt;I fixed it by adding:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;```json id="r8t2k6"&lt;br&gt;
{&lt;br&gt;
  "scripts": {&lt;br&gt;
    "dev": "tsx watch src/server.ts",&lt;br&gt;
    "test": "vitest"&lt;br&gt;
  }&lt;br&gt;
}&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;


Now the command in the README matched the actual project.

That gave me my first real lesson:

Documentation has to be tested, not just written.**

---

 Second Problem: The API Worked, But the Example Didn't

The original request example used Unix-style `curl` syntax.

I was testing the project in Windows PowerShell.

The API wasn't broken.

The example was.

That's an easy problem to overlook when you're the person who built the API.

You already know how everything works.

A new developer doesn't.

So I replaced the example with a PowerShell-friendly request:



```powershell id="p1m4x7"
Invoke-RestMethod -Uri "http://localhost:3000/hello" -Method Post -ContentType "application/json" -Body '{"name":"Virginia"}'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This was a small change, but it made me think more carefully about developer environments.&lt;/p&gt;

&lt;p&gt;A command isn't useful simply because it's technically correct.&lt;/p&gt;

&lt;p&gt;The developer has to be able to &lt;strong&gt;copy it, run it, and understand what happens next.&lt;/strong&gt;&lt;/p&gt;



&lt;p&gt;Third Problem: The Happy Path Isn't Enough&lt;/p&gt;

&lt;p&gt;Documentation often focuses on the successful request.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;```json id="c8v3n1"&lt;br&gt;
{&lt;br&gt;
  "name": "Virginia"&lt;br&gt;
}&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;


But what happens when the developer sends:



```json id="k2f7q9"
{}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The API returns:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;```json id="d6r1s5"&lt;br&gt;
{&lt;br&gt;
  "error": "Name is required"&lt;br&gt;
}&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;


That error is part of the developer experience.

It's not just an implementation detail.

It tells the developer:

* what went wrong
* which input matters
* what the API expects

This made me think about API documentation differently.

Sometimes the best documentation isn't another paragraph.

Sometimes it's a good error message.

---

 Tests Became Documentation Insurance

After manually testing the API, I wanted a repeatable way to verify its behavior.

So I added automated tests using **Vitest** and **Supertest**.

The final suite covered:

* successful GET request
* successful POST request
* invalid `name`
* missing `name`

Result:



```text id="w5n8c2"
Test Files  1 passed
Tests       4 passed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This gave me confidence that the behavior I was documenting was behavior the application actually supported.&lt;/p&gt;

&lt;p&gt;It also made me think about documentation drift.&lt;/p&gt;

&lt;p&gt;An API can change.&lt;/p&gt;

&lt;p&gt;The README might not.&lt;/p&gt;

&lt;p&gt;An error message might change.&lt;/p&gt;

&lt;p&gt;The documentation might not.&lt;/p&gt;

&lt;p&gt;A response format might change.&lt;/p&gt;

&lt;p&gt;The example might not.&lt;/p&gt;

&lt;p&gt;Tests can help catch some of that gap.&lt;/p&gt;

&lt;p&gt;So I started thinking of tests as a kind of:&lt;/p&gt;

&lt;p&gt;documentation insurance&lt;/p&gt;

&lt;p&gt;Not a replacement for documentation.&lt;/p&gt;

&lt;p&gt;A safeguard against documentation silently becoming outdated.&lt;/p&gt;



&lt;p&gt;Then I Added OpenAPI&lt;/p&gt;

&lt;p&gt;Once the API behavior was stable, I documented it using &lt;strong&gt;OpenAPI&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The specification describes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;GET /hello&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;POST /hello&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;request body requirements&lt;/li&gt;
&lt;li&gt;successful responses&lt;/li&gt;
&lt;li&gt;validation errors&lt;/li&gt;
&lt;li&gt;operation IDs&lt;/li&gt;
&lt;li&gt;the local development server&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I also validated the specification with &lt;strong&gt;Redocly&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This gave the project another useful layer:&lt;/p&gt;

&lt;p&gt;machine-readable API documentation.&lt;/p&gt;

&lt;p&gt;Instead of having the implementation and prose documentation as the only sources of information, the API also had a structured specification.&lt;/p&gt;

&lt;p&gt;That matters when you're building developer-facing products.&lt;/p&gt;

&lt;p&gt;Developers don't all consume documentation the same way.&lt;/p&gt;

&lt;p&gt;Some want a QuickStart.&lt;/p&gt;

&lt;p&gt;Some want endpoint references.&lt;/p&gt;

&lt;p&gt;Some want examples.&lt;/p&gt;

&lt;p&gt;Some want machine-readable API definitions.&lt;/p&gt;

&lt;p&gt;Good developer documentation needs to support the journey, not just describe the technology.&lt;/p&gt;



&lt;p&gt;Docs-as-Code Started Making More Sense&lt;/p&gt;

&lt;p&gt;The project eventually included:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;QuickStart documentation&lt;/li&gt;
&lt;li&gt;API reference information&lt;/li&gt;
&lt;li&gt;troubleshooting guidance&lt;/li&gt;
&lt;li&gt;a DevEx audit&lt;/li&gt;
&lt;li&gt;automated tests&lt;/li&gt;
&lt;li&gt;OpenAPI documentation&lt;/li&gt;
&lt;li&gt;Redocly validation&lt;/li&gt;
&lt;li&gt;GitHub Actions CI&lt;/li&gt;
&lt;li&gt;a case study documenting the investigation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I also used Git branches and pull requests to make documentation changes.&lt;/p&gt;

&lt;p&gt;That changed how I think about &lt;strong&gt;docs-as-code&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It isn't simply:&lt;/p&gt;

&lt;p&gt;"Put Markdown in Git."&lt;/p&gt;

&lt;p&gt;It's treating developer-facing information as part of the product development process.&lt;/p&gt;

&lt;p&gt;Write it.&lt;/p&gt;

&lt;p&gt;Review it.&lt;/p&gt;

&lt;p&gt;Test it.&lt;/p&gt;

&lt;p&gt;Validate it.&lt;/p&gt;

&lt;p&gt;Maintain it.&lt;/p&gt;

&lt;p&gt;Verify it.&lt;/p&gt;



&lt;p&gt;The Developer Experience Loop&lt;/p&gt;

&lt;p&gt;The biggest thing I took away from this project wasn't a particular tool.&lt;/p&gt;

&lt;p&gt;It was a workflow.&lt;/p&gt;

&lt;p&gt;I started thinking about Developer Experience like this:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;```text id="s3j7p1"&lt;br&gt;
Build&lt;br&gt;
  ↓&lt;br&gt;
Document&lt;br&gt;
  ↓&lt;br&gt;
Use&lt;br&gt;
  ↓&lt;br&gt;
Find friction&lt;br&gt;
  ↓&lt;br&gt;
Improve&lt;br&gt;
  ↓&lt;br&gt;
Test&lt;br&gt;
  ↓&lt;br&gt;
Verify&lt;br&gt;
  ↓&lt;br&gt;
Repeat&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;


That changes the question.

Instead of asking:

 "Did I write the documentation?"

I want to ask:

 "Can a developer actually make progress with it?"

That is a much more useful question.

---

 My New Definition of "Done"

Before calling API documentation finished, I'd now check:

 Can a developer get started?

The setup instructions should work from a fresh environment.

 Can they run the examples?

A developer shouldn't have to rewrite your commands before they work.

 Do the examples match reality?

The documentation should reflect the actual API.

 Are errors understandable?

Developers should have enough information to know what to fix.

 Is there a path when something goes wrong?

Troubleshooting shouldn't begin and end with "check your setup."

 Is important behavior tested?

Automated tests can help protect documented behavior.

 Does the API specification match the implementation?

OpenAPI shouldn't become a second, outdated version of the API.

 Has someone actually followed the documentation?

This might be the most important one.

Because documentation can look excellent until someone actually tries to use it.

---

 The Shift

This project changed the question I ask about documentation.

Before:

 "Is this documentation accurate?"**

Now:

 "Does this documentation help the developer succeed?"**

Those aren't the same thing.

A page can be technically accurate and still be difficult to use.

A command can be correct and still fail in someone's shell.

An API can return the right status code and still provide an unhelpful error.

A README can contain everything a developer needs and still leave them thinking:

 "Okay... what do I do now?"

That gap is where Developer Experience lives.

---

 The Best Way to Find Developer Friction?

Become the developer.

Don't just review your README.

Follow it.

Don't just look at your example.

Run it.

Don't just document your errors.

Break the API.

Don't just say the tests pass.

Use them to verify the behavior you promise developers.

That shift in perspective uncovered more issues for me than simply reading through the code.

---

 What This Small Project Taught Me

I started this project thinking I was building an API.

I ended up learning how much of the developer experience exists outside the API itself.

The code has to work.

The documentation has to work.

The examples have to work.

The errors have to help.

The tests have to verify.

And the journey has to make sense.

That's the standard I want to carry into the next things I build and document, especially around APIs, SDKs, AI integrations, and developer tools.

Because developers don't experience your codebase directly.

They experience the path you create around it.

And that path is part of the product.

 Build it. Use it. Break it. Document it. Improve it. Verify it.

---

 One question for the community

What's a small piece of developer friction you've encountered that had a much bigger impact than you expected?

I'd love to hear what you've seen.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>devex</category>
      <category>api</category>
      <category>documentation</category>
      <category>typescript</category>
    </item>
    <item>
      <title>What Building an API Taught Me About Developer Experience</title>
      <dc:creator>Virginia Nyambura  Mwega</dc:creator>
      <pubDate>Tue, 06 Oct 2026 12:18:33 +0000</pubDate>
      <link>https://dev.to/virginiamwega2svg/what-building-an-api-taught-me-about-developer-experience-51i0</link>
      <guid>https://dev.to/virginiamwega2svg/what-building-an-api-taught-me-about-developer-experience-51i0</guid>
      <description>&lt;p&gt;I thought I was testing an API.&lt;/p&gt;

&lt;p&gt;I was actually testing the &lt;strong&gt;developer experience&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I built a small Express API with two endpoints:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET  /hello
POST /hello
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API itself was intentionally simple.&lt;/p&gt;

&lt;p&gt;What interested me was what happened when I stopped looking at it as the person who built it and started using it as a developer discovering the project for the first time.&lt;/p&gt;

&lt;p&gt;I didn't just ask:&lt;/p&gt;

&lt;p&gt;"Does the code work?"&lt;/p&gt;

&lt;p&gt;I started asking:&lt;/p&gt;

&lt;p&gt;"Can another developer actually use this without getting stuck?"&lt;/p&gt;

&lt;p&gt;That changed the way I looked at the project.&lt;/p&gt;

&lt;p&gt;I started with the README&lt;/p&gt;

&lt;p&gt;The README included the usual setup instructions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looks fine, right?&lt;/p&gt;

&lt;p&gt;Except there was a problem.&lt;/p&gt;

&lt;p&gt;The project didn't actually have a &lt;code&gt;dev&lt;/code&gt; script.&lt;/p&gt;

&lt;p&gt;The documentation was telling developers to run a command that the project couldn't execute.&lt;/p&gt;

&lt;p&gt;That was my first reminder:&lt;/p&gt;

&lt;p&gt;Documentation is a promise.&lt;/p&gt;

&lt;p&gt;If I tell a developer to run a command, that command needs to work.&lt;/p&gt;

&lt;p&gt;I fixed the issue by adding &lt;code&gt;tsx&lt;/code&gt; and configuring the development script:&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;"scripts"&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;"dev"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tsx watch src/server.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"test"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vitest"&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;After that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;successfully started the server.&lt;/p&gt;

&lt;p&gt;A small fix, but an important lesson:&lt;/p&gt;

&lt;p&gt;A documentation review isn't complete until the documented steps have actually been executed.&lt;/p&gt;

&lt;p&gt;Then I discovered a portability problem&lt;/p&gt;

&lt;p&gt;Next, I tested the POST request example from Windows PowerShell.&lt;/p&gt;

&lt;p&gt;My original example used Unix-style &lt;code&gt;curl&lt;/code&gt; syntax.&lt;/p&gt;

&lt;p&gt;It didn't behave as expected.&lt;/p&gt;

&lt;p&gt;The API wasn't broken.&lt;/p&gt;

&lt;p&gt;My documentation was.&lt;/p&gt;

&lt;p&gt;I had written an example without considering the environment in which another developer might run it.&lt;/p&gt;

&lt;p&gt;I replaced it with a PowerShell-compatible example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Invoke-RestMethod&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Uri&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:3000/hello"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Method&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Post&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ContentType&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"application/json"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Body&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'{"name":"Virginia"}'&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This worked.&lt;/p&gt;

&lt;p&gt;The API returned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Hello, Virginia!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That led to another important lesson:&lt;/p&gt;

&lt;p&gt;A code example isn't finished when it looks correct. It's finished when a developer can copy it, run it, and get the expected result.&lt;/p&gt;

&lt;p&gt;That's an important distinction in developer documentation.&lt;/p&gt;

&lt;p&gt;I tested the failure path too&lt;/p&gt;

&lt;p&gt;Developers don't only follow the happy path.&lt;/p&gt;

&lt;p&gt;They make mistakes.&lt;/p&gt;

&lt;p&gt;They send incomplete requests.&lt;/p&gt;

&lt;p&gt;They misunderstand parameters.&lt;/p&gt;

&lt;p&gt;They use the wrong data type.&lt;/p&gt;

&lt;p&gt;So I intentionally sent:&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API responded with:&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;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Name is required"&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;That's useful API behavior.&lt;/p&gt;

&lt;p&gt;The developer immediately knows what went wrong and what needs to be fixed.&lt;/p&gt;

&lt;p&gt;They don't have to inspect the source code.&lt;/p&gt;

&lt;p&gt;They don't have to guess.&lt;/p&gt;

&lt;p&gt;They don't necessarily have to leave the terminal and search through documentation.&lt;/p&gt;

&lt;p&gt;The API itself provides useful guidance.&lt;/p&gt;

&lt;p&gt;And that made me think about something important:&lt;/p&gt;

&lt;p&gt;Error messages are part of the developer experience too.&lt;/p&gt;

&lt;p&gt;Tests became part of the API contract&lt;/p&gt;

&lt;p&gt;I added automated tests covering:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;GET /hello&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;A successful &lt;code&gt;POST /hello&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;An invalid &lt;code&gt;name&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;A missing &lt;code&gt;name&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Test Files  1 passed
Tests       4 passed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These tests aren't only there to catch bugs.&lt;/p&gt;

&lt;p&gt;They also describe the behavior the API is expected to provide.&lt;/p&gt;

&lt;p&gt;In that sense, tests become a form of &lt;strong&gt;executable documentation&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If the API behavior changes unexpectedly, the tests can expose that change.&lt;/p&gt;

&lt;p&gt;That gives developers and documentation authors something concrete to rely on.&lt;/p&gt;

&lt;p&gt;I documented the friction&lt;/p&gt;

&lt;p&gt;After going through the developer journey, I created two additional documents.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;DEVEX-AUDIT.md&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This documents:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The developer journey I tested&lt;/li&gt;
&lt;li&gt;The friction points I discovered&lt;/li&gt;
&lt;li&gt;Why the problems happened&lt;/li&gt;
&lt;li&gt;What I changed&lt;/li&gt;
&lt;li&gt;How I verified the fixes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;TROUBLESHOOTING.md&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This documents the actual problems encountered during setup and API usage, along with their solutions.&lt;/p&gt;

&lt;p&gt;This distinction matters.&lt;/p&gt;

&lt;p&gt;I wasn't writing troubleshooting content based on hypothetical problems.&lt;/p&gt;

&lt;p&gt;I was documenting problems I had actually encountered while using the project.&lt;/p&gt;

&lt;p&gt;That made the documentation much more grounded.&lt;/p&gt;

&lt;p&gt;The workflow changed&lt;/p&gt;

&lt;p&gt;The project eventually became this loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Build
  ↓
Document
  ↓
Follow the documentation
  ↓
Find friction
  ↓
Investigate
  ↓
Fix
  ↓
Test
  ↓
Update documentation
  ↓
Verify again
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And this is probably the biggest lesson I've taken from the exercise.&lt;/p&gt;

&lt;p&gt;Documentation shouldn't be treated as a final step after development.&lt;/p&gt;

&lt;p&gt;It should be part of the development and feedback loop.&lt;/p&gt;

&lt;p&gt;Developer Experience is bigger than documentation&lt;/p&gt;

&lt;p&gt;A developer experience isn't just a documentation website.&lt;/p&gt;

&lt;p&gt;It's the entire journey between:&lt;/p&gt;

&lt;p&gt;"I found this project."&lt;/p&gt;

&lt;p&gt;and:&lt;/p&gt;

&lt;p&gt;"I successfully built something with it."&lt;/p&gt;

&lt;p&gt;That journey can include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Installation&lt;/li&gt;
&lt;li&gt;Environment setup&lt;/li&gt;
&lt;li&gt;Commands&lt;/li&gt;
&lt;li&gt;APIs&lt;/li&gt;
&lt;li&gt;Code examples&lt;/li&gt;
&lt;li&gt;Error messages&lt;/li&gt;
&lt;li&gt;Tests&lt;/li&gt;
&lt;li&gt;Troubleshooting&lt;/li&gt;
&lt;li&gt;Documentation&lt;/li&gt;
&lt;li&gt;Tooling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every one of those can either create momentum or create friction.&lt;/p&gt;

&lt;p&gt;A beautiful README doesn't help if the first command fails.&lt;/p&gt;

&lt;p&gt;A technically correct example doesn't help if it doesn't work in the developer's environment.&lt;/p&gt;

&lt;p&gt;And an API that returns unclear errors creates friction even when the documentation is excellent.&lt;/p&gt;

&lt;p&gt;Everything is connected.&lt;/p&gt;

&lt;p&gt;The question I'm asking now&lt;/p&gt;

&lt;p&gt;Instead of asking:&lt;/p&gt;

&lt;p&gt;"Did I document this?"&lt;/p&gt;

&lt;p&gt;I'm starting to ask:&lt;/p&gt;

&lt;p&gt;"Can a developer successfully use this?"&lt;/p&gt;

&lt;p&gt;That's a much better standard.&lt;/p&gt;

&lt;p&gt;It pushes documentation beyond simply writing content and into:&lt;/p&gt;

&lt;p&gt;building → testing → observing → improving.&lt;/p&gt;

&lt;p&gt;And it gives you a way to collect evidence.&lt;/p&gt;

&lt;p&gt;You can test the setup.&lt;/p&gt;

&lt;p&gt;You can test the examples.&lt;/p&gt;

&lt;p&gt;You can test the API behavior.&lt;/p&gt;

&lt;p&gt;You can test the failure paths.&lt;/p&gt;

&lt;p&gt;You can verify that the documentation reflects reality.&lt;/p&gt;

&lt;p&gt;What I'm taking forward&lt;/p&gt;

&lt;p&gt;This was a small API.&lt;/p&gt;

&lt;p&gt;But the exercise taught me something much bigger.&lt;/p&gt;

&lt;p&gt;I found a missing development command.&lt;/p&gt;

&lt;p&gt;I found a platform-specific documentation issue.&lt;/p&gt;

&lt;p&gt;I tested API validation.&lt;/p&gt;

&lt;p&gt;I improved the error experience.&lt;/p&gt;

&lt;p&gt;I added automated tests.&lt;/p&gt;

&lt;p&gt;I created troubleshooting documentation.&lt;/p&gt;

&lt;p&gt;And then I followed the documentation again to verify that the experience actually worked.&lt;/p&gt;

&lt;p&gt;That changed how I think about Developer Experience.&lt;/p&gt;

&lt;p&gt;Good developer documentation isn't simply about explaining how a system works.&lt;/p&gt;

&lt;p&gt;It's about helping developers make progress.&lt;/p&gt;

&lt;p&gt;And sometimes the best way to find out whether your documentation does that is to become the developer who has to use it.&lt;/p&gt;

&lt;p&gt;Build it. Use it. Break it. Document it. Improve it.&lt;/p&gt;

</description>
      <category>documentation</category>
      <category>devex</category>
      <category>api</category>
      <category>typescript</category>
    </item>
    <item>
      <title>ktx Docs Review: Excellent for AI Agents, One Onboarding Gap</title>
      <dc:creator>Virginia Nyambura  Mwega</dc:creator>
      <pubDate>Wed, 08 Jul 2026 16:29:42 +0000</pubDate>
      <link>https://dev.to/virginiamwega2svg/ktx-docs-review-excellent-for-ai-agents-one-onboarding-gap-54ik</link>
      <guid>https://dev.to/virginiamwega2svg/ktx-docs-review-excellent-for-ai-agents-one-onboarding-gap-54ik</guid>
      <description>&lt;p&gt;A few days ago I announced a personal challenge: reviewing AI startup documentation in public — one thing done well, one place onboarding could be smoother, one rewritten example. This is review #1.&lt;/p&gt;

&lt;p&gt;The product: ktx by Kaelio — an open-source, self-improving context layer for data agents. It teaches coding agents like Claude Code, Codex, and Cursor how to query your warehouse accurately, using approved metric definitions and business context stored as reviewable YAML and Markdown in git. You install it with npm install -g @kaelio/ktx (Node.js 22+) and run one guided ktx setup.&lt;/p&gt;

&lt;p&gt;I tested the docs rather than just reading them. Everything below was verified live against docs.kaelio.com on July 8, 2026.&lt;/p&gt;

&lt;p&gt;What Kaelio does really well: docs written for agents, not just humans&lt;/p&gt;

&lt;p&gt;This is the part I want other AI startups to copy.&lt;/p&gt;

&lt;p&gt;Most documentation assumes a human reading a browser tab. ktx treats an AI coding agent as a first-class reader:&lt;/p&gt;

&lt;p&gt;A dedicated AI Resources route with an Agent Quickstart, Agent Instructions, and Prompt Recipes.&lt;br&gt;
/llms.txt (a curated index of high-value pages) and /llms-full.txt (the full corpus) so an assistant can discover the right pages before diving in.&lt;br&gt;
Per-page Markdown on demand: curl -H "Accept: text/markdown"  returns clean source — frontmatter stripped, code blocks preserved, tables preserved.&lt;br&gt;
Missing pages return a plain-text 404 instead of silently falling back to rendered HTML. Small detail, big kindness — it stops an agent from confidently parsing a garbage page.&lt;br&gt;
Copy as Markdown, View MD, and Copy MDX actions sit right on each rendered page.&lt;/p&gt;

&lt;p&gt;One of my standing review questions is literally: is the documentation structured so both developers and AI agents can use it effectively? ktx is the clearest "yes" I've come across. If you're building a developer product in 2026, this is the bar.&lt;/p&gt;

&lt;p&gt;The human path is strong too: a single guided ktx setup wizard with clearly numbered steps, a live demo warehouse with paste-ready credentials, and a Common issues table that maps symptom → fix.&lt;/p&gt;

&lt;p&gt;Where onboarding could be smoother: the MCP step the happy path skips&lt;/p&gt;

&lt;p&gt;Here's the one seam.&lt;/p&gt;

&lt;p&gt;The quickstart walks you through a clean sequence: install → ktx setup → ktx status (verify) → Connect a coding agent. That final section covers installing project-local agent rules with ktx setup --agents.&lt;/p&gt;

&lt;p&gt;What it doesn't mention is that, before your agent client can actually reach ktx, you may need to start the local MCP daemon. That step lives in the README and FAQ — not in the quickstart:&lt;/p&gt;

&lt;p&gt;The README notes: if ktx status prints ktx mcp start --project-dir ..., run it before opening your agent client.&lt;br&gt;
The FAQ explains there's no hosted service — the local MCP daemon runs on demand via ktx mcp start when an agent client needs it.&lt;/p&gt;

&lt;p&gt;So the failure mode is: a new developer follows the quickstart end-to-end, opens Claude Code or Cursor, and the agent silently can't see ktx — with no breadcrumb in the quickstart pointing at the fix. Making it slightly stickier, the ktx status example output in the Verify section shows a fully-ready project but doesn't include the ktx mcp start line the README says status can emit — so even the example doesn't prepare you for it.&lt;/p&gt;

&lt;p&gt;To be fair: ktx is under active development, and this is one missing signpost on an otherwise excellent path. But it sits at the last mile of onboarding — exactly where a new developer's first impression is decided.&lt;/p&gt;

&lt;p&gt;The rewritten section&lt;/p&gt;

&lt;p&gt;Here's how I'd close the gap — a version of "Connect a coding agent" written in ktx's own voice, folding in the missing step:&lt;/p&gt;

&lt;p&gt;Connect a coding agent&lt;/p&gt;

&lt;p&gt;The setup wizard installs project-local agent rules in its last step. To install or change targets later:&lt;/p&gt;

&lt;p&gt;ktx setup --agents&lt;/p&gt;

&lt;p&gt;Claude Code and Codex also support global installs with --global. Agent rules point at the ktx CLI path that created them, so agents don't need a separate ktx binary on PATH. If the CLI path changes, rerun ktx setup --agents.&lt;/p&gt;

&lt;p&gt;Start the MCP server if your status asks for it. Some setups serve agents through an on-demand local MCP daemon. If ktx status prints a line like:&lt;/p&gt;

&lt;p&gt;ktx mcp start --project-dir /home/user/analytics&lt;/p&gt;

&lt;p&gt;run that command before opening your agent client. Otherwise the agent connects to nothing and ktx tools appear empty. You can confirm with ktx status — Agent integration should read ready.&lt;/p&gt;

&lt;p&gt;One short subsection, one status breadcrumb. That's the whole fix — and it's the difference between a silent dead-end and a thirty-second recovery.&lt;/p&gt;

&lt;p&gt;The pattern worth noticing&lt;/p&gt;

&lt;p&gt;The irony here is instructive: ktx has some of the best agent-facing documentation I've reviewed, and its one gap is on the human path — a step the team likely stopped seeing because they already know it's there. That's the most common failure I find across audits: docs that are perfectly correct for someone who already understands the product, which is exactly the one audience that doesn't need them.&lt;/p&gt;

&lt;p&gt;If you write docs, the cheapest test available is still the best one: hand your quickstart to someone (or some agent) with zero context, and watch where they stall.&lt;/p&gt;

&lt;p&gt;Next review&lt;/p&gt;

&lt;p&gt;If you're building an AI product with public documentation, I'd love to include it — drop a link in the comments or reach out. Same format every time: one thing done well, one gap, one rewrite.&lt;/p&gt;

&lt;p&gt;I'm Virginia Mwega — a full-stack &amp;amp; AI engineer specializing in documentation engineering and developer experience. Portfolio: virginia-mwega.vercel.app · Writing: virginiamwega-com.vercel.app · Connect: &lt;a href="https://www.linkedin.com/in/virginia-mwega-196309313/" rel="noopener noreferrer"&gt;https://www.linkedin.com/in/virginia-mwega-196309313/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>documentation</category>
      <category>ai</category>
      <category>devrel</category>
      <category>devtools</category>
    </item>
    <item>
      <title>I Reviewed 10 AI Startup Documentation Sites. Here Are the 7 Mistakes I Kept Seeing.</title>
      <dc:creator>Virginia Nyambura  Mwega</dc:creator>
      <pubDate>Mon, 06 Jul 2026 18:12:05 +0000</pubDate>
      <link>https://dev.to/virginiamwega2svg/i-reviewed-10-ai-startup-documentation-sites-here-are-the-7-mistakes-i-kept-seeing-1j</link>
      <guid>https://dev.to/virginiamwega2svg/i-reviewed-10-ai-startup-documentation-sites-here-are-the-7-mistakes-i-kept-seeing-1j</guid>
      <description>&lt;p&gt;Documentation is often the first product a developer experiences.&lt;/p&gt;

&lt;p&gt;Before they see your architecture, your engineering culture, or your code quality, they interact with your documentation.&lt;/p&gt;

&lt;p&gt;If that experience is confusing, incomplete, or frustrating, many developers won't make it to their first successful API request.&lt;/p&gt;

&lt;p&gt;Over the past few weeks, I've been reviewing documentation from AI startups to understand what makes onboarding smooth—and where teams unintentionally create friction.&lt;/p&gt;

&lt;p&gt;While every company is different, the same patterns kept appearing.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Quickstarts assume too much
&lt;/h2&gt;

&lt;p&gt;Many Quickstarts jump straight into code without explaining prerequisites.&lt;/p&gt;

&lt;p&gt;Developers are expected to know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Where to get an API key&lt;/li&gt;
&lt;li&gt;Which SDK to install&lt;/li&gt;
&lt;li&gt;Required environment variables&lt;/li&gt;
&lt;li&gt;Authentication steps&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A Quickstart should help someone go from zero to a successful request with as little guesswork as possible.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Error messages aren't documented
&lt;/h2&gt;

&lt;p&gt;Developers don't judge documentation by how it works when everything goes right.&lt;/p&gt;

&lt;p&gt;They judge it by how quickly it helps them recover when something goes wrong.&lt;/p&gt;

&lt;p&gt;Instead of only listing error codes, explain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why the error happens&lt;/li&gt;
&lt;li&gt;Common causes&lt;/li&gt;
&lt;li&gt;How to fix it&lt;/li&gt;
&lt;li&gt;What to try next&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Good troubleshooting documentation builds confidence.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Examples are incomplete
&lt;/h2&gt;

&lt;p&gt;Too many examples leave out important details.&lt;/p&gt;

&lt;p&gt;Developers shouldn't have to infer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Authentication headers&lt;/li&gt;
&lt;li&gt;Environment variables&lt;/li&gt;
&lt;li&gt;Request payloads&lt;/li&gt;
&lt;li&gt;Expected responses&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Examples should be copy, paste, run, and understand.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. There's no clear learning path
&lt;/h2&gt;

&lt;p&gt;Documentation often feels like a collection of pages instead of a guided journey.&lt;/p&gt;

&lt;p&gt;A better structure might look like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Quickstart&lt;/li&gt;
&lt;li&gt;Core Concepts&lt;/li&gt;
&lt;li&gt;Tutorials&lt;/li&gt;
&lt;li&gt;API Reference&lt;/li&gt;
&lt;li&gt;Advanced Guides&lt;/li&gt;
&lt;li&gt;Troubleshooting&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When developers always know what to read next, they make progress faster.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Documentation isn't written for AI-assisted development
&lt;/h2&gt;

&lt;p&gt;Today, developers increasingly rely on AI coding assistants.&lt;/p&gt;

&lt;p&gt;That means documentation should also be easy for AI tools to interpret.&lt;/p&gt;

&lt;p&gt;This includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Consistent headings&lt;/li&gt;
&lt;li&gt;Clear terminology&lt;/li&gt;
&lt;li&gt;Structured examples&lt;/li&gt;
&lt;li&gt;Explicit parameter descriptions&lt;/li&gt;
&lt;li&gt;Predictable page organization&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Well-structured documentation helps both humans and AI systems retrieve accurate information.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Missing "next steps"
&lt;/h2&gt;

&lt;p&gt;A successful API call shouldn't be the end of the journey.&lt;/p&gt;

&lt;p&gt;Guide developers toward meaningful progress:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Build a chatbot&lt;/li&gt;
&lt;li&gt;Upload files&lt;/li&gt;
&lt;li&gt;Authenticate users&lt;/li&gt;
&lt;li&gt;Stream responses&lt;/li&gt;
&lt;li&gt;Explore advanced features&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Momentum matters.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Documentation is treated as an afterthought
&lt;/h2&gt;

&lt;p&gt;The strongest engineering teams treat documentation as part of the product—not something that's written after the code ships.&lt;/p&gt;

&lt;p&gt;Documentation improves:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Developer experience&lt;/li&gt;
&lt;li&gt;Product adoption&lt;/li&gt;
&lt;li&gt;Support efficiency&lt;/li&gt;
&lt;li&gt;Customer success&lt;/li&gt;
&lt;li&gt;Developer trust&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's not just a support resource.&lt;/p&gt;

&lt;p&gt;It's a growth asset.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Challenge
&lt;/h2&gt;

&lt;p&gt;I'm starting a public challenge where I review AI startup documentation and share practical improvements.&lt;/p&gt;

&lt;p&gt;Each review includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One thing the team did well&lt;/li&gt;
&lt;li&gt;One improvement opportunity&lt;/li&gt;
&lt;li&gt;A rewritten example or suggestion&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal isn't to criticize.&lt;/p&gt;

&lt;p&gt;It's to learn, contribute, and help create better developer experiences.&lt;/p&gt;

&lt;p&gt;If you're building an AI product with public documentation, I'd love to review it.&lt;/p&gt;

&lt;p&gt;I'm always looking for examples of thoughtful documentation—and opportunities to make good docs even better.&lt;/p&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;

</description>
      <category>devrel</category>
      <category>documentation</category>
      <category>webdev</category>
      <category>ai</category>
    </item>
    <item>
      <title>The Documentation System Every Startup Should Have</title>
      <dc:creator>Virginia Nyambura  Mwega</dc:creator>
      <pubDate>Wed, 01 Jul 2026 16:43:37 +0000</pubDate>
      <link>https://dev.to/virginiamwega2svg/the-documentation-system-every-startup-should-have-139e</link>
      <guid>https://dev.to/virginiamwega2svg/the-documentation-system-every-startup-should-have-139e</guid>
      <description>&lt;p&gt;&lt;strong&gt;Key Takeaways&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Most documentation advice is written by people whose job is &lt;em&gt;only&lt;/em&gt; documentation. This isn't that. This is the system I run as a solo founder shipping production AI, where docs that rot don't get caught by a docs team — they get caught by me, at the worst possible time.&lt;/li&gt;
&lt;li&gt;A documentation system isn't a wiki. It's four separable things (Diátaxis: tutorial, how-to, reference, explanation) plus a mechanism that keeps them from drifting out of sync with the code.&lt;/li&gt;
&lt;li&gt;The differentiator between docs that survive and docs that rot isn't writing quality. It's whether documentation is coupled to the same PR that changes the behavior, and whether it fails loudly when it goes stale.&lt;/li&gt;
&lt;li&gt;Reference docs should be &lt;em&gt;generated&lt;/em&gt; from typed code wherever possible, so the one category most likely to drift can't.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why you should ignore most documentation advice (including some of mine)
&lt;/h2&gt;

&lt;p&gt;Nearly every "how to do documentation" post is written by a technical writer — someone whose entire role is documentation, on a team where documentation is a budgeted function. That advice is often good, but it quietly assumes a luxury most early startups don't have: a person whose job is to notice when the docs stopped being true.&lt;/p&gt;

&lt;p&gt;I don't have that person. I'm a solo founder building a production AI system (FamNest, a multi-agent wellness coach). When my documentation drifts, no docs team catches it. I catch it — usually at 11pm, when I'm trying to remember how my own auth boundary works and the note I wrote three months ago is now confidently wrong.&lt;/p&gt;

&lt;p&gt;So the system I'm about to describe isn't optimized for a documentation department. It's optimized for the constraint most startups actually have: &lt;strong&gt;nobody's full-time job is keeping the docs honest, so the system itself has to do it.&lt;/strong&gt; That constraint changes the answer, and it's why this isn't a generic "write good docs" post.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four things a documentation system actually is
&lt;/h2&gt;

&lt;p&gt;The single most useful mental model I've found is Diátaxis — the framework created by Daniele Procida and now used by Cloudflare, Gatsby, and a chunk of the Python ecosystem. Its core claim is deceptively simple: there aren't many kinds of documentation, there are exactly &lt;strong&gt;four&lt;/strong&gt;, they serve different needs, and mixing them is the most common cause of docs that feel wrong even when every individual sentence is correct.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tutorials&lt;/strong&gt; — learning-oriented. A beginner, on rails, following exact steps to a known destination. Your quickstart is a tutorial. Its job is confidence, not completeness.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How-to guides&lt;/strong&gt; — task-oriented. An already-competent user trying to accomplish a specific real-world goal. "How to configure X." Not teaching — helping someone who already knows the basics get a thing done.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reference&lt;/strong&gt; — information-oriented. The dry, factual, complete description of the machinery. Your API reference, your schema, your config options. Consulted, not read.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explanation&lt;/strong&gt; — understanding-oriented. The "why" behind a design decision, read away from the keyboard. Why the architecture is shaped this way. This is the category startups skip and later regret skipping, because it's the institutional memory of &lt;em&gt;why&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The reason this matters practically: the most common documentation failure isn't a missing page, it's a &lt;strong&gt;tutorial that derailed into reference&lt;/strong&gt; — the getting-started guide that stops to enumerate every flag, until a beginner who wanted to feel capable instead feels buried. Once you can name the four types, you can see the mixing, and most "our docs are confusing" problems turn out to be mixing problems, not writing problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part nobody frames as engineering: keeping it from rotting
&lt;/h2&gt;

&lt;p&gt;Here's the thing the framework alone won't save you from, and where the solo-builder constraint actually produces a &lt;em&gt;better&lt;/em&gt; answer than the well-resourced one.&lt;/p&gt;

&lt;p&gt;Documentation doesn't fail at the moment of writing. It fails silently, later, every time the code changes and the sentence describing it doesn't. A page that's 80% accurate is arguably worse than a missing page — a missing page sends you to read the source; a subtly-wrong page confidently teaches you the wrong thing and costs you an hour before you stop trusting it.&lt;/p&gt;

&lt;p&gt;The teams that beat this don't beat it with discipline. Discipline doesn't scale and it definitely doesn't survive a solo founder's worst week. They beat it structurally, by borrowing the property that makes tests valuable: &lt;strong&gt;failing loudly instead of silently.&lt;/strong&gt; Concretely, the system I run has three rules:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Docs live in the same PR as the behavior they describe.&lt;/strong&gt; If a change to an endpoint doesn't touch the docs, that's a visible gap in the diff, not an invisible one discovered months later. This is the single highest-leverage practice and it costs nothing but a PR-template checkbox.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Reference is generated, not hand-written, wherever the code is typed.&lt;/strong&gt; Reference is the category most prone to drift — it's the most detailed and the most tightly coupled to code. So it's the category you should hand-write the &lt;em&gt;least&lt;/em&gt;. My Next.js API routes are typed; the OpenAPI reference is generated from those types, which means the one kind of documentation most likely to lie can't, because it has no independent existence to drift away from. &lt;em&gt;(I've got a full walkthrough of the endpoint-to-OpenAPI generation as its own post — linked at the end.)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Some docs should be executable, so staleness breaks a build.&lt;/strong&gt; A code example that's just text will rot. A code example that actually runs in CI fails the day it stops being true. The closer your documentation sits to something that executes, the less it can quietly lie to you. My agent-boundary contracts are validated with Zod, which means the "shape" documentation and the runtime enforcement are the same artifact — the docs can't disagree with the code because they &lt;em&gt;are&lt;/em&gt; the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The startup-specific version of this
&lt;/h2&gt;

&lt;p&gt;If you're pre-first-hire, here's the minimum viable documentation system, in priority order — not "write all four types of everything," which is how docs projects die, but the smallest thing that compounds:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;One tutorial&lt;/strong&gt; — a quickstart that actually works if a stranger copy-pastes it. Test it by having someone who isn't you run it. If they get stuck, that's a bug, not their fault.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generated reference&lt;/strong&gt; — wire your typed API to auto-generated reference so the most drift-prone category maintains itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A running "why" doc&lt;/strong&gt; — one explanation file where you write down &lt;em&gt;why&lt;/em&gt; you made the load-bearing decisions, as you make them. This is the cheapest thing on the list and the one you'll be most grateful for in a year, because it's the context you can't reconstruct later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How-to guides on demand&lt;/strong&gt; — write one every time you answer the same question twice. Let real friction, not a content plan, decide what gets written.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Notice what's not on the list: exhaustive coverage. A documentation system isn't measured by how many pages it has. It's measured by whether the pages it has stay true, and whether the structure tells a reader which kind of page they're looking at.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I'm the one writing this
&lt;/h2&gt;

&lt;p&gt;Because I live on the wrong side of the constraint that makes this advice real. I don't document my systems because a style guide told me to — I document them because I'm the one who has to reload the entire context of my own architecture at unpredictable moments, alone, and the difference between a system I documented well and one I didn't is whether that reload takes five minutes or two hours.&lt;/p&gt;

&lt;p&gt;That's also, not coincidentally, exactly the position a &lt;em&gt;new engineer joining your startup&lt;/em&gt; is in. Every argument I just made for why I document my own system for my own future self is the same argument for why your docs determine how fast your next hire becomes useful. The solo constraint just makes the cost legible sooner, because the person paying it is me, tonight.&lt;/p&gt;

&lt;p&gt;A documentation system every startup should have, in one sentence: &lt;strong&gt;four clearly separated types, reference generated from typed code, docs coupled to the PRs that change behavior, and the "why" written down as you go.&lt;/strong&gt; Everything else is a variation on those four moves.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I build FamNest and write about the engineering underneath it. The specific pieces of the system above have their own deep-dives — endpoint-to-OpenAPI generation, validating agent boundaries with Zod, and treating Supabase RLS as living auth documentation — linked as I publish them in this series.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>documentation</category>
      <category>devrel</category>
      <category>startup</category>
      <category>api</category>
    </item>
    <item>
      <title>Evaluating Agents With an LLM-as-Judge Harness (Without Kidding Yourself About It)</title>
      <dc:creator>Virginia Nyambura  Mwega</dc:creator>
      <pubDate>Wed, 01 Jul 2026 15:54:18 +0000</pubDate>
      <link>https://dev.to/virginiamwega2svg/evaluating-agents-with-an-llm-as-judge-harness-without-kidding-yourself-about-it-186k</link>
      <guid>https://dev.to/virginiamwega2svg/evaluating-agents-with-an-llm-as-judge-harness-without-kidding-yourself-about-it-186k</guid>
      <description>&lt;p&gt;&lt;strong&gt;Key Takeaways&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You can't unit-test a coach agent the way you test a pure function — the output is non-deterministic and "good" is a judgment call, not an assertion.&lt;/li&gt;
&lt;li&gt;An LLM-as-judge harness lets you grade a whole test set automatically against a rubric, which is the only way solo-scale eval stays sustainable.&lt;/li&gt;
&lt;li&gt;But the judge is itself a fallible model. If you don't design around its known biases — position, verbosity, self-preference, and quiet drift when the judge model updates — you build a green dashboard that means nothing.&lt;/li&gt;
&lt;li&gt;The mitigations that actually work are mechanical, not prompt-magic: shuffle order on every pairwise call, pin the judge version, keep a small human-labelled anchor set, and re-check the judge against it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The problem I actually had
&lt;/h2&gt;

&lt;p&gt;FamNest's coach agent generates responses to parents — check-ins, encouragement, the occasional gentle redirect. I have a growing pile of these interactions, and every time I change a prompt, swap a model, or adjust the pipeline, I need to know one thing: &lt;strong&gt;did I just make it better or worse?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For normal code, that's what tests are for. I change something, the suite runs, red or green, done. But there's no &lt;code&gt;assertEqual&lt;/code&gt; for "was this an empathetic, useful response to a tired parent." The output changes every run even at temperature zero-ish, and the quality bar is a human judgment, not a fixed string. Two responses can be worded completely differently and both be good. One can match my "expected output" word for word and still be worse than a version that didn't.&lt;/p&gt;

&lt;p&gt;So the honest options were: read every response by hand every time I change something (does not scale past about week two), or build a harness where a model grades the outputs against a rubric. I built the harness. Then I spent an uncomfortable amount of time learning all the ways a harness like that can lie to you.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the harness actually is
&lt;/h2&gt;

&lt;p&gt;At its simplest, it's a loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;test_cases&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;coach_agent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;judge&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;case&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;test_cases&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;coach_agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;case&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;case&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;verdict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;judge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;rubric&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;COACH_RUBRIC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;user_message&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;case&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;case_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;case&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;verdict&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reasoning&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;verdict&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reasoning&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="n"&gt;results&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;COACH_RUBRIC&lt;/code&gt; is the part that matters. It's not "rate this 1–10" — that produces mush. It's specific, scored dimensions: does the response acknowledge the actual thing the user said (not a generic version of it)? Does it avoid giving medical advice? Is it the right length for the moment, or is it a wall of text at someone who's exhausted? Each dimension gets a small integer and a one-line justification, and the harness keeps the justification, not just the number — because when the score drops, the reasoning is what tells me whether the agent regressed or the judge just had an opinion.&lt;/p&gt;

&lt;p&gt;That last distinction is the whole game.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part where I stopped trusting the judge
&lt;/h2&gt;

&lt;p&gt;Here's the failure mode that made me rebuild the whole thing. You score helpfulness at 0.91 all quarter. Then the judge model ships a minor version bump. The mean shifts a few points, the distribution narrows, and your CI gate keeps passing — so you don't look. Weeks later the agent does something genuinely bad and the eval never flagged it, because the judge changed underneath you and the number stopped meaning what it meant the day you set the threshold.&lt;/p&gt;

&lt;p&gt;The research here is not subtle, and it's worth internalizing before you trust a single green checkmark. A 2026 RAND study that stress-tested judges across multiple benchmarks concluded that no judge was uniformly reliable, and frontier models exceeded 50% error rates on hard bias benchmarks. Consistency broke on inputs as trivial as formatting changes and paraphrasing. Separately, the classic MT-Bench work found that in pairwise comparisons, the answer in the first slot wins something like 10–15 points more often &lt;em&gt;purely because it's first&lt;/em&gt; — position bias, nothing to do with quality.&lt;/p&gt;

&lt;p&gt;(Worth noting the field isn't static: some 2026 reproductions find position bias has shrunk to near-negligible on current-gen models under a clean pairwise rubric, while verbosity bias stays small. Which is exactly the point — the biases move as the models move, so you measure them yourself rather than trusting a blog post from last year, including this one.)&lt;/p&gt;

&lt;p&gt;The named biases I actually design around:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Position bias&lt;/strong&gt; — in any A-vs-B comparison, slot order can decide the winner. Mitigation: run every pairwise comparison twice with the order flipped, and only count it if the verdict is stable across both.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verbosity bias&lt;/strong&gt; — longer answers tend to score higher even at matched quality. Mitigation: put length appropriateness &lt;em&gt;in the rubric as an explicit dimension&lt;/em&gt; so the judge is scoring it on purpose instead of rewarding it by accident.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-preference&lt;/strong&gt; — a judge from the same model family as the candidate tends to over-score it. Mitigation: don't let the judge be the same model as the agent it's grading. (In my case the coach runs on one provider; I judge with a different family entirely.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Calibration drift&lt;/strong&gt; — the silent one above. Mitigation below, because it's the most important.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The anchor set is the thing that keeps you honest
&lt;/h2&gt;

&lt;p&gt;The single highest-leverage piece of the harness isn't the judge prompt. It's a small set — a few dozen cases — that I labelled by hand, carefully, once. Good responses, bad responses, and the genuinely ambiguous ones. That's my ground truth.&lt;/p&gt;

&lt;p&gt;Every time I run the harness, it grades the anchor set too. If the judge's scores on those known cases still line up with my human labels, I trust its scores on the rest of the run. If the judge drifts on the anchor set — because the model updated, because I tweaked the rubric, because Mercury is in retrograde — I find out immediately, on cases where I already know the right answer, instead of finding out in production on a case where I don't.&lt;/p&gt;

&lt;p&gt;This is the same instinct as the deterministic crisis floor I wrote about earlier in this series: &lt;strong&gt;the most consequential check should be the one that's simplest and least dependent on a model behaving.&lt;/strong&gt; For safety, that's regex. For evaluation, it's a few dozen examples I graded with my own eyes and refuse to let a model overrule silently.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell someone starting this
&lt;/h2&gt;

&lt;p&gt;Build the harness — reading every output by hand does not scale, and an LLM judge genuinely does correlate with human preference well enough to be useful. But treat the judge as a component that can fail, not an oracle. Pin its version so it doesn't change without you deciding. Shuffle order on comparisons. Keep the reasoning, not just the score. And keep a small hand-labelled anchor set that you re-check every single run, because a green eval dashboard that you never validate is worse than no dashboard — it's the &lt;em&gt;confidence&lt;/em&gt; of measurement without the substance, and that's exactly the kind of thing that ships a broken agent with a clean conscience.&lt;/p&gt;

&lt;p&gt;The harness didn't remove my judgment from the loop. It moved my judgment to where it's cheap and permanent — a small set of examples I curate once — instead of where it's expensive and forgettable, which is re-reading a hundred responses every time I touch a prompt.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Part of an ongoing series documenting FamNest's architecture. Earlier posts cover the deterministic crisis floor and the multi-agent coach pipeline. Next: how we test a non-deterministic system end to end.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>llm</category>
      <category>systemdesign</category>
      <category>testing</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The doc was the spec: building a safety layer for an AI app that talks to tired parents published: false</title>
      <dc:creator>Virginia Nyambura  Mwega</dc:creator>
      <pubDate>Tue, 30 Jun 2026 10:23:52 +0000</pubDate>
      <link>https://dev.to/virginiamwega2svg/the-doc-was-the-spec-building-a-safety-layer-for-an-ai-app-that-talks-to-tired-parents-published-1pnd</link>
      <guid>https://dev.to/virginiamwega2svg/the-doc-was-the-spec-building-a-safety-layer-for-an-ai-app-that-talks-to-tired-parents-published-1pnd</guid>
      <description>&lt;p&gt;"&lt;em&gt;What happened when I stopped designing my AI coach for flexibility and started designing it to be predictable enough to write down."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Last month I deleted three features from my AI app. Nobody asked me to.&lt;br&gt;
I removed them because I couldn't explain them clearly. And I've started treating that as a hard signal: if I can't describe how a feature behaves in plain language a tired parent would understand, it isn't finished — it's a liability.&lt;br&gt;
This post is about the engineering that came out of that rule. It's specific, it has code, and it's the part of building an AI product that nobody puts in the demo.&lt;br&gt;
The context&lt;br&gt;
I'm building FamNest, an AI wellness tool for parents. The user I design for is the one refreshing a feeding tracker at 3am, googling "is this normal" with one hand while holding a sleeping baby with the other.&lt;br&gt;
That user changes your engineering priorities. Latency matters less than predictability. "Clever" is a risk, not a feature. And the moment your AI says something subtly wrong to someone in that state, you've lost them — and possibly done real harm.&lt;br&gt;
So the question I kept returning to wasn't "what can the model do?" It was "what is the model never allowed to improvise?"&lt;br&gt;
Problem 1: a coach with too many answers&lt;br&gt;
In the first version, my AI coach could respond to the same question a dozen slightly different ways. It felt smart. Flexible, even.&lt;br&gt;
It was actually a nightmare. When I sat down to write the documentation, I couldn't promise a parent what the system would do. And if I can't promise it, why should they trust it?&lt;br&gt;
The fix was to put a second model in front of the user — a safety reviewer — and give it exactly three outcomes. Not a confidence score. Not a freeform critique. Three discrete verdicts.&lt;br&gt;
``type Verdict = "ok" | "revise" | "crisis";&lt;/p&gt;

&lt;p&gt;interface ReviewResult {&lt;br&gt;
  verdict: Verdict;&lt;br&gt;
  reason: string;        // for logging, never shown to the user&lt;br&gt;
  revisedDraft?: string; // only present when verdict === "revise"&lt;br&gt;
}``&lt;br&gt;
The coach generates a draft. The reviewer judges it. The whole contract fits in four lines, and that's the point — I can document it in one sentence: the reviewer either approves the draft, rewrites it, or escalates to a crisis response.&lt;/p&gt;

&lt;p&gt;``async function generateReply(userMessage: string): Promise {&lt;br&gt;
  const draft = await coach.respond(userMessage);&lt;br&gt;
  const review = await safetyReviewer.review(userMessage, draft);&lt;/p&gt;

&lt;p&gt;switch (review.verdict) {&lt;br&gt;
    case "ok":&lt;br&gt;
      return draft;&lt;br&gt;
    case "revise":&lt;br&gt;
      return review.revisedDraft ?? draft;&lt;br&gt;
    case "crisis":&lt;br&gt;
      return CRISIS_RESPONSE; // see below — this is not generated&lt;br&gt;
  }&lt;br&gt;
}&lt;code&gt;&lt;br&gt;
Problem 2: the crisis floor&lt;br&gt;
Here's the decision I'm most sure about.&lt;br&gt;
When a message hints at a crisis, the system does not generate a response. It returns fixed, human-reviewed text. Every time. Byte for byte.&lt;br&gt;
&lt;/code&gt;// This string is reviewed by a human, version-controlled,&lt;br&gt;
// and documented word-for-word in the user-facing docs.&lt;br&gt;
const CRISIS_RESPONSE = &lt;code&gt;&lt;br&gt;
It sounds like you're going through something really hard right now.&lt;br&gt;
You don't have to handle this alone. If you're in immediate danger,&lt;br&gt;
please contact your local emergency number...&lt;br&gt;
&lt;/code&gt;.trim();&lt;code&gt;&lt;br&gt;
This is the opposite of how we usually think about generative AI. The whole appeal of an LLM is that it composes something new. But the situation where a parent most needs the response to be right is exactly the situation where I least want the model to be creative.&lt;br&gt;
I call this the crisis floor: a deterministic baseline that the system can never generate its way below. The model can make the experience better above the floor. It is never allowed to touch what happens at it.&lt;br&gt;
A subtle but important detail: the floor is checked before the clever path, not after.&lt;br&gt;
&lt;/code&gt;async function handleMessage(userMessage: string): Promise {&lt;br&gt;
  // Deterministic guardrail runs first, independent of the LLM.&lt;br&gt;
  if (crisisFloor.matches(userMessage)) {&lt;br&gt;
    return CRISIS_RESPONSE;&lt;br&gt;
  }&lt;br&gt;
  return generateReply(userMessage);&lt;br&gt;
}&lt;code&gt;&lt;br&gt;
If the generative pipeline is down, malfunctioning, or hallucinating, the floor still holds. It doesn't depend on the part of the system most likely to fail.&lt;br&gt;
Problem 3: what happens when the provider falls over&lt;br&gt;
LLM APIs go down. Rate limits hit. A region has a bad day. For most apps that's an annoyance. For an app a parent leans on at 3am, a blank screen is a broken promise.&lt;br&gt;
So every external dependency has a known fallback, and "the model is unavailable" is a documented state — not an exception that bubbles up to a stack trace.&lt;br&gt;
&lt;/code&gt;async function coachRespond(message: string): Promise {&lt;br&gt;
  try {&lt;br&gt;
    return await llm.complete(buildPrompt(message));&lt;br&gt;
  } catch (err) {&lt;br&gt;
    logger.warn("LLM provider unavailable, degrading gracefully", { err });&lt;br&gt;
    // A safe, generic, pre-written reply. Not an error.&lt;br&gt;
    return GRACEFUL_FALLBACK;&lt;br&gt;
  }&lt;br&gt;
}``&lt;br&gt;
The user gets a calm, honest message instead of a spinner that never resolves. Graceful degradation isn't a nice-to-have here. It's part of the trust contract.&lt;br&gt;
The thing I actually learned&lt;br&gt;
The pattern underneath all three: documentation wasn't something I wrote after the engineering. It was the engineering.&lt;br&gt;
The doc became the spec. When a behavior couldn't be written down cleanly — three verdicts, fixed crisis text, a named fallback state — that was the signal the design was wrong, not the writing.&lt;br&gt;
It turns out "predictable enough to document" is a great design constraint. It pushes you toward small, discrete contracts and away from the kind of open-ended cleverness that demos well and ships badly.&lt;br&gt;
I think a lot of AI products are shipping behavior they can't fully explain and quietly calling that confusion "intelligence." For the users I build for, clarity is the feature.&lt;br&gt;
If you can't document it, you don't understand it yet.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I write about building and documenting production AI systems. If you've shipped a guardrail or fallback you're proud of — or one that bit you — I'd genuinely like to hear about it in .&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>typescript</category>
      <category>webdev</category>
      <category>devjournal</category>
    </item>
    <item>
      <title>Writing API docs an AI agent can actually consume</title>
      <dc:creator>Virginia Nyambura  Mwega</dc:creator>
      <pubDate>Mon, 29 Jun 2026 15:01:58 +0000</pubDate>
      <link>https://dev.to/virginiamwega2svg/writing-api-docs-an-ai-agent-can-actually-consume-16bb</link>
      <guid>https://dev.to/virginiamwega2svg/writing-api-docs-an-ai-agent-can-actually-consume-16bb</guid>
      <description>&lt;p&gt;&lt;em&gt;Your docs are written for a human who can guess. The agent calling your API can't&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;I found the gap the embarrassing way: one of my own agents couldn't call one of my own APIs.&lt;/p&gt;

&lt;p&gt;FamNest runs a small agent graph — a router hands a parent's message to a retriever, the retriever grounds an answer in a vetted corpus, a coach agent (Groq, Llama 3.3 70B) drafts a reply, and a safety-reviewer agent signs off before anything reaches a human. The agents call internal endpoints the same way a third-party integrator would. And one afternoon the coach kept constructing malformed calls to the retrieval endpoint — wrong field name, missing a required filter, occasionally inventing a parameter that never existed.&lt;/p&gt;

&lt;p&gt;The endpoint wasn't broken. The docs were. They were written for a human who could fill in the blanks, and the agent had no blanks to fill — only the tokens I gave it.&lt;/p&gt;

&lt;p&gt;That's the whole lesson, and it's worth more than a trend.&lt;/p&gt;

&lt;p&gt;The trend everyone's shipping — and where it stops&lt;/p&gt;

&lt;p&gt;If you've touched developer tooling in 2026 you've watched llms.txt go from a September-2024 proposal to a routine piece of infrastructure. It's a Markdown file at your domain root that points AI systems at the content that matters, with a one-line summary of each link. Mintlify, Fern, and GitBook ship one-click toggles for it. IDE agents — Cursor, Windsurf, Claude Code, Copilot — fetch it when you point them at a docs site, then pull only the linked pages they need before writing code. LangChain even shipped an MCP server (mcpdoc) that hands those files to host apps as a fetch_docs tool.&lt;/p&gt;

&lt;p&gt;People are calling this the Business-to-Agent web, and the framing is right: just as you once needed a site humans could navigate, you now need surfaces agents can route on. Ship the llms.txt. It's a half-day of work.&lt;/p&gt;

&lt;p&gt;But notice what it actually solves: discovery. It answers "which page matters." It says nothing about the harder question that broke my coach agent:&lt;/p&gt;

&lt;p&gt;Once the agent has found your endpoint, can it call it correctly on the first try — with no human in the loop to recover when your prose is ambiguous?&lt;/p&gt;

&lt;p&gt;That's not a discovery problem. That's a contract problem. And it's where most docs quietly fail.&lt;/p&gt;

&lt;p&gt;An agent is a different kind of reader&lt;/p&gt;

&lt;p&gt;A human reading your docs brings a lifetime of priors. They infer that userId is probably a UUID. They notice the example uses snake_case and adjust. They hit a 400, shrug, read the error, and try again. If they're really stuck they ask a teammate. Human docs can be good enough because the human closes the gap.&lt;/p&gt;

&lt;p&gt;An agent closes nothing. It has your tokens and a probability distribution. It pattern-matches structure: if your example shows one field, it produces one field; if you describe an error in a sentence, it treats the sentence as flavor, not as a branch it has to handle. Ambiguity doesn't make an agent cautious — it makes it confident and wrong.&lt;/p&gt;

&lt;p&gt;So the doc stops being documentation and becomes the interface itself. Everything the agent will ever know about your endpoint is in the text. If a fact isn't on the page, it doesn't exist.&lt;/p&gt;

&lt;p&gt;That reframes what a good endpoint doc has to contain. Here are the five things mine were missing.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A typed schema, not a prose description&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Prose says: "Send the user's question and an optional list of topic tags."&lt;/p&gt;

&lt;p&gt;A schema says exactly what's allowed, and an agent can pattern-match it without guessing:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ts// retrieve — request&lt;br&gt;
const RetrieveRequest = z.object({&lt;br&gt;
  query: z.string().min(1).max(2000),&lt;br&gt;
  topics: z.array(z.enum(["sleep", "feeding", "behavior", "self_care"]))&lt;br&gt;
            .max(4)&lt;br&gt;
            .default([]),&lt;br&gt;
  topK: z.number().int().min(1).max(10).default(5),&lt;br&gt;
});&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The difference is the enum, the bounds, the default. "Optional list of topic tags" let my coach invent "toddler_tantrums". z.enum([...]) makes the valid set unguessable-wrong. Publish the schema, not a paragraph about the schema.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Exhaustive examples — including the unhappy paths&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Agents copy examples. Whatever you show is what you'll get back. If your only example is the happy path, the happy path is the only thing the model knows how to produce.&lt;/p&gt;

&lt;p&gt;So I document the empty result and the rejected request as first-class examples, not footnotes:&lt;/p&gt;

&lt;p&gt;``jsonc// 200 — results found&lt;br&gt;
{ "matches": [{ "id": "c_18", "score": 0.82, "text": "..." }], "truncated": false }&lt;/p&gt;

&lt;p&gt;// 200 — valid query, nothing relevant (NOT an error)&lt;br&gt;
{ "matches": [], "truncated": false }&lt;/p&gt;

&lt;p&gt;// 422 — query failed validation&lt;br&gt;
{ "error": "validation_error", "field": "topics", "detail": "unknown topic 'toddler_tantrums'" }`&lt;/p&gt;

&lt;p&gt;The middle case is the one humans leave out and agents desperately need. "No matches" is a normal outcome, not a failure — and if you don't say so, the agent will treat an empty array as a bug and retry forever.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;An error taxonomy with recovery semantics&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Most docs describe errors. Agents need to be told what to do about them. "Returns 429 when rate-limited" is a description. An agent needs a decision.&lt;/p&gt;

&lt;p&gt;So I ship a table where every row ends in an action:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Code&lt;/th&gt;
&lt;th&gt;&lt;code&gt;error&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;Cause&lt;/th&gt;
&lt;th&gt;What the caller should do&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;422&lt;/td&gt;
&lt;td&gt;&lt;code&gt;validation_error&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Bad input&lt;/td&gt;
&lt;td&gt;Fix the field named in &lt;code&gt;detail&lt;/code&gt;; do &lt;strong&gt;not&lt;/strong&gt; retry unchanged&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;429&lt;/td&gt;
&lt;td&gt;&lt;code&gt;rate_limited&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Too many calls&lt;/td&gt;
&lt;td&gt;Back off using &lt;code&gt;Retry-After&lt;/code&gt;; retry the same body&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;503&lt;/td&gt;
&lt;td&gt;&lt;code&gt;model_unavailable&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Upstream LLM down&lt;/td&gt;
&lt;td&gt;Fall back to cached/deterministic path; do not retry tightly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;409&lt;/td&gt;
&lt;td&gt;&lt;code&gt;idempotency_conflict&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Key reused, different body&lt;/td&gt;
&lt;td&gt;Stop; surface to a human&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A human reads that table for reference. An agent reads it as a control-flow graph. The "do not retry" cells are the ones that stop a confused agent from hammering your endpoint at 3am.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;An explicit determinism / idempotency contract&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The single most useful sentence I added to any endpoint doc was: "Is it safe to retry this?"&lt;/p&gt;

&lt;p&gt;Agents retry. Networks are flaky, and a retried call that isn't idempotent is how you double-charge a card or send two replies to one anxious parent. For anything with a side effect, I now state the contract in the doc itself:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;br&gt;
Idempotency: required for POST /coach/reply and all payment routes.&lt;br&gt;
Send an `Idempotency-Key` header (UUID). Replays with the same key + same&lt;br&gt;
body return the original result. Same key + different body → 409.&lt;br&gt;
Retrieval (GET /retrieve) is side-effect-free and safe to retry freely.&lt;br&gt;
&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;That paragraph is the difference between a retry loop that heals and one that does damage. It's also the kind of thing humans infer and agents simply won't — there is no prior that tells a model your payment webhook is replay-safe. You have to say it.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Auth and limits as data, not folklore&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;"Authenticated requests only, please don't spam it" is not a contract. Scopes, the exact header, the rate limit, and the window belong in the doc as structured fields the agent can read and self-regulate against:&lt;/p&gt;

&lt;p&gt;`&lt;code&gt;&lt;/code&gt;plaintext&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;&lt;br&gt;
Auth: Bearer token in&lt;/code&gt;Authorization&lt;code&gt;. Scope&lt;/code&gt;coach:read&lt;code&gt;for /retrieve.&lt;br&gt;
Limits: 60 req/min/token. On exceed → 429 +&lt;/code&gt;Retry-After` (seconds).&lt;/p&gt;

&lt;p&gt;`&lt;code&gt;&lt;/code&gt;plaintext&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;`&lt;/p&gt;

&lt;p&gt;Now the agent can pace itself instead of discovering your limit by tripping it.&lt;/p&gt;

&lt;p&gt;Keep it honest: one source of truth&lt;/p&gt;

&lt;p&gt;All of this rots the moment your docs and your code disagree — and an agent can't smell a stale doc the way a human can. So the contract has to be generated, not hand-maintained.&lt;/p&gt;

&lt;p&gt;My chain is boring on purpose: the typed Next.js handler validates with the Zod schema, the schema generates the OpenAPI spec, and my llms.txt links to the generated reference. The schema is the only thing I edit. The doc can't drift, because the doc is downstream of the thing that's actually true.&lt;/p&gt;

&lt;p&gt;*&lt;em&gt;Zod schema  ──►  request validation (runtime)&lt;br&gt;
     │&lt;br&gt;
     └────────►  OpenAPI spec  ──►  /llms.txt entry  ──►  agent reads it&lt;br&gt;
*&lt;/em&gt;&lt;br&gt;
If the handler changes, every artifact downstream changes with it. The doc lies only if the code lies.&lt;/p&gt;

&lt;p&gt;The test that actually proves it&lt;/p&gt;

&lt;p&gt;Here's the check I run before I trust an endpoint doc: give a fresh model only the doc — no codebase, no context — and ask it to (a) construct a valid call and (b) handle a seeded error. If it can't, the gap is in the doc, not the model. I keep these as tiny snapshot tests next to the endpoint, so a doc regression fails CI like any other bug.&lt;/p&gt;

&lt;p&gt;When my coach agent broke, this test would have caught it in seconds. The retrieval doc, fed to a cold model, produced exactly the malformed call I saw in production — because the ambiguity was right there on the page.&lt;/p&gt;

&lt;p&gt;This isn't new. It's just a new reader.&lt;/p&gt;

&lt;p&gt;None of this is novel discipline. "Unambiguous, complete, verifiable" is the spine of IEEE 29148 — the requirements standard I write everything against. What's changed is that the consumer of your interface is no longer guaranteed to be a person who can paper over a vague spec. Half your integrators in 2026 are agents, and they read your docs literally, exhaustively, and without charity.&lt;/p&gt;

&lt;p&gt;Ship the llms.txt so agents can find you. But the thing that makes them succeed once they arrive is older and less glamorous: a contract precise enough that it can't be misread. The agentic web doesn't need prettier docs. It needs docs that can't be guessed wrong.&lt;/p&gt;

&lt;p&gt;I build FamNest, an AI wellness coach for busy parents, and write about production reliability and safety for multi-agent systems. If you're documenting an agent-callable API and want a second pair of eyes on the contract, my notes are open.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>documentation</category>
      <category>webdev</category>
      <category>llm</category>
    </item>
  </channel>
</rss>
