<?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: DevUnionX</title>
    <description>The latest articles on DEV Community by DevUnionX (@devunionx).</description>
    <link>https://dev.to/devunionx</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%2F3180316%2F804c7ae5-1a93-4c38-b9ec-023a59a621a8.jpg</url>
      <title>DEV Community: DevUnionX</title>
      <link>https://dev.to/devunionx</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/devunionx"/>
    <language>en</language>
    <item>
      <title>5 Things Even AI Can’t Do: Context, Creativity, and Human Touch in Node/NestJS Development</title>
      <dc:creator>DevUnionX</dc:creator>
      <pubDate>Fri, 17 Jul 2026 22:40:32 +0000</pubDate>
      <link>https://dev.to/devunionx/5-things-even-ai-cant-do-context-creativity-and-human-touch-in-nodenestjs-development-5e3l</link>
      <guid>https://dev.to/devunionx/5-things-even-ai-cant-do-context-creativity-and-human-touch-in-nodenestjs-development-5e3l</guid>
      <description>&lt;h2&gt;
  
  
  MY GAME JUST LAUNCHED
&lt;/h2&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://play.google.com/store/apps/details?id=com.flip_duel_card_battle" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fplay-lh.googleusercontent.com%2FQKf5gbEwnY_z_Yvfgb5oGDsRHphvEFFZ12gxkXdve76F9KWDZrACEjKzynQt4kNIUf422odV07pRCHKt20B1" height="512" class="m-0" width="512"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://play.google.com/store/apps/details?id=com.flip_duel_card_battle" rel="noopener noreferrer" class="c-link"&gt;
            Flip Duel: Online Card Game - Apps on Google Play
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            1v1 card duel! Bluff, beat your rival. Combine your cards, raid the dungeon!
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.gstatic.com%2Fandroid%2Fmarket_images%2Fweb%2Ffavicon_v3.ico" width="32" height="32"&gt;
          play.google.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;AI is &lt;strong&gt;powerful&lt;/strong&gt; and getting smarter – it can generate code, answer questions, and even help design systems. Yet seasoned engineers know there are tasks only people can do. A recent industry overview bluntly states: &lt;em&gt;“it does not understand meaning… it cannot create independent judgment… [and] it lacks emotional intelligence”&lt;/em&gt;. In other words, AI excels at narrow, data-driven tasks but fails where true understanding, empathy, or accountability is required. This article – aimed at Node.js/NestJS developers and engineers – digs into five such areas. Each section explains the concept, provides real-world dev examples (with code snippets in JavaScript/TypeScript when relevant), and notes where AI helps and where it stumbles. We cite the latest research and official docs to back up these points. By the end, you’ll have a grounded perspective on AI-assisted development: how to benefit from code generation tools and LLMs while knowing when human judgment must step in.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. AI Can’t Truly Understand Context
&lt;/h2&gt;

&lt;p&gt;AI models do a fantastic job spotting patterns in data and text, but &lt;strong&gt;real context&lt;/strong&gt; remains elusive. LLMs process words based on statistical associations, not on meaning the way humans do. As one AI analyst notes, &lt;em&gt;“AI… operates by detecting patterns in historical data. It does not understand meaning. It does not know consequences”&lt;/em&gt;. In practice, that means an LLM won’t “get” the broader situation behind your code or data. It has no sense of what happened a minute ago in the team standup or what business rules apply to a request.&lt;/p&gt;

&lt;h3&gt;
  
  
  Context in Software Systems
&lt;/h3&gt;

&lt;p&gt;Consider NestJS’s &lt;strong&gt;ExecutionContext&lt;/strong&gt;, which provides metadata about a request or event handler. In a HTTP context, a guard or interceptor can see exactly which controller and method are handling the request. For example, Nest’s docs show that when handling a &lt;code&gt;POST&lt;/code&gt; to &lt;code&gt;CatsController.create()&lt;/code&gt;, &lt;code&gt;getHandler()&lt;/code&gt; returns the &lt;code&gt;create()&lt;/code&gt; function and &lt;code&gt;getClass()&lt;/code&gt; returns &lt;code&gt;CatsController&lt;/code&gt;. This allows the code to adapt behavior based on context:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Injectable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LoggingInterceptor&lt;/span&gt; &lt;span class="k"&gt;implements&lt;/span&gt; &lt;span class="nx"&gt;NestInterceptor&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;intercept&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ExecutionContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CallHandler&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;httpCtx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;switchToHttp&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;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;httpCtx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getRequest&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Request&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;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="s2"&gt;`Incoming &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  
    &lt;span class="nx"&gt;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="s2"&gt;`Handler: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getClass&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getHandler&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handle&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;Here, the developer explicitly uses runtime context (HTTP request, handler info) to log or branch logic. An AI given only a code snippet &lt;em&gt;and a prompt&lt;/em&gt; might not infer all those details about when and how the code executes. It lacks true awareness of the request lifecycle and runtime state.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where AI Excels and Falls Short
&lt;/h3&gt;

&lt;p&gt;Modern LLMs can “remember” recent conversation (via a context window) and even long documents. For example, Google’s Gemini can handle over a &lt;strong&gt;million tokens&lt;/strong&gt;. But larger windows aren’t a magic fix: as the Anthropic team points out, &lt;em&gt;“LLMs, like humans, lose focus… as the number of tokens in the context window increases, the model’s ability to recall information decreases”&lt;/em&gt;. This “context rot” means that even if you feed the AI pages of documentation, it may still get confused about which part is relevant to the current query. &lt;/p&gt;

&lt;p&gt;In practice, developers use &lt;strong&gt;Retrieval-Augmented Generation (RAG)&lt;/strong&gt; to mitigate this. RAG fetches specific data from external sources into the prompt. As NVIDIA explains, RAG &lt;em&gt;“enhances the accuracy and reliability of generative AI models with information from specific and relevant data sources”&lt;/em&gt;. In other words, you build a “knowledge base” (say, your own code docs) and include facts in the prompt. This helps ground the AI’s answers. NVIDIA even notes that RAG &lt;em&gt;“reduces the possibility that a model will give a very plausible but incorrect answer, a phenomenon called hallucination”&lt;/em&gt;. &lt;/p&gt;

&lt;p&gt;For example, in Node.js you might write a simple RAG function to answer questions about your project:&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;answerWithRAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;vectorStore&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;docs&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;vectorStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;retrieveRelevant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// fetch related docs&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`
    Use the following information to answer the question.
    &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;docs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;
    Q: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
    A:
  `&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;completion&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;openai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createChatCompletion&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;gpt-4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&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="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;completion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&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;Here the code explicitly retrieves context (docs from a vector store) and injects it into the prompt. The AI no longer has to recall everything; it sees targeted context. This improves reliability but also highlights AI’s gap: it needed human help (the vector store, curation of docs) to supply true context. AI by itself would likely hallucinate or miss important facts without that.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lessons for Developers
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use prompt and context engineering:&lt;/strong&gt; Carefully craft prompts, provide relevant docs, and manage the token window. Treat context as finite. Anthropic advises thinking &lt;em&gt;“in context”&lt;/em&gt; – iteratively curating only the &lt;strong&gt;smallest set of high-signal tokens&lt;/strong&gt; needed for the task.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validate outputs:&lt;/strong&gt; Even with RAG or careful prompting, always test AI-generated code or answers against known facts. The AI might produce fluent but incorrect text once you exceed its context capacity.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Combine AI with logic:&lt;/strong&gt; Use static types, unit tests, and logging (as in the NestJS example) to catch context mismatches early. AI can suggest code, but &lt;strong&gt;you&lt;/strong&gt; must check it in the full system context.
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In summary, AI struggles with hidden context and system state. It can automate low-level coding, but developers must manage broader context, architecture and integration – tasks that require human insight.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. AI Can’t Take Responsibility
&lt;/h2&gt;

&lt;p&gt;AI will help write the code or suggest a design, but it &lt;em&gt;won’t own the results&lt;/em&gt;. Machines don’t have accountability or intention. A recent discussion on AI accountability emphasizes: &lt;em&gt;“it cannot assume responsibility for the outcomes of those decisions… The emphasis must remain on ensuring that humans are equipped and willing to take accountability”&lt;/em&gt;. In other words, if an AI-driven system causes a bug, outage, or even an ethical breach, the responsibility ultimately falls on the human developers, operators, or managers involved.&lt;/p&gt;

&lt;h3&gt;
  
  
  Real-World Software Example
&lt;/h3&gt;

&lt;p&gt;Imagine deploying a NestJS microservice that uses an AI to classify user inputs. If the AI misclassifies sensitive data, who fixes it? Engineers must detect it, debug the model or logic, and apologize to stakeholders – tasks no AI can do. Similarly, an AI-generated pull request still requires a developer to review, merge, and be ready to revert if things go wrong. &lt;/p&gt;

&lt;p&gt;Consider DevOps and CI/CD: AI tools can suggest pipeline configurations or even generate Kubernetes YAMLs. But when a deployment fails or a security issue emerges, a human is on-call to investigate and respond, not the AI. If the pipeline says “build succeeded” but a hidden error slipped through, the engineer digs into logs and patches the code.&lt;/p&gt;

&lt;p&gt;The need for human oversight is often summed up: &lt;em&gt;“you can outsource execution; you can’t outsource consequences. If it goes wrong, 'AI said so' won’t save you”&lt;/em&gt;. (An Instagram quote puts it simply: accountability cannot be automated – and in a production system, someone must own every decision.)&lt;/p&gt;

&lt;h3&gt;
  
  
  AI’s Strengths vs. Weaknesses (Accountability)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;AI Role (Strength)&lt;/th&gt;
&lt;th&gt;Limitation (Responsibility)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Decision Support&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Generates options and analysis rapidly&lt;/td&gt;
&lt;td&gt;Cannot endorse or guarantee outcomes; doesn’t understand ethics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Consistency&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Follows rules consistently in code generation&lt;/td&gt;
&lt;td&gt;Lacks judgment; blindly follows flawed instructions without question&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Speed&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Suggests fixes or configs in seconds&lt;/td&gt;
&lt;td&gt;May miss context that a human would catch; not liable for mistakes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;As the Lumenalta report on AI limits notes, &lt;em&gt;“high-stakes work still depends on judgment about tradeoffs, ambiguity, and consequences. AI can support that work. It will not own it well.”&lt;/em&gt;. AI can accelerate the “explore” phase (e.g., propose database schemas or microservice structures), but the final decision (“commit” phase) remains human-led.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lessons for Developers
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Always review and test:&lt;/strong&gt; Never merge AI-generated code or configs unreviewed, especially for critical paths. Pair AI with code reviews, automated tests, and staging deployments.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Human-in-the-loop:&lt;/strong&gt; Design systems so that important decisions (security, product priorities, unusual conditions) require a human sign-off. As one AI executive advises: &lt;em&gt;“treat outputs as hypotheses, keep decision-making human-led”&lt;/em&gt;.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clear ownership:&lt;/strong&gt; In agile teams, use clear ownership or responsibility matrices. Document which team/person is responsible for each AI-assisted component. This way, when an issue arises, it doesn’t become “AI’s fault,” it’s traceable to the responsible owner.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By acknowledging that AI isn’t accountable, teams avoid the trap of over-reliance. AI is a tool that suggests – but we, the developers and architects, must decide and own the results.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. AI Can’t Replace Human Creativity
&lt;/h2&gt;

&lt;p&gt;AI can &lt;em&gt;recombine&lt;/em&gt; existing code patterns in novel ways, but it does &lt;strong&gt;not originate new ideas&lt;/strong&gt; with intent the way developers and designers do. It might surprise you with an unusual code snippet, but that’s remixing what it “saw” during training. As one expert pithily puts it, &lt;em&gt;“AI recombines patterns. Humans create with purpose, emotion, and narrative.”&lt;/em&gt;. &lt;/p&gt;

&lt;h3&gt;
  
  
  Creativity in Software Engineering
&lt;/h3&gt;

&lt;p&gt;Consider a developer brainstorming a new feature or user story. They think of product vision, user needs, and constraints. They sketch architectures, whiteboard ideas, and iterate. AI can help flesh out details – for instance, suggesting class structures or API endpoints – but it cannot conceive the original vision or the “why” behind the feature.&lt;/p&gt;

&lt;p&gt;For example, writing a complex Node.js application often involves creative design: deciding the right microservices boundaries, naming conventions, or novel algorithms. An AI model can suggest code snippets (like a clever function), but it doesn’t understand the &lt;em&gt;problem domain&lt;/em&gt;. It doesn’t know your startup’s unique value proposition or the UX story your frontend needs. It only knows patterns.&lt;/p&gt;

&lt;p&gt;One way to see this is how AI deals with unexpected situations. The Lumenalta analysis gives a marketing use case: &lt;em&gt;AI can generate campaign variants and test subject lines quickly. The same system will not recognize that a market shift makes the campaign premise wrong, or that a customer reaction signals a brand risk that historical data cannot capture.&lt;/em&gt; The AI is excellent at extending existing patterns, but it &lt;em&gt;“does not originate a better frame on its own.”&lt;/em&gt; It takes a human to see beyond the data and pivot creatively.&lt;/p&gt;

&lt;p&gt;Even in coding, creativity shows up in choosing algorithms or patterns. Given two approaches, an engineer might innovate a hybrid or use domain knowledge. AI will tend to use common design patterns. It might suggest a decorator or service for a NestJS app, but it won’t invent a brand-new architecture style or fix a deep UX issue in ways never documented.&lt;/p&gt;

&lt;h3&gt;
  
  
  AI’s Strengths vs. Weaknesses (Creativity)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Capability&lt;/th&gt;
&lt;th&gt;AI Strength&lt;/th&gt;
&lt;th&gt;Limitation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Code Generation&lt;/td&gt;
&lt;td&gt;Generates boilerplate and known patterns fast&lt;/td&gt;
&lt;td&gt;Lacks original intent or problem framing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Problem Solving&lt;/td&gt;
&lt;td&gt;Offers multiple solution variants&lt;/td&gt;
&lt;td&gt;Doesn’t understand when to break the mold&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Innovation&lt;/td&gt;
&lt;td&gt;Can suggest combinatorial recombinations&lt;/td&gt;
&lt;td&gt;“Surprising output” comes from scale, not true novel ideas&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In fact, a data scientist notes that the common saying &lt;em&gt;“AI can’t be creative – it only recombines what already exists”&lt;/em&gt; has truth, but also nuance. Modern LLMs with huge context windows &lt;em&gt;can&lt;/em&gt; produce surprisingly novel-seeming outputs (thanks to exploring massive combinatorial spaces). But those surprises are still rooted in the training data, not some internal “imagination.” &lt;/p&gt;

&lt;h3&gt;
  
  
  Lessons for Developers
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Focus AI on repetitive creativity:&lt;/strong&gt; Let AI handle rote creativity tasks (e.g. generating code templates, unit test stubs, or design variations). For truly open-ended innovation, rely on human brainstorming.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use AI to augment, not replace design:&lt;/strong&gt; For example, ask an LLM to propose NestJS service names or database schema from a docstring – it can speed things up. But always vet them: humans must ensure these proposals fit the product’s unique needs.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Break down big problems:&lt;/strong&gt; If AI suggestions seem “stuck,” break the task into smaller parts. Then use your own creativity for the overall structure and let AI assist with the details.
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By embracing that creativity is ultimately human, teams can use AI to rapidly prototype and experiment, while reserving the “big idea” work for developers’ insight.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. AI Can’t Build Genuine Human Relationships
&lt;/h2&gt;

&lt;p&gt;No matter how friendly a chatbot sounds, it isn’t a human colleague or friend. Machines lack &lt;strong&gt;authentic empathy, trust, and emotional intelligence&lt;/strong&gt;. As a therapist writes, AI &lt;em&gt;“aids with facts but lacks emotional depth and intuition. Professionals offer empathy and care that AI cannot replicate.”&lt;/em&gt;. In software teams, this means AI cannot truly motivate developers, resolve interpersonal conflicts, or mentor junior engineers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Team and Customer Context
&lt;/h3&gt;

&lt;p&gt;On a project, relationships matter. A senior developer encourages a newcomer, understands frustration, and adapts feedback to their learning style. An AI code reviewer might suggest changes, but it won’t pick up on a dev’s confusion or morale. It can’t shake hands, share a joke, or build trust after a deployment outage. In DevOps incidents, an on-call engineer consoles stakeholders, while AI tools only log errors.&lt;/p&gt;

&lt;p&gt;In customer-facing systems (like chatbots), scripted “compassion” is limited. A customer unhappy with a bug needs understanding; an AI response can sound polite, but it doesn’t actually feel urgency or remorse. If a chatbot just repeats instructions, the user feels unheard. Real human support engineers will empathize and maybe go the extra mile. This reflects the Psychology Today point that &lt;em&gt;“genuine connection… [from AI] is missing”&lt;/em&gt; – when people use digital support, &lt;em&gt;“the missing piece? Real connection”&lt;/em&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  AI’s Strengths vs. Weaknesses (Relationships)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;AI Role (Strength)&lt;/th&gt;
&lt;th&gt;Limitation (Relationships)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;User Engagement&lt;/td&gt;
&lt;td&gt;Can handle routine queries quickly&lt;/td&gt;
&lt;td&gt;Lacks true empathy; misses unspoken cues&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Team Support&lt;/td&gt;
&lt;td&gt;Provides answers or suggestions 24/7&lt;/td&gt;
&lt;td&gt;Cannot inspire, trust, or genuinely motivate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Social Interaction&lt;/td&gt;
&lt;td&gt;Can mimic friendly tone in text&lt;/td&gt;
&lt;td&gt;Has no self, no genuine care or understanding&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Rafiq Wayani, a leadership coach, lists candidly that &lt;em&gt;“AI cannot build relationships: Business is still built on trust… No algorithm can replicate genuine human connection”&lt;/em&gt;. In other words, while AI can simulate conversation, it can’t &lt;em&gt;feel&lt;/em&gt; the emotional currents between people. It won’t notice if a team member is anxious, nor remember a manager’s leadership style to tailor its output.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lessons for Developers
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use AI for tasks, but invest in people skills:&lt;/strong&gt; Let chatbots handle FAQs or generate resource links, freeing time for your team to focus on empathetic communication and collaboration.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Preserve human contact points:&lt;/strong&gt; For example, in customer support, use AI to draft responses but have a human review them. Or use AI to process logs and let engineers do the actual user outreach.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build trust in processes:&lt;/strong&gt; When adopting AI tools (like Copilots or code reviews), be transparent. Don’t promise the AI &lt;em&gt;understands&lt;/em&gt; your code; instead say it &lt;em&gt;offers suggestions&lt;/em&gt;. This way developers feel in control, maintaining trust in the tool.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ultimately, AI can aid productivity and consistency, but the “glue” of any engineering effort remains human relationships and trust. Machines support us; people support each other.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. AI Can’t Decide What Should Be Built
&lt;/h2&gt;

&lt;p&gt;Perhaps most fundamentally: AI can’t &lt;em&gt;decide the vision&lt;/em&gt;. It doesn’t know your users’ deepest needs, your company’s mission, or which product idea will really move the needle. No matter how much data it has, it can’t set priorities or conceive what to build first. &lt;/p&gt;

&lt;h3&gt;
  
  
  Strategic Decision-Making
&lt;/h3&gt;

&lt;p&gt;In startup or product work, deciding &lt;em&gt;what to build&lt;/em&gt; involves strategy, ethics, and foresight – “dirty work” beyond data patterns. Craig Sullivan summarizes it: &lt;em&gt;“No LLM is going to tell you which deal to prioritize… or when a prospect needs a human conversation instead of another automated touchpoint… Treat outputs as hypotheses, keep decision-making human-led”&lt;/em&gt;. For example, an LLM can analyze customer feedback for topics, but it won’t know that shifting market trends mean the feedback signals a new feature idea. Only human product managers with market context can interpret that.&lt;/p&gt;

&lt;p&gt;In system architecture, this shows up too. AI might generate microservice code or suggest a database (OK for boilerplate), but choosing between architectural paradigms (e.g. REST vs. GraphQL, monolith vs. microservices) requires understanding team skills, legacy constraints, and future roadmap. AI lacks that broader perspective and cannot weigh the trade-offs beyond statistical patterns.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where AI Helps and Falls Short
&lt;/h3&gt;

&lt;p&gt;AI is great at the &lt;strong&gt;exploration phase&lt;/strong&gt; of problem solving – generating lots of ideas or code variants. But it’s weak at &lt;strong&gt;convergence&lt;/strong&gt;. As one engineer puts it, &lt;em&gt;“LLMs are great at divergence (getting more ideas), and weak at convergence (picking the right one).”&lt;/em&gt; Even with large-scale outputs, picking the best solution relies on human context: team culture, performance constraints, and politics.&lt;/p&gt;

&lt;p&gt;For example, RAG or agents can gather and summarize information to inform a decision – they scale knowledge. But they won’t say “build this feature because we’ll capture X market.” They lack business acumen and on-ground insight. An LLM might suggest moving a block of code to a new file; it cannot suggest building a whole new component because “Team A is overworked, and Team B has expertise.”&lt;/p&gt;

&lt;h3&gt;
  
  
  Lessons for Developers
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Keep humans in the loop for vision:&lt;/strong&gt; Always have senior engineers or product owners steer the high-level roadmap. Use AI to gather data or simulate user scenarios, then decide the plan yourself.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use AI as a &lt;em&gt;tool&lt;/em&gt;, not a boss:&lt;/strong&gt; When an AI suggests many solutions, treat them as hypotheses. Score them using metrics you care about and choose with human judgment.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Invest in domain expertise:&lt;/strong&gt; Your knowledge of customers, business, and technical debt are irreplaceable. As one summary reminds us: &lt;em&gt;“Jobs evolve. Judgment does not disappear.”&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In short, AI can never replace the creative spark and responsibility of deciding &lt;em&gt;what&lt;/em&gt; to build. It lacks purpose. That means it’s up to the team to set direction and ensure technology serves human goals.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Context is limited:&lt;/strong&gt; LLMs handle text in isolation and have finite memory (context window). Use techniques like RAG to inject relevant data, and always test AI-generated code in the full system context.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI is &lt;em&gt;assistive&lt;/em&gt;, not accountable:&lt;/strong&gt; AI can generate code or suggestions, but it &lt;em&gt;cannot&lt;/em&gt; own mistakes or make judgment calls. Human engineers must review outputs and bear responsibility.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Creativity stays human:&lt;/strong&gt; AI recombines patterns at scale but does not innovate with intent. Use AI for prototyping, but rely on developers for vision and strategy.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Relationships need humans:&lt;/strong&gt; No matter how “smart,” AI lacks real empathy or emotional intelligence. Preserve human interaction in teamwork and user support.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Humans decide what to build:&lt;/strong&gt; AI can suggest and explore, but picking the right path requires human judgment and domain knowledge. Treat AI output as &lt;em&gt;hypotheses&lt;/em&gt;, not decisions.&lt;/li&gt;
&lt;/ul&gt;




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

&lt;p&gt;AI and LLM tools are reshaping software development – they’re incredibly powerful assistants. They can write boilerplate, suggest fixes, summarize logs, and even generate initial system designs. But they have clear limitations. They &lt;strong&gt;don’t understand&lt;/strong&gt; systems the way we do, they &lt;strong&gt;don’t own&lt;/strong&gt; outcomes, they &lt;strong&gt;aren’t truly creative&lt;/strong&gt;, they &lt;strong&gt;can’t form real relationships&lt;/strong&gt;, and they &lt;strong&gt;won’t set the product vision&lt;/strong&gt;. These aren’t flaws; they’re just the nature of current AI. &lt;/p&gt;

&lt;p&gt;As developers, our mission is to harness AI’s strengths while managing its weaknesses. That means leveraging prompt engineering, retrieval systems, and CI/CD pipelines to get the best outputs – while always keeping humans in the loop for testing, oversight, and final decisions. When a chatbot “hallucinates,” or Copilot writes vulnerable code, it’s a reminder that &lt;em&gt;context&lt;/em&gt;, &lt;em&gt;responsibility&lt;/em&gt;, and &lt;em&gt;human values&lt;/em&gt; still require us. &lt;/p&gt;

&lt;p&gt;The future is not &lt;em&gt;humans vs AI&lt;/em&gt;, but &lt;em&gt;humans with AI&lt;/em&gt;. By understanding these five boundaries, we can use AI to accelerate development without losing sight of what only people can do. As one developer put it, AI “accelerates the explore phase; humans own the commit phase”. Remember that rule of thumb, and AI becomes an advantage – not a liability.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Call to Action:&lt;/strong&gt; Have you encountered situations where an AI’s suggestion went wrong? Or where human judgment caught an AI error? Share your experiences and thoughts in the comments below. Let’s learn from each other about the real-world limits of AI in development.&lt;/p&gt;




&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: Can AI ever fully replace software developers?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
A: Not in the foreseeable future. Current AI lacks true understanding, ethics, and responsibility. It’s best used as a coding assistant. Developers still need to review, integrate, and make judgment calls about AI outputs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How do we guard against AI “hallucinations” in code?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
A: Always validate AI-generated code with tests and type checks. Use retrieval-augmented generation (RAG) and prompt constraints to ground answers. Never trust the AI blindly – treat it as a collaborator, not an oracle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What is the role of prompt engineering?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
A: Prompt engineering helps focus the AI on desired outcomes. By crafting clear, specific prompts (and including context via system messages or RAG), you improve the quality of AI assistance. However, even perfect prompts can’t teach the model empathy or responsibility.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: If AI can’t be creative, why do some say it is creative?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
A: AI can generate novel-looking combinations because it has seen vast amounts of data. Technically, it’s remixing patterns at scale. Some surprising results can feel creative, but AI doesn’t conceive ideas from first principles or feelings. It lacks intent and can’t do &lt;em&gt;strategic&lt;/em&gt; innovation without human guidance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How should DevOps teams handle AI tools?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
A: Treat AI tools (like infrastructure-as-code generators or monitoring bots) as helpers. Use them to automate routine tasks, but maintain human oversight on critical deployment steps. For example, an AI can draft a Kubernetes manifest, but a human must review security settings and verify it in a dev cluster.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Can prompt-based agents (AI that calls APIs and tools) overcome these limitations?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
A: Agents can mimic multi-step reasoning and use tools, improving task execution. But they still rely on underlying LLMs. They don’t truly understand business context or own outcomes. They reduce some manual work but raise new concerns (e.g., uncontrolled API usage). Even advanced agents need carefully defined boundaries and human supervision.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How does AI impact architecture and system design?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
A: AI can suggest design patterns and even draw diagrams from high-level descriptions, which is useful for brainstorming. But choosing the right architecture (monolith vs microservices, specific frameworks, database technologies) requires weighing long-term factors. AI suggestions should be one input, not a final design, as it won’t know your team’s full constraints and goals.&lt;/p&gt;

&lt;p&gt;Feel free to drop additional questions or comments below. We encourage discussion on how to balance AI tools with human expertise in your development workflow!&lt;/p&gt;

</description>
      <category>node</category>
      <category>nestjs</category>
      <category>nextjs</category>
      <category>javascript</category>
    </item>
    <item>
      <title>5 Things AI Cannot Do at Node.js</title>
      <dc:creator>DevUnionX</dc:creator>
      <pubDate>Mon, 13 Jul 2026 19:21:28 +0000</pubDate>
      <link>https://dev.to/devunionx/5-things-ai-cannot-do-at-nodejs-184n</link>
      <guid>https://dev.to/devunionx/5-things-ai-cannot-do-at-nodejs-184n</guid>
      <description>&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://play.google.com/store/apps/details?id=com.flip_duel_card_battle" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fplay-lh.googleusercontent.com%2FQKf5gbEwnY_z_Yvfgb5oGDsRHphvEFFZ12gxkXdve76F9KWDZrACEjKzynQt4kNIUf422odV07pRCHKt20B1" height="512" class="m-0" width="512"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://play.google.com/store/apps/details?id=com.flip_duel_card_battle" rel="noopener noreferrer" class="c-link"&gt;
            Flip Duel: Online Card Game - Apps on Google Play
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            1v1 card duel! Bluff, beat your rival. Combine your cards, raid the dungeon!
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.gstatic.com%2Fandroid%2Fmarket_images%2Fweb%2Ffavicon_v3.ico" width="32" height="32"&gt;
          play.google.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  Executive Summary
&lt;/h2&gt;

&lt;p&gt;This report examines five fundamental tasks and limitations that artificial intelligence &lt;strong&gt;cannot reliably perform&lt;/strong&gt; for Node.js developers. AI models generally operate through statistical approaches based on large datasets, which can lead to problems such as &lt;strong&gt;factual mismatch (hallucination)&lt;/strong&gt;, loss of context, and security vulnerabilities. For example, an LLM (Large Language Model) may suggest a piece of code that looks attractive but is incorrect or fabricated. In the Node.js context, errors of this kind can create serious problems such as blocking the single-threaded event loop or degrading performance. Throughout the report, we address the causes of each limitation, its impact on Node.js, and the design patterns that can be used to mitigate it. We illustrate these solutions with code examples and architectural diagrams, and we also offer insights into future research trends along with recommended resources. Ultimately, the report emphasizes that in order to maximize the benefits of AI tools, developer oversight, testing, and sound architectural planning remain critically important.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Hallucination and Accuracy Problems
&lt;/h2&gt;

&lt;p&gt;Although current AI models produce convincing, template-consistent answers, they &lt;strong&gt;provide no guarantee of accuracy&lt;/strong&gt;. Because LLMs rely on statistical language modeling, they frequently produce the phenomenon known as "hallucination" — that is, they can generate fabricated answers that are entirely inconsistent with the user's request. For example, when given a legal writing assignment, an LLM may produce fabricated citations and unsupported arguments on a topic; similarly, when asked for Node.js code, it may output code containing nonexistent functions, missing parameters, or faulty logic. Hallucinations generally stem from biases or contradictory information in the model's training data. An arXiv study showed that roughly 95% of the errors arising in LLM-assisted code generation were caused by incorrect functionality (failure to meet requirements). This means that when you attempt to produce "production code," you will encounter unexpected errors and unreliable output.&lt;/p&gt;

&lt;p&gt;For Node.js developers, this creates two fundamental risks. First, if AI-generated code is used directly, the application may experience &lt;strong&gt;functional errors&lt;/strong&gt; and unexpected crashes. For example, even when AI-assisted code constructs an API request, it may fail to perform proper authentication or parameter validation; or an operation may return an unexpected result. Such code may not even appear to be broken while running, yet the application's business logic breaks down or security vulnerabilities emerge. Second, hallucinations make code &lt;strong&gt;harder to maintain&lt;/strong&gt;. When complex, unnecessary, or unexplainable pieces of code are generated, maintenance becomes more expensive over time and developer productivity declines. The ambiguities contained in AI-generated code require extra testing and review to separate out the real bugs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mitigation Approaches:&lt;/strong&gt; In the Node.js world, there are various approaches to overcoming this problem. First and foremost, AI-generated code must pass through human oversight and be &lt;strong&gt;verified with unit tests&lt;/strong&gt;. Within the code flow, critical outputs can be checked using &lt;strong&gt;assertion libraries&lt;/strong&gt; or a type system (TypeScript). In addition, &lt;strong&gt;balancing (prompt engineering)&lt;/strong&gt; methods can be applied: specifying all necessary context and constraints in detail before the code prompt can reduce the likelihood of hallucination. For example, OpenAI's &lt;code&gt;system&lt;/code&gt; message can be used to describe the code environment or fixed variables. A Node.js example is given below:&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="c1"&gt;// Example: AI call (OpenAI API)&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;OpenAIApi&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;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;openai&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;OpenAIApi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Configuration&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_KEY&lt;/span&gt; &lt;span class="p"&gt;}));&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;generateValidatedCode&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="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// First, provide context by adding a test scenario&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt; &lt;span class="o"&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;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;system&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;You are a JavaScript expert and must produce only correct, tested code.&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="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&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="p"&gt;];&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&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;openai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createChatCompletion&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="s1"&gt;gpt-4&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Usage&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;generateValidatedCode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Write a function that adds two numbers.&lt;/span&gt;&lt;span class="dl"&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;code&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this approach, the responses coming from the AI can become more reliable in terms of accuracy. Furthermore, with methods similar to &lt;strong&gt;Chain of Thought&lt;/strong&gt;, the AI can be encouraged to evaluate the logical steps of its answers one by one and correct them where necessary. Finally, methods such as &lt;em&gt;Retrieval-Augmented Generation (RAG)&lt;/em&gt; — which draw on existing databases or documentation — also reduce the risk of hallucination; when responding to a request, the AI can first consult sources such as a manual or a code repository.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Architectural and Performance Reasoning
&lt;/h2&gt;

&lt;p&gt;Node.js's single-threaded event loop architecture provides a major advantage under high &lt;strong&gt;I/O-intensive&lt;/strong&gt; loads, but it introduces architectural constraints for CPU-intensive operations. AI tools generally offer a standard solution when writing code, yet they &lt;strong&gt;cannot foresee&lt;/strong&gt; a Node application's architectural decisions or performance bottlenecks. As noted on the Full Scale blog, AI can code an Express route; however, it &lt;strong&gt;cannot recognize that this route contains a blocking file system call, or that it will crash due to a missing &lt;code&gt;await&lt;/code&gt;&lt;/strong&gt;. In other words, AI produces code but cannot analyze that code's effects under real system load. For Node.js developers, this can lead to performance bugs and scalability problems.&lt;/p&gt;

&lt;p&gt;For example, when an endpoint runs too slowly, an experienced developer immediately suspects that &lt;strong&gt;the event loop is being blocked&lt;/strong&gt;. But even when an AI model sees a blocking call such as &lt;code&gt;fs.readFileSync&lt;/code&gt; in the code, it "cannot understand" the load this will place on the system and may proceed with its recommendation regardless. In such a case, the application struggles to handle all incoming requests at once. Similarly, situations such as a memory leak or an &lt;code&gt;unhandledRejection&lt;/code&gt; also cannot be foreseen by AI; these errors only emerge through real production experience and require a human eye.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Effects on Node.js:&lt;/strong&gt; This limitation leads to the following effects on the architecture of Node.js services:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Single-Threaded Limitation:&lt;/strong&gt; If any operation blocks the event loop, the entire application slows down. For this reason, &lt;em&gt;synchronous&lt;/em&gt; methods should be avoided for critical operations, even in scenarios recommended by AI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resource Management:&lt;/strong&gt; Efficient memory usage is required during large file uploads or multiple concurrent user requests. AI-generated code may overlook this complex flow and backpressure management, which can lead to memory overflows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error Recovery and Logging:&lt;/strong&gt; Understanding the causes of errors that occur in distributed systems is difficult for AI. In Node.js, recovery mechanisms for unexpected shutdowns (e.g., a job queue, restart strategies) rely on developer experience; an LLM cannot foresee these automatically.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Practical Solutions and Design Patterns:&lt;/strong&gt; To mitigate these problems, developers use the following methods:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Using Threads:&lt;/strong&gt; CPU-intensive tasks (&lt;code&gt;image processing&lt;/code&gt;, &lt;code&gt;video encoding&lt;/code&gt;, etc.) should not block the main event loop. Instead, Node.js's &lt;a href="https://nodejs.org/api/worker_threads.html" rel="noopener noreferrer"&gt;worker_threads&lt;/a&gt; module can be used. For example, the simple Node.js code below runs a heavy operation in a thread so that the event loop is not blocked:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// worker-pool.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Worker&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;worker_threads&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;runHeavyTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reject&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;worker&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;Worker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./worker-task.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;workerData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nx"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;once&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;message&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;once&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reject&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="c1"&gt;// worker-task.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;parentPort&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;workerData&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;worker_threads&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// CPU-intensive computation&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computeFibonacci&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;workerData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;parentPort&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Using Multiple Processes (Cluster):&lt;/strong&gt; To take advantage of multiple CPU cores, the Node.js &lt;code&gt;cluster&lt;/code&gt; module can be used. This module creates multiple instances (Workers) of the application and distributes incoming requests among them to achieve parallelism. For example:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// cluster-example.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;cluster&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:cluster&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;availableParallelism&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:os&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:http&lt;/span&gt;&lt;span class="dl"&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;cluster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isPrimary&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;cores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;availableParallelism&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;cores&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&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;cluster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fork&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;cluster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;exit&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;worker&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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="s2"&gt;`Worker &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;worker&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;pid&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; died`&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="c1"&gt;// Each worker opens its own HTTP server&lt;/span&gt;
  &lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createServer&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Hello World&lt;/span&gt;&lt;span class="se"&gt;\n&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;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8000&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;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stream-Based I/O Processing:&lt;/strong&gt; When processing large data loads (files, network packets), using the Node.js stream API keeps memory usage constant. For example, to process a file line by line, code like the following can be used:
&lt;/li&gt;
&lt;/ul&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createReadStream&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;readline&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;readline&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;processLargeFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&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;fileStream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createReadStream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&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;reader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;readline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createInterface&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;fileStream&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;line&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// A small line is processed each time&lt;/span&gt;
    &lt;span class="nf"&gt;handleLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;line&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;In this way, since the entire file is not loaded into memory, the risk of an &lt;strong&gt;out-of-memory&lt;/strong&gt; overflow decreases. Likewise, large data chains can be built with &lt;code&gt;pipeline&lt;/code&gt; and &lt;code&gt;Transform&lt;/code&gt; streams.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Performance Testing and Profiling:&lt;/strong&gt; Rather than using the AI's code suggestions directly, load tests and profiling tools (e.g., &lt;code&gt;clinic.js&lt;/code&gt;, V8 profilers) are used to detect and optimize bottlenecks. A Node developer should test an AI-suggested code snippet with these tools before moving it into production.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The patterns above carry developers' &lt;strong&gt;hierarchical decision-making abilities&lt;/strong&gt; (which task should be solved with which method, and what is suitable for whom) beyond the reach of AI. Although AI accelerates coding, the weakest link in the system can only be detected through human oversight. Design quality, the selection of the right asynchronous patterns, and efficient resource usage ultimately remain dependent on developers' expertise.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Security and Contextual Knowledge Gaps
&lt;/h2&gt;

&lt;p&gt;AI models are generally trained on &lt;strong&gt;publicly available and historical code datasets&lt;/strong&gt;. However, these datasets often contain outdated or weak security practices. Because AI is fundamentally a statistical model, it learns the most frequently encountered code patterns; yet it has no knowledge of how secure those learned patterns are or whether they conform to an application's specific security requirements. This means that &lt;strong&gt;even when it runs seemingly functional code&lt;/strong&gt;, it will bypass hidden security layers.&lt;/p&gt;

&lt;p&gt;For example, an AI tool may fail to add input validation when constructing a SQL query, because it is difficult for it to understand the situations in which input sanitization is required. A study cited above concludes that "AI models generate code without deeply understanding your application's security needs, business logic, or architecture." For a developer, this carries the risk that the generated code may contain &lt;strong&gt;injection points (injection, XSS, CSRF, etc.)&lt;/strong&gt;. According to a Veracode report, in 2025, 45% of AI-generated code contained one of the OWASP Top 10 security vulnerabilities. For JavaScript specifically, this rate was reported as 43%.&lt;/p&gt;

&lt;p&gt;Possible effects for Node.js developers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Endpoint Security:&lt;/strong&gt; AI-assisted code may fail to add proper validation when receiving user data such as &lt;code&gt;req.body&lt;/code&gt; on the server side. This leaves the door open to malicious requests. In Node.js applications, libraries such as &lt;code&gt;helmet&lt;/code&gt; and &lt;code&gt;express-validator&lt;/code&gt; should be used; in addition, encryption/security functions should be manually reviewed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dependency Chains:&lt;/strong&gt; AI may add npm packages for the functionality it needs. However, the owner or version of these packages may contain security vulnerabilities. When adding a new package, the developer should check the transitive (indirect) dependency chain and perform package signature and CVE scanning. Tools that detect &lt;strong&gt;library vulnerabilities&lt;/strong&gt; (Snyk, npm audit, etc.) should be used in the environment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Personal and Confidential Data:&lt;/strong&gt; AI models cannot check for the presence of a hardcoded secret or confidential information within the code. If a server key or password has been mistakenly placed inside the code, the AI cannot catch this error. On the developer's side, &lt;strong&gt;environment variables&lt;/strong&gt; and secret-management practices (secret manager) should be adopted.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Mitigation Methods:&lt;/strong&gt; To overcome this limitation, the following can be done:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Static and Dynamic Security Analysis:&lt;/strong&gt; All code suggested by AI should be examined with static analysis tools (e.g., ESLint + security rules, SonarQube) and dynamic scans. For example, &lt;strong&gt;OpenAI's security code review API&lt;/strong&gt; or open-source tools can inspect the code. If security tests are integrated into the continuous integration (CI) pipeline, known weaknesses in AI code can be caught in advance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security Libraries and Frameworks:&lt;/strong&gt; Critical operations can be standardized by using security-focused libraries built for Node.js (e.g., &lt;code&gt;express-validator&lt;/code&gt;, &lt;code&gt;csurf&lt;/code&gt;, or &lt;code&gt;bcrypt&lt;/code&gt;). When obtaining output from AI, it is helpful to explicitly request (state in the prompt) that these libraries be used. Whether the code sanitizes user input when receiving it must be checked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security in the Software Development Lifecycle (DevSecOps):&lt;/strong&gt; Adding security after the fact can be difficult when writing code with AI tools. For this reason, &lt;strong&gt;threat modeling&lt;/strong&gt; should be performed at the outset, and the potential attack surfaces should be identified. For example, if a chatbot is being developed, precautions such as reviewing database access queries and adding XSS protection should be planned from the start.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Role-Based Controls:&lt;/strong&gt; Some decisions require human judgment. For example, for code snippets that require user consent or legal compliance, human intervention should be preferred over an automated solution. AI output can also be run through human-approved security code reviews (peer review).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In general, however much AI accelerates code writing, "automatic convenience" can be a trap when it comes to security. Ensuring flawless security practices is still the job of experienced developers and security specialists. In the Node.js context, only humans can notice and fix the potentially dangerous aspects of code. For this reason, AI code should be treated as a "first draft," and security checks should never be skipped.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Lack of Context and Long-Term Memory
&lt;/h2&gt;

&lt;p&gt;Modern LLMs are quite effective within &lt;strong&gt;short-term context&lt;/strong&gt; (the context window), but they have limited memory. A model's context window is bounded by the amount of text it can "remember" at once. For example, GPT-4's context length is not billions of tokens — it is limited. This prevents the model from considering a very long conversation history or a large code repository all at once. In long interactions with a user or in large datasets, the AI may forget information that was passed in an earlier glance.&lt;/p&gt;

&lt;p&gt;In a Node.js development environment, this shortcoming is felt in the following situations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stateful Context:&lt;/strong&gt; Cross-process or long-term session information is not retained by the AI. For example, a chatbot that does not know what a user did in previous requests may, over time, forget information it needs to remember. As a solution, Node.js applications should store state in a &lt;strong&gt;session store&lt;/strong&gt; or in databases and re-supply this state to the AI at each interaction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Project and Code Recall:&lt;/strong&gt; It is not practical to give an entire large codebase to an AI model. The AI may not remember the specific configurations used in your Node.js project (file paths, custom packages). In this case, it may be necessary to summarize important configuration information (e.g., database schemas) in advance and add it to the input, or to have the AI read part of the project incrementally. Otherwise, the model may make incorrect suggestions by overlooking the current code's dependencies or architecture.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As IBM has noted, an LLM's context window is like "working memory"; if too much information is fed in, the parts that cannot fit into the model &lt;em&gt;are consumed, or must be summarized and passed along&lt;/em&gt;. This limits how much information the AI can learn at once. In Node.js services, when there are continuously running long-lived tasks or multi-step workflows, this situation can lead to synchronization problems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Precautions and Patterns:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Chunking:&lt;/strong&gt; Breaking long operations into small pieces enables the AI to remember the context in each one. For example, if a large dataset is being processed, a summary query can be made first; the AI is then guided to answer each step incrementally.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Persistent Memory Mechanisms:&lt;/strong&gt; For information such as conversation history or user preferences, a long-term data storage system can be used. For example, a Node.js application can save user messages to a &lt;strong&gt;vector database&lt;/strong&gt; or a simple database. On the next AI call, the relevant historical information is queried again and added as model input. This is part of the &lt;strong&gt;Retrieval-Augmented Generation&lt;/strong&gt; (RAG) strategy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model Updating and Incremental Learning:&lt;/strong&gt; Some approaches suggest reaching a result without retraining the AI each time by adding a dedicated RAG server or memory layer. For example, it is possible to set up a Node.js API that records user feedback and turn this data into a data source for the project. In the future, giving models the ability to "retain what they learned in a task" is also a research topic.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In short, AI models are limited in terms of &lt;strong&gt;short-term memory&lt;/strong&gt;. Rather than continuously feeding the AI all the information the application needs, Node.js developers should design their systems to keep important state externally and feed it in when needed. This way, both context loss is prevented and the model is spared from information overload.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Lack of Creativity and Human Judgment
&lt;/h2&gt;

&lt;p&gt;AI can produce content that appears "creative" to the extent of the data it was trained on; however, it does not possess a genuine capacity for original artistic or conceptual creation. AI draws inferences from templates it has previously seen or been given; it cannot create a solution from scratch, on its own, within an entirely new paradigm. For example, when a design problem or an original algorithmic optimization is required, human creativity still lies beyond AI. As IBM has said, "creativity is still a final frontier for artificial intelligence."&lt;/p&gt;

&lt;p&gt;Similarly, AI models &lt;strong&gt;lack ethical and emotional reasoning&lt;/strong&gt;. For instance, a Node.js chatbot may write sentences that appear to offer emotional support to a distressed user, but this is not genuine empathy; the AI does not have the ability to "understand sadness." In decision-support systems or in areas that touch on human matters, human intervention is still required. This situation has the following effects on Node.js development processes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Design Understanding:&lt;/strong&gt; AI can make suggestions about code or architecture, but it does not grasp the reasons behind design choices. For example, high-level decisions such as why a microservice architecture should be preferred, or that an endpoint is actually unnecessary, do not come from AI. This kind of strategic "completing the scenario" task relies on human expertise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Developer Experience and the Human Factor:&lt;/strong&gt; AI is limited in human-centered matters such as an application's ease of use, workflow design, or user interface. For example, when designing a new API, assessments such as "did this make things harder for the developer to use" are related to human perception. In the Node.js community, an application's fit with teams' workflows is critical — often more so than the technical perfection of the code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Learning and Adaptation:&lt;/strong&gt; AI cannot instantly learn from experience and adapt (no incremental learning). A developer, by contrast, can change their code by drawing lessons from problems experienced in a live system. An LLM cannot add this kind of feedback to its own model. Consequently, it requires human analysis again for each new problem.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Precautions:&lt;/strong&gt; To address this weakness of AI, developers need to reinforce creative thinking and human judgment. AI outputs should be seen as mere suggestions and used as a &lt;strong&gt;"decision-support" tool&lt;/strong&gt;. For example, if a code template is provided, the developer should confirm whether it is appropriate based on their own experience. Simply &lt;strong&gt;clarifying questions&lt;/strong&gt; is also important: before a business requirement description is given to the AI, one should make sure it is fully understood (which in turn requires human communication). Full Scale experts emphasize this point by saying, "reviewing and correcting AI outputs is like the leadership a senior engineer exercises." A Node developer should show the same diligence, questioning the AI's statements with "okay, but is this really correct?"&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparative Summary Table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Limitation / Task&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Impact on Node.js&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Solution / Mitigation Approach&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Information Hallucination and Accuracy&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;AI-assisted code may produce &lt;strong&gt;incorrect functional results&lt;/strong&gt;; the burden of readability and maintenance increases.&lt;/td&gt;
&lt;td&gt;Code review, unit testing, type checking. Developer oversight and feeding current context via &lt;strong&gt;Retrieval&lt;/strong&gt;. Human-approved vs. automated checks against AI output.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Performance / Event Loop&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;When the single thread is blocked, the entire Node service slows down; CPU-heavy operations tie up the processor.&lt;/td&gt;
&lt;td&gt;Parallelizing heavy workloads with &lt;strong&gt;worker_threads&lt;/strong&gt; or &lt;strong&gt;cluster&lt;/strong&gt;. Processing large data piece by piece with the Stream API.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Security and Privacy&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;AI code often does not include security checks; it may harbor undetected &lt;strong&gt;security vulnerabilities&lt;/strong&gt; (e.g., XSS, SQL inj.).&lt;/td&gt;
&lt;td&gt;Static/dynamic security scanning, code review. Security-focused libraries (express-validator, csurf, etc.). Dependency auditing, version updates.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Context and Long-Term Memory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The LLM's memory limit causes past context to be lost in long conversations or large data processing. Process management becomes outdated.&lt;/td&gt;
&lt;td&gt;Storing conversation/session data in a database. Using external memory via &lt;em&gt;Retrieval-Augmented Generation&lt;/em&gt;. Feeding the LLM by breaking requests into small pieces.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Creativity / Ethical Judgment&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;AI cannot produce innovative architectural or design solutions, nor make ethical or emotional decisions. Developer inference is essential.&lt;/td&gt;
&lt;td&gt;Design decisions and user experience under human oversight. AI output should always be treated as a "draft." Requirements should be re-examined and human judgment brought in.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Architecture Example Diagram
&lt;/h2&gt;

&lt;p&gt;The mermaid diagram below is an example of a typical architectural pattern showing interaction with an AI service in Node.js applications. Requests from the client are routed to the Node.js API; based on the type of work, the API connects to components such as LLM services, the database, or job queues. Work-intensive tasks are processed in separate services or threads:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
  A[Client Application] --&amp;gt; B[Node.js API Server]
  B --&amp;gt; C[LLM Service]
  B --&amp;gt; D[Database]
  B --&amp;gt; E[Job Queue]
  E --&amp;gt; F[CPU-Heavy Worker]
  C --&amp;gt; D
  style A fill:#D6EAF8,stroke:#1B4F72,stroke-width:2px
  style B fill:#D1F2EB,stroke:#145A32,stroke-width:2px
  style C fill:#F9E79F,stroke:#7D6608,stroke-width:2px
  style D fill:#FADBD8,stroke:#7B241C,stroke-width:2px
  style E fill:#D7BDE2,stroke:#512E5F,stroke-width:2px
  style F fill:#AED6F1,stroke:#154360,stroke-width:2px
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the architecture above, the Node.js API is designed in an &lt;em&gt;asynchronous&lt;/em&gt; (non-blocking) manner. Large data jobs are placed onto the &lt;code&gt;Job Queue&lt;/code&gt; and processed in parallel by background &lt;code&gt;Worker&lt;/code&gt; processes. When AI support is required (e.g., text summarization, analysis), queries are sent to the &lt;code&gt;LLM Service&lt;/code&gt;. This service makes a request to the AI model, receives the response, and forwards it to the API. This design prevents a single service from bearing the entire load and provides scalability.&lt;/p&gt;

&lt;h2&gt;
  
  
  Looking Ahead and Research Directions
&lt;/h2&gt;

&lt;p&gt;In the coming years, many advances are expected in AI technology. Work continues on increasing model context lengths, on &lt;strong&gt;long-term memory&lt;/strong&gt; models, and on enabling AI systems to explain their own decision processes. Researchers are focusing on improving LLMs' real-time learning capabilities and on enhancing secure code generation. For example, chain-of-thought methods reduce hallucinations, while algorithmic learning models may play a role in optimizing code. On the Node.js side, new releases may bring stronger concurrency (advances in worker_threads, new protocols) and built-in security scanning.&lt;/p&gt;

&lt;p&gt;The timeline below shows important milestones in both AI and Node.js:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gantt
  dateFormat  YYYY
  title AI and Node.js Development Timeline
  section AI Developments
    GPT-3 Released         :milestone, 2020,    1d
    ChatGPT Launch         :milestone, 2022,    1d
    GPT-4 Released         :milestone, 2023,    1d
    Hallucination Research :milestone, 2024,    1d
    GPT-5 Anticipated      :milestone, 2025,    1d
  section Node.js Versions
    Node.js 14 LTS         :milestone, 2020,    1d
    Node.js 18 LTS         :milestone, 2022,    1d
    Node.js 20 LTS         :milestone, 2023,    1d
    Node.js 26 (latest)    :milestone, 2026,    1d
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The diagram above includes large models such as GPT-3 (2020) and GPT-4 (2023), as well as new research areas (hallucination categories, etc.). Important Node.js LTS versions have also been added. This technology development line points to upcoming changes for both the AI and Node.js communities. For example, the development of GPT-5's logical reasoning and security orientations may indicate that integration with Node.js applications will become safer. Likewise, in future Node.js releases, stream management and process parallelism may be further improved.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Matt Watson&lt;/strong&gt;, &lt;em&gt;AI-proof Node.js Developer Interview Questions&lt;/em&gt;, Full Scale (June 2026).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Jens Wessling&lt;/strong&gt;, &lt;em&gt;"We Asked 100+ AI Models to Write Code. Here's How Many Failed Security Tests."&lt;/em&gt; Veracode Blog (July 2025).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SoftwareSeni&lt;/strong&gt;, &lt;em&gt;Why 45 Percent of AI Generated Code Contains Security Vulnerabilities&lt;/em&gt; (November 2025).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fang Liu et al.&lt;/strong&gt;, &lt;em&gt;"Beyond Functional Correctness: Exploring Hallucinations in LLM-Generated Code"&lt;/em&gt;, ArXiv (April 2024).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Laurence Santy&lt;/strong&gt;, &lt;em&gt;15 Things AI Can — and Can't Do (So Far)&lt;/em&gt;, Invoca Blog (March 2025).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Veracode 2025 GenAI Code Security Report&lt;/strong&gt;, Veracode (July 2025).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IBM&lt;/strong&gt;, &lt;em&gt;What is a context window?&lt;/em&gt; (IBM Think).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node.js Documentation&lt;/strong&gt;, &lt;em&gt;worker_threads&lt;/em&gt;, &lt;em&gt;cluster&lt;/em&gt;, &lt;em&gt;stream&lt;/em&gt; (v26.5.0).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DEV Community (Atlas Whoff)&lt;/strong&gt;, &lt;em&gt;Node.js Streams: Processing Large Files Without Running Out of Memory&lt;/em&gt; (April 2020).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full Scale Booklet (Matt Watson)&lt;/strong&gt;, &lt;em&gt;Product Driven: Build Something People Want&lt;/em&gt; (2023) — Node.js best practices.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>node</category>
      <category>javascript</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Flip duel</title>
      <dc:creator>DevUnionX</dc:creator>
      <pubDate>Tue, 23 Jun 2026 15:17:07 +0000</pubDate>
      <link>https://dev.to/devunionx/flip-duel-49hj</link>
      <guid>https://dev.to/devunionx/flip-duel-49hj</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/devunionx/new5-things-even-ai-cant-do-fetch-api-328e" class="crayons-story__hidden-navigation-link"&gt;(New!)5 Things Even AI Can't Do, Fetch API&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/devunionx" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3180316%2F804c7ae5-1a93-4c38-b9ec-023a59a621a8.jpg" alt="devunionx profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/devunionx" class="crayons-story__secondary fw-medium m:hidden"&gt;
              DevUnionX
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                DevUnionX
                
              
              &lt;div id="story-author-preview-content-3971645" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/devunionx" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3180316%2F804c7ae5-1a93-4c38-b9ec-023a59a621a8.jpg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;DevUnionX&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/devunionx/new5-things-even-ai-cant-do-fetch-api-328e" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Jun 23&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/devunionx/new5-things-even-ai-cant-do-fetch-api-328e" id="article-link-3971645"&gt;
          (New!)5 Things Even AI Can't Do, Fetch API
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/api"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;api&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/webdev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;webdev&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/programming"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;programming&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/productivity"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;productivity&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/devunionx/new5-things-even-ai-cant-do-fetch-api-328e" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/exploding-head-daceb38d627e6ae9b730f36a1e390fca556a4289d5a41abb2c35068ad3e2c4b5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;5&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/devunionx/new5-things-even-ai-cant-do-fetch-api-328e#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            19 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
    </item>
    <item>
      <title>(New!)5 Things Even AI Can't Do, Fetch API</title>
      <dc:creator>DevUnionX</dc:creator>
      <pubDate>Tue, 23 Jun 2026 15:16:51 +0000</pubDate>
      <link>https://dev.to/devunionx/new5-things-even-ai-cant-do-fetch-api-328e</link>
      <guid>https://dev.to/devunionx/new5-things-even-ai-cant-do-fetch-api-328e</guid>
      <description>&lt;h2&gt;
  
  
  MY GAME PUBLISHED IN PLAY STORE
&lt;/h2&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://play.google.com/store/apps/details?id=com.flip_duel_card_battle" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fplay-lh.googleusercontent.com%2FQiVSCNak_1Y7ZpkDvhy_7UNbA0BGOpwEFEmEPfMdo7UcRvrgf4s-7_JIj-FT2tRUTVdFfs8FPKVW6CGIGg7Ksg" height="512" class="m-0" width="512"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://play.google.com/store/apps/details?id=com.flip_duel_card_battle" rel="noopener noreferrer" class="c-link"&gt;
            Flip Duel: Online Card Battle - Apps on Google Play
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Outsmart rivals in 1v1 card duels. Joker, bluff, ranked PvP. 5 rounds.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.gstatic.com%2Fandroid%2Fmarket_images%2Fweb%2Ffavicon_v3.ico" width="32" height="32"&gt;
          play.google.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;
&lt;br&gt;
AI is genuinely good at writing the &lt;code&gt;fetch()&lt;/code&gt; one-liner. Ask Copilot, Cursor, Claude, or ChatGPT for "fetch some JSON from this endpoint" and you'll get the canonical happy-path snippet back instantly:&lt;br&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/user&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;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;data&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;err&lt;/span&gt; &lt;span class="o"&gt;=&amp;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="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It compiles. It runs. It demos beautifully. And it is wrong in almost every way that matters once real traffic hits it.&lt;/p&gt;

&lt;p&gt;This is the second piece in a series about where AI coding assistants break down on specific, widely-used pieces of the web platform. The first looked at React Query. The thesis carries over verbatim: &lt;strong&gt;large language models don't produce correct code, they produce plausible code.&lt;/strong&gt; They're trained to predict the most likely next token given everything they've seen in public repos, and public repos are overflowing with the same happy-path &lt;code&gt;fetch().then(res =&amp;gt; res.json())&lt;/code&gt; snippet copy-pasted ten million times. So that's the median of what you get back. It looks right. It passes the vibe check. It even passes a quick manual test against a working endpoint. Then it hits a 500, a flaky network, a slow server, a streaming response, or a cross-origin cookie, and the gap between "looks right" and "is right" is exactly where your production incident lives.&lt;/p&gt;

&lt;p&gt;The Fetch API is a particularly brutal proving ground for this, because &lt;code&gt;fetch()&lt;/code&gt; has a bunch of deliberately surprising, low-level semantics that contradict what developers (and the training data) intuitively expect. It's a primitive, not a batteries-included HTTP client. The things it leaves to you — error semantics, cancellation, streaming, resilience, body lifecycle, CORS, runtime differences — are exactly the things that depend on &lt;strong&gt;your&lt;/strong&gt; API contract, &lt;strong&gt;your&lt;/strong&gt; auth setup, &lt;strong&gt;your&lt;/strong&gt; idempotency guarantees, and &lt;strong&gt;your&lt;/strong&gt; failure modes. None of that context lives in any codebase the model trained on. It cannot reason about it because it has never seen it.&lt;/p&gt;

&lt;p&gt;Here are the five things AI consistently gets wrong with &lt;code&gt;fetch()&lt;/code&gt;, why it gets them wrong, and what you actually have to own yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. fetch Doesn't Reject on HTTP Errors — and AI Forgets This Every Time
&lt;/h2&gt;

&lt;p&gt;This is the single most common Fetch bug AI produces, and it produces it constantly.&lt;/p&gt;

&lt;p&gt;Here's the mental model nearly every developer (and every model trained on their code) brings to &lt;code&gt;fetch()&lt;/code&gt;: "if the request fails, the promise rejects and I'll catch it." That model is wrong, and it comes largely from years of jQuery's &lt;code&gt;$.ajax()&lt;/code&gt; and from libraries like Axios, which &lt;strong&gt;do&lt;/strong&gt; reject on 4xx/5xx. &lt;code&gt;fetch()&lt;/code&gt; does not.&lt;/p&gt;

&lt;p&gt;Straight from MDN: a &lt;code&gt;fetch()&lt;/code&gt; promise "only rejects when the request fails, for example, because of a badly-formed request URL or a network error. A &lt;code&gt;fetch()&lt;/code&gt; promise &lt;em&gt;does not&lt;/em&gt; reject if the server responds with HTTP status codes that indicate errors (404, 504, etc.). Instead, a &lt;code&gt;then()&lt;/code&gt; handler must check the &lt;code&gt;Response.ok&lt;/code&gt; and/or &lt;code&gt;Response.status&lt;/code&gt; properties."&lt;/p&gt;

&lt;p&gt;So this AI-generated code:&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="c1"&gt;// ❌ What AI writes — looks like real error handling, isn't&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;getUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&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;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/users/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// 404? 500? Doesn't matter, we're here anyway&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;user&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;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// This ONLY runs on network failure, DNS failure, CORS block, or abort.&lt;/span&gt;
    &lt;span class="c1"&gt;// A 404 or 500 sails right past it.&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="s1"&gt;Request failed:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&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;err&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;…silently treats a &lt;code&gt;500 Internal Server Error&lt;/code&gt; as success. The &lt;code&gt;try/catch&lt;/code&gt; gives a false sense of safety — it looks like robust error handling, and a reviewer skimming the PR sees a try/catch and moves on. But for a 404 or 500, &lt;code&gt;fetch()&lt;/code&gt; resolves normally with &lt;code&gt;response.ok === false&lt;/code&gt;, and execution proceeds to &lt;code&gt;res.json()&lt;/code&gt;. At that point one of two things happens: either the error body happens to be valid JSON and you return an error payload as if it were a user object, or (very commonly) the server returned an HTML error page and &lt;code&gt;res.json()&lt;/code&gt; throws a confusing &lt;code&gt;SyntaxError: Unexpected token '&amp;lt;'&lt;/code&gt; — which lands in your catch block and gets misreported as a parse error rather than the 500 it actually was.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;response.ok&lt;/code&gt; is &lt;code&gt;true&lt;/code&gt; only for statuses in the 200–299 range. You have to check it yourself:&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="c1"&gt;// ✅ Correct — explicitly check response.ok and separate error classes&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;getUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;res&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="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/users/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="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;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Genuine transport-level failure: offline, DNS, CORS, aborted.&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;NetworkError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Network request failed: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;cause&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;err&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// The server answered — it just answered with an error.&lt;/span&gt;
    &lt;span class="c1"&gt;// Read the body for the API's error detail (often JSON on a 400/422).&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;detail&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="nx"&gt;detail&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="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;detail&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;null&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;HttpError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why can't AI just do this? It often &lt;em&gt;can&lt;/em&gt; if you explicitly prompt "remember fetch doesn't throw on HTTP errors." But left to its own devices it reaches for the statistically dominant pattern, which is the broken one. More importantly, the &lt;em&gt;interesting&lt;/em&gt; decisions here are ones AI can't make for you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Is a 404 an error or an expected outcome?&lt;/strong&gt; For &lt;code&gt;GET /api/users/123&lt;/code&gt;, a 404 might be a real error. For &lt;code&gt;GET /api/users/by-email?x=...&lt;/code&gt; used as an existence check, a 404 is a normal "no" and should resolve to &lt;code&gt;null&lt;/code&gt;, not throw. AI has no idea which of these your endpoint is.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Does your API put error details in the body on a 400/422?&lt;/strong&gt; Most well-designed APIs return field-level validation errors in the response body of a 4xx. Whether you should read that body, and what shape it takes, is part of &lt;em&gt;your&lt;/em&gt; API contract.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Should network errors and HTTP errors be handled the same way?&lt;/strong&gt; Almost never. A network error is often retryable; a 400 never is. Conflating them — which the naive try/catch does — destroys your ability to build sane retry logic later.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That distinction between "transport failed" and "server said no" is a semantic, architectural decision about your system. The model can scaffold the syntax; it cannot make the call.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Cancellation, AbortController, and the Race Conditions AI Can't See
&lt;/h2&gt;

&lt;p&gt;The second thing AI reliably botches is request cancellation, because cancellation is invisible in the happy path. The code works fine in a demo. It leaks, races, and throws spurious errors in production.&lt;/p&gt;

&lt;p&gt;Native &lt;code&gt;fetch()&lt;/code&gt; has no cancellation built into the call itself. You cancel by passing an &lt;code&gt;AbortSignal&lt;/code&gt;, and you trigger the abort through an &lt;code&gt;AbortController&lt;/code&gt;. AI knows this pattern exists — it's in the training data — but it routinely (a) forgets to wire it up at all, (b) fails to clean it up in React, and (c) mishandles the &lt;code&gt;AbortError&lt;/code&gt; that results.&lt;/p&gt;

&lt;h3&gt;
  
  
  The React useEffect leak
&lt;/h3&gt;

&lt;p&gt;Ask an assistant to "fetch data in a React component" and you'll usually get this:&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="c1"&gt;// ❌ No cancellation, no cleanup — race conditions + state-update-after-unmount&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;UserProfile&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;userId&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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setUser&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&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="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/users/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;setUser&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;userId&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&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;user&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two bugs hide here. First, if the component unmounts before the request resolves, you call &lt;code&gt;setUser&lt;/code&gt; on an unmounted component — a leak and a React warning. Second, and worse, is the &lt;strong&gt;race condition&lt;/strong&gt;: if &lt;code&gt;userId&lt;/code&gt; changes quickly (say a user clicks through a list), you fire request A then request B. There is no guarantee they resolve in order. As the React docs themselves note, "network responses may arrive in a different order than you sent them." If A resolves &lt;em&gt;after&lt;/em&gt; B, you'll display data for the old &lt;code&gt;userId&lt;/code&gt; and it will sit there, stale and wrong, with no error anywhere.&lt;/p&gt;

&lt;p&gt;The fix is to abort on cleanup:&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="c1"&gt;// ✅ AbortController in cleanup cancels the stale request&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;UserProfile&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;userId&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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setUser&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&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="nx"&gt;setError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&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="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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;()&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;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/users/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&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;HttpError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nf"&gt;setUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
      &lt;span class="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;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// The abort is EXPECTED. It is not an error to surface to the user.&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;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AbortError&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nf"&gt;setError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&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="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;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="c1"&gt;// cancel on unmount or when userId changes&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;userId&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;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Something&lt;/span&gt; &lt;span class="nx"&gt;went&lt;/span&gt; &lt;span class="nx"&gt;wrong&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;;
&lt;/span&gt;  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&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;user&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&amp;gt;&lt;/span&gt;&lt;span class="err"&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 subtle part that AI almost always misses is the &lt;code&gt;if (err.name === 'AbortError') return;&lt;/code&gt; line. When you call &lt;code&gt;controller.abort()&lt;/code&gt;, the in-flight &lt;code&gt;fetch()&lt;/code&gt; rejects with a &lt;code&gt;DOMException&lt;/code&gt; named &lt;code&gt;AbortError&lt;/code&gt;. If your catch block doesn't special-case it, you'll catch your own intentional cancellation and render an error state — the dreaded flash of "Something went wrong" every time the user navigates. The naive code doesn't even have the abort, so it never learns this lesson; the slightly-less-naive code adds the abort but forgets to filter the error. Both are common AI outputs.&lt;/p&gt;

&lt;p&gt;Note also that an abort can fire &lt;em&gt;after&lt;/em&gt; the response headers arrive but before the body is read. Per MDN, if you abort after &lt;code&gt;fetch()&lt;/code&gt; has fulfilled but before you've read the body, "attempting to read the response body will reject with an &lt;code&gt;AbortError&lt;/code&gt; exception." So the guard has to wrap the &lt;code&gt;.json()&lt;/code&gt; call too, not just the &lt;code&gt;fetch()&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Timeouts and combining signals
&lt;/h3&gt;

&lt;p&gt;Native &lt;code&gt;fetch()&lt;/code&gt; has no timeout. None. A request can hang indefinitely. AI, asked for a timeout, will usually produce the old &lt;code&gt;setTimeout(() =&amp;gt; controller.abort(), 5000)&lt;/code&gt; dance — which works, but is verbose and leaks the timer if the request succeeds first. The modern primitives are cleaner, and AI underuses them because they're newer than the bulk of its training data:&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="c1"&gt;// ✅ AbortSignal.timeout() — declarative per-request timeout&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&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;AbortSignal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ AbortSignal.any() — combine a user-cancel signal with a timeout&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userCancel&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;res2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&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;AbortSignal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;userCancel&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;AbortSignal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5000&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;AbortSignal.timeout(ms)&lt;/code&gt; returns a signal that aborts on its own after the given time, and — importantly — per MDN it "rejects with a &lt;code&gt;TimeoutError&lt;/code&gt; &lt;code&gt;DOMException&lt;/code&gt;," distinct from the &lt;code&gt;AbortError&lt;/code&gt; you get from a manual &lt;code&gt;controller.abort()&lt;/code&gt;. That distinction lets you tell "the server was too slow" apart from "the user navigated away," which matters for whether you show a message and whether you retry. &lt;code&gt;AbortSignal.any([...])&lt;/code&gt; composes multiple signals into one that fires on the first abort — though MDN notes that, unlike &lt;code&gt;timeout()&lt;/code&gt;, with &lt;code&gt;any()&lt;/code&gt; "there is no way to tell whether the final abort was caused by a timeout." Both are Baseline across modern browsers since 2024 (for &lt;code&gt;any()&lt;/code&gt;: Chrome 116+, Firefox 124+, Safari 17+); both are available in Node 18+/20+. If you support older targets, feature-detect or polyfill.&lt;/p&gt;

&lt;p&gt;One honest caveat the model will never volunteer: there's a long-standing Chromium quirk where a fetch aborted by &lt;code&gt;AbortSignal.timeout()&lt;/code&gt; sometimes surfaces as &lt;code&gt;AbortError&lt;/code&gt; rather than &lt;code&gt;TimeoutError&lt;/code&gt;. If you branch on the error name, test it in the browsers you actually ship to.&lt;/p&gt;

&lt;p&gt;And the deepest point: &lt;strong&gt;client-side abort only stops the client.&lt;/strong&gt; It frees the connection and stops you waiting. If the server already received the request, it may keep right on processing — charging the card, sending the email. For read-only requests that's fine. For mutations it's not, and the answer is server-side idempotency, which is squarely your design problem, not something &lt;code&gt;fetch()&lt;/code&gt; or an AI can paper over.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Streaming the Response Body — Where AI Falls Apart Completely
&lt;/h2&gt;

&lt;p&gt;The third area is streaming, and it's where AI's limitations are starkest, because correct streaming code requires reasoning about byte boundaries, buffering, and protocol framing — things that are genuinely hard to pattern-match.&lt;/p&gt;

&lt;p&gt;By default, AI treats every response as a monolith: &lt;code&gt;await res.json()&lt;/code&gt; or &lt;code&gt;await res.text()&lt;/code&gt;, get the whole thing, done. That's correct for a 2KB JSON payload. It's a disaster for a streaming LLM response, a large NDJSON export, or a Server-Sent Events feed, where the entire point is to process bytes as they arrive. &lt;code&gt;response.body&lt;/code&gt; is a &lt;code&gt;ReadableStream&lt;/code&gt; of &lt;code&gt;Uint8Array&lt;/code&gt; chunks, and consuming it correctly is fiddly.&lt;/p&gt;

&lt;p&gt;Here's the kind of streaming loop AI tends to write when you push it toward streaming:&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="c1"&gt;// ❌ Subtly broken in (at least) two ways&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;streamTokens&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onToken&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;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&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;reader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getReader&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;while &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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;done&lt;/span&gt; &lt;span class="p"&gt;}&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;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&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;done&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&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;text&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;TextDecoder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// BUG 1&lt;/span&gt;
    &lt;span class="k"&gt;for &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;line&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&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="c1"&gt;// BUG 2&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;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data: &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;onToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&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;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are two classic, production-breaking bugs here, and AI produces both because the broken version appears all over the internet:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bug 1 — multi-byte characters split across chunks.&lt;/strong&gt; Network chunks are arbitrary byte boundaries. A multi-byte UTF-8 character (an emoji, a CJK glyph, an accented letter) can be split across two &lt;code&gt;read()&lt;/code&gt; calls. Calling &lt;code&gt;new TextDecoder().decode(value)&lt;/code&gt; on each chunk independently corrupts any character straddling the boundary, replacing it with the &lt;code&gt;�&lt;/code&gt; replacement character. The fix is to reuse a single decoder with streaming mode: &lt;code&gt;decoder.decode(value, { stream: true })&lt;/code&gt;, which buffers the incomplete trailing bytes until the rest arrives. Or pipe through a &lt;code&gt;TextDecoderStream&lt;/code&gt;, which handles this for you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bug 2 — delimiters split across chunks.&lt;/strong&gt; A chunk can end in the middle of a line. &lt;code&gt;text.split('\n')&lt;/code&gt; on a single chunk will mangle any record that spans a boundary — you'll get half a JSON object and a &lt;code&gt;JSON.parse&lt;/code&gt; throw. You must keep a buffer across reads and only process complete lines, retaining the trailing partial fragment for the next iteration.&lt;/p&gt;

&lt;p&gt;Here's a version that actually survives the network:&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="c1"&gt;// ✅ Correct: single streaming decoder + cross-chunk line buffer&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;streamSSE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onMessage&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&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;HttpError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// pipeThrough(TextDecoderStream()) handles multi-byte boundaries for us.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pipeThrough&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextDecoderStream&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getReader&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&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="k"&gt;while &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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;done&lt;/span&gt; &lt;span class="p"&gt;}&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;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&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;done&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;value&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;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// keep the last, possibly-incomplete line&lt;/span&gt;

      &lt;span class="k"&gt;for &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;line&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;lines&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data: &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;continue&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;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&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;data&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[DONE]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&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="nf"&gt;onMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&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="cm"&gt;/* ignore keep-alive / partial frames */&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="c1"&gt;// flush any trailing complete record left in the buffer&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;buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data: &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;onMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&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="nx"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;releaseLock&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;A few things worth noting that AI rarely gets right on its own:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;for await (const chunk of response.body)&lt;/code&gt; async iteration is not universally supported.&lt;/strong&gt; It reads cleaner, but Safari didn't support it for a while, so the explicit &lt;code&gt;getReader()&lt;/code&gt; loop remains the production-safe choice. AI will happily suggest the async-iterator form without flagging the support gap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;For real SSE in the browser, &lt;code&gt;EventSource&lt;/code&gt; already handles all of this&lt;/strong&gt; — parsing, reconnection, the &lt;code&gt;\n\n&lt;/code&gt; framing — but it's GET-only and can't send custom headers (no &lt;code&gt;Authorization&lt;/code&gt;). The reason people hand-roll SSE over &lt;code&gt;fetch&lt;/code&gt; is precisely to POST a body and set auth headers. That trade-off is a judgment call about your API; AI doesn't know you need a bearer token, so it can't tell you which approach fits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backpressure.&lt;/strong&gt; A reader naturally applies backpressure — you don't pull the next chunk until you've processed the current one — but if you spin the loop and buffer everything into memory regardless, you've thrown that away. Whether that matters depends on payload size and your environment, which, again, is context the model lacks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Correct streaming is a reasoning problem about byte framing and protocol, not a recall problem. That's why AI does so badly at it.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Retries, Timeouts, and Resilience — Logic AI Can't Architect for Your API
&lt;/h2&gt;

&lt;p&gt;Native &lt;code&gt;fetch()&lt;/code&gt; has no retry logic, no backoff, no timeout. Resilience is entirely yours to build, and this is the category where AI's output is the most dangerous, because a naive retry loop doesn't just fail to help — it can actively make an outage worse.&lt;/p&gt;

&lt;p&gt;Here's the retry loop AI loves to write:&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="c1"&gt;// ❌ Dangerous: retries everything, no backoff, retries non-idempotent writes&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;fetchWithRetry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;retries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&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="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;retries&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&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;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;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&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;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// swallow and try again&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="s1"&gt;Failed after retries&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Count the ways this hurts you in production:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;It retries non-idempotent requests.&lt;/strong&gt; If &lt;code&gt;options.method&lt;/code&gt; is &lt;code&gt;POST&lt;/code&gt; and the request actually reached the server and succeeded — but the &lt;em&gt;response&lt;/em&gt; got lost — this retries it and you double-charge the customer or create two orders. AWS's own resilience guidance (Marc Brooker, &lt;em&gt;Timeouts, retries and backoff with jitter&lt;/em&gt;, Amazon Builders' Library) is blunt: "In general, our view is that APIs with side effects aren't safe to retry unless they provide idempotency. This guarantees that the side effects happen only once no matter how often you retry."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It retries 4xx.&lt;/strong&gt; A &lt;code&gt;400&lt;/code&gt;, &lt;code&gt;401&lt;/code&gt;, &lt;code&gt;403&lt;/code&gt;, or &lt;code&gt;422&lt;/code&gt; will &lt;em&gt;never&lt;/em&gt; succeed on retry — the request is malformed or unauthorized. The AWS Well-Architected Framework (REL05-BP03) explicitly warns against "retrying all errors, including those with a clear cause that indicates lack of permission, configuration error, or another condition that predictably will not resolve without manual intervention." You should only retry genuinely transient failures: network errors, &lt;code&gt;429&lt;/code&gt;, and &lt;code&gt;503&lt;/code&gt; (often &lt;code&gt;502&lt;/code&gt;/&lt;code&gt;504&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It has no backoff.&lt;/strong&gt; Immediate retries are a stampede. When a service is already struggling, a fleet of clients retrying in a tight loop is precisely how a brief blip becomes a cascading failure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It ignores &lt;code&gt;Retry-After&lt;/code&gt;.&lt;/strong&gt; A &lt;code&gt;429&lt;/code&gt; or &lt;code&gt;503&lt;/code&gt; frequently comes with a &lt;code&gt;Retry-After&lt;/code&gt; header telling you exactly how long to wait. The naive loop steamrolls it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No jitter.&lt;/strong&gt; Even with exponential backoff, if every client backs off on the same schedule they retry in synchronized waves — the "thundering herd." Randomized jitter spreads them out.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A defensible version encodes real decisions:&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="c1"&gt;// ✅ Retries only what's safe, with backoff + jitter + Retry-After&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;RETRYABLE_STATUS&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;Set&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;502&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;503&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;504&lt;/span&gt;&lt;span class="p"&gt;]);&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;resilientFetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;maxRetries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;baseDelay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Only retry idempotent methods unless the caller explicitly opts in.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;idempotent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;HEAD&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PUT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DELETE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="o"&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;let&lt;/span&gt; &lt;span class="nx"&gt;res&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="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;method&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;AbortSignal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;options&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;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// network error or timeout — retryable if idempotent and budget remains&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;idempotent&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;maxRetries&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;err&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;backoff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;baseDelay&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
      &lt;span class="k"&gt;continue&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;RETRYABLE_STATUS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&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;idempotent&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;maxRetries&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;HttpError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Respect Retry-After (seconds or HTTP-date) when present.&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;retryAfter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseRetryAfter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Retry-After&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;retryAfter&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nf"&gt;backoff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;baseDelay&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;backoff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;base&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;exp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;base&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;          &lt;span class="c1"&gt;// 300, 600, 1200, ...&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;exp&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;base&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;        &lt;span class="c1"&gt;// full-ish jitter&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;sleep&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But here's the real point: even this is just a &lt;em&gt;template&lt;/em&gt;. The genuinely important questions are ones only you can answer, because they depend on your system:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Which of your endpoints are actually idempotent?&lt;/strong&gt; A &lt;code&gt;POST /search&lt;/code&gt; is safe to retry. A &lt;code&gt;POST /charge&lt;/code&gt; is not — unless you send an idempotency key, which is a server-side contract you have to design and the server has to honor. AI cannot inspect your backend to know which is which.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What's your retry budget across layers?&lt;/strong&gt; If your client retries 3×, your API gateway retries 3×, and your service-to-service calls retry 3×, you've just built a 27× load amplifier for a single user action. Coordinating retry budgets across the stack is an architecture problem.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Should you even retry, or fail fast?&lt;/strong&gt; Sometimes the right answer for an interactive request is to surface the error immediately and let the user decide, not to make them wait through three silent backoffs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are decisions about &lt;em&gt;your&lt;/em&gt; failure modes and &lt;em&gt;your&lt;/em&gt; tolerance for duplication and latency. The model has no model of your system. It pattern-matches a retry loop from GitHub and hands it to you with the unsafe defaults baked in.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. The Subtle Semantics: Body Lifecycle, CORS, Content-Type, Redirects, and Node vs Browser
&lt;/h2&gt;

&lt;p&gt;The last category is a grab-bag of low-level &lt;code&gt;fetch()&lt;/code&gt; semantics that are individually small but collectively responsible for a huge share of "why is this broken" debugging sessions. AI gets these wrong because they're the kind of detail that's underrepresented in training data relative to the happy path.&lt;/p&gt;

&lt;h3&gt;
  
  
  The body can only be read once
&lt;/h3&gt;

&lt;p&gt;A response body is a stream. Once you consume it — &lt;code&gt;.json()&lt;/code&gt;, &lt;code&gt;.text()&lt;/code&gt;, &lt;code&gt;.arrayBuffer()&lt;/code&gt;, &lt;code&gt;.formData()&lt;/code&gt;, &lt;code&gt;.blob()&lt;/code&gt; — it's gone. Call a second consuming method and you get &lt;code&gt;TypeError: Body has already been consumed&lt;/code&gt; (or &lt;code&gt;body stream already read&lt;/code&gt;).&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="c1"&gt;// ❌ Throws on the second read&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&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;data&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;raw&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// 💥 "Body has already been consumed"&lt;/span&gt;

&lt;span class="c1"&gt;// A subtler version of the same bug — logging the body, then returning it:&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;loggingFetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&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;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;   &lt;span class="c1"&gt;// consumes the body...&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;               &lt;span class="c1"&gt;// 💥 ...so this throws&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This bites hard in middleware, interceptors, and caching layers — anywhere one piece of code wants to peek at the body and another wants to use it. The fix is &lt;code&gt;response.clone()&lt;/code&gt; &lt;strong&gt;before&lt;/strong&gt; the first read:&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="c1"&gt;// ✅ Clone before reading if you need the body twice&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;loggingFetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&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;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&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;audit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clone&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;        &lt;span class="c1"&gt;// clone first&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;audit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;  &lt;span class="c1"&gt;// read the clone&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;                &lt;span class="c1"&gt;// original still intact&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Per MDN, &lt;code&gt;clone()&lt;/code&gt; "throws a &lt;code&gt;TypeError&lt;/code&gt; if the response body has already been used" — so the clone must happen first. And a performance caveat the model won't mention: if you read the two branches at very different speeds, the faster one forces the slower one's data to buffer in memory, so &lt;code&gt;clone()&lt;/code&gt; is fine for read-twice-in-sequence but not for tee-ing a huge body to two slow consumers. The same one-shot rule applies to &lt;strong&gt;request&lt;/strong&gt; bodies, which is why you can't &lt;code&gt;fetch(request)&lt;/code&gt; the same &lt;code&gt;Request&lt;/code&gt; object twice without cloning it.&lt;/p&gt;

&lt;h3&gt;
  
  
  CORS and credentials — AI cannot reason about your origin setup
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;fetch()&lt;/code&gt; defaults to &lt;code&gt;mode: 'cors'&lt;/code&gt; and, critically, &lt;code&gt;credentials: 'same-origin'&lt;/code&gt;. That means &lt;strong&gt;by default, cookies are not sent on cross-origin requests.&lt;/strong&gt; Developers (and AI) are constantly surprised that their authenticated cross-origin call returns a 401, because the session cookie silently wasn't attached.&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="c1"&gt;// ❌ Cross-origin call that needs the session cookie — but doesn't send it&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.example.com/me&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// credentials default to same-origin&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ Explicitly include credentials for cross-origin auth&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.example.com/me&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="na"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;include&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;But &lt;code&gt;credentials: 'include'&lt;/code&gt; is necessary, not sufficient. For a credentialed cross-origin request to actually work, the server must respond with &lt;code&gt;Access-Control-Allow-Credentials: true&lt;/code&gt; &lt;em&gt;and&lt;/em&gt; an explicit &lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt; echoing your exact origin — the &lt;code&gt;*&lt;/code&gt; wildcard is forbidden with credentials. And a &lt;code&gt;SameSite=Strict&lt;/code&gt;/&lt;code&gt;Lax&lt;/code&gt; cookie won't be sent cross-site regardless of what you put in &lt;code&gt;fetch&lt;/code&gt;. This is the perfect example of something AI structurally cannot do: it doesn't know whether your API is same-origin or cross-origin, whether it's cookie-based or bearer-token-based, how your &lt;code&gt;SameSite&lt;/code&gt; attributes are set, or what your server's CORS config looks like. None of that is in the code it's editing. It guesses, and it's frequently wrong, and the failure shows up as an opaque CORS console error that gives JavaScript no detail by design.&lt;/p&gt;

&lt;h3&gt;
  
  
  Content-Type: when fetch sets it, and when you must
&lt;/h3&gt;

&lt;p&gt;For a JSON body you must set the header yourself; the model usually gets this one right:&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="c1"&gt;// ✅ JSON — you set Content-Type&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/items&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="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-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="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Widget&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But the inverse is the trap. When you pass a &lt;code&gt;FormData&lt;/code&gt; body, the browser sets &lt;code&gt;Content-Type: multipart/form-data&lt;/code&gt; &lt;strong&gt;for you&lt;/strong&gt;, including the all-important &lt;code&gt;boundary&lt;/code&gt; parameter. If you "helpfully" set it yourself — which AI does constantly, applying the JSON pattern by analogy — you omit the boundary and the server can't parse the body:&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="c1"&gt;// ❌ Setting Content-Type manually with FormData breaks the boundary&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/upload&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="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-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="s1"&gt;multipart/form-data&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;   &lt;span class="c1"&gt;// 🔴 no boundary → server fails&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ Let the browser set it — including the boundary&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/upload&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="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;formData&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same goes for &lt;code&gt;URLSearchParams&lt;/code&gt; bodies, which auto-set &lt;code&gt;application/x-www-form-urlencoded; charset=UTF-8&lt;/code&gt;. The rule — "set it for JSON, never set it for FormData/URLSearchParams" — is exactly the kind of conditional, counterintuitive detail AI flattens into "always set Content-Type."&lt;/p&gt;

&lt;h3&gt;
  
  
  Redirects
&lt;/h3&gt;

&lt;p&gt;By default &lt;code&gt;fetch()&lt;/code&gt; follows redirects transparently (&lt;code&gt;redirect: 'follow'&lt;/code&gt;), and &lt;code&gt;response.url&lt;/code&gt; gives you the final URL while &lt;code&gt;response.redirected&lt;/code&gt; tells you whether a redirect happened. If you need to &lt;em&gt;prevent&lt;/em&gt; redirects for security reasons, MDN is explicit that checking &lt;code&gt;response.redirected&lt;/code&gt; after the fact is unsafe — "by the time a response is received, the redirect has already happened, and you may have sent the request to an unintended destination, potentially sending sensitive information." The correct approach is &lt;code&gt;redirect: 'error'&lt;/code&gt; (or &lt;code&gt;'manual'&lt;/code&gt;) set on the request up front. This is a security nuance AI won't surface unless you know to ask.&lt;/p&gt;

&lt;h3&gt;
  
  
  Browser vs Node — same API, different behavior
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;fetch()&lt;/code&gt; has been global in Node since v18 (via undici) and stable since &lt;strong&gt;Node v21&lt;/strong&gt; — as LogRocket puts it, fetch "was added to the Node.js core in v18. However, until v21, it was mostly experimental... The stable release in v21 is a big milestone." It's exposed with no import, and it looks identical to the browser. It is not identical, and the differences are precisely the kind of environment-specific behavior AI conflates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Node applies default timeouts the browser doesn't.&lt;/strong&gt; Per the nodejs/undici docs (Client API), undici sets &lt;code&gt;bodyTimeout&lt;/code&gt; and &lt;code&gt;headersTimeout&lt;/code&gt; to &lt;code&gt;300e3&lt;/code&gt; — "Defaults to 300 seconds" each (older undici 5.x bundled in early Node 18 used &lt;code&gt;30e3&lt;/code&gt;/30s), and you'll see errors with code &lt;code&gt;UND_ERR_HEADERS_TIMEOUT&lt;/code&gt;. There is no equivalent default in the browser — there, an un-aborted request can hang forever.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node does not enforce CORS.&lt;/strong&gt; Verbatim from the undici docs: "Unlike browsers, Undici does not implement CORS (Cross-Origin Resource Sharing) checks by default... No preflight requests are automatically sent for cross-origin requests. No validation of Access-Control-Allow-Origin headers is performed. Requests to any origin are allowed regardless of the source." All the CORS reasoning above simply doesn't apply server-side — which means code that "works" in your Node tests can still fail in the browser.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You must consume or cancel the body in Node, or leak connections.&lt;/strong&gt; The undici docs warn that "Garbage collection in Node is less aggressive and deterministic... which means that leaving the release of connection resources to the garbage collector can lead to excessive connection usage, reduced performance... and even stalls or deadlocks when running out of connections. Therefore, it is important to always either consume or cancel the response body." In the browser, GC bails you out. In Node it doesn't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;There's no browser cookie jar, no &lt;code&gt;SameSite&lt;/code&gt;, no origin&lt;/strong&gt; server-side, and undici exposes non-standard extensions (a &lt;code&gt;dispatcher&lt;/code&gt; option for proxies/pooling, async-iterable request bodies requiring &lt;code&gt;duplex: 'half'&lt;/code&gt;) that don't exist in browsers at all.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AI trained on a mix of browser and Node code will cheerfully mix their assumptions. It can't tell which runtime your file targets, so it can't warn you when a browser assumption will break in Node or vice versa.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Actually Use AI With fetch
&lt;/h2&gt;

&lt;p&gt;None of this is an argument against using AI assistants. They're a genuine accelerator. It's an argument for knowing exactly where the line is.&lt;/p&gt;

&lt;p&gt;Here's the division of labor that works:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Let AI do the scaffolding.&lt;/strong&gt; The boilerplate &lt;code&gt;fetch()&lt;/code&gt; call, the shape of an async function, the wiring of an &lt;code&gt;AbortController&lt;/code&gt;, the skeleton of a retry loop, the JSON-body POST — let it type all of that. It's fast and it's usually structurally fine. This is "acceleration mode": you know what you want, AI gets you there faster.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You own the semantics.&lt;/strong&gt; Specifically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Error handling&lt;/strong&gt; — because whether a 404 is an error, whether the error body matters, and how network failures differ from HTTP failures depends on &lt;em&gt;your&lt;/em&gt; API contract. Always check &lt;code&gt;response.ok&lt;/code&gt;; never let a try/catch lull you into thinking a 500 was handled.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cancellation&lt;/strong&gt; — because cleanup, race conditions, and abort-error filtering depend on &lt;em&gt;your&lt;/em&gt; component lifecycle and UX. Wire an &lt;code&gt;AbortController&lt;/code&gt; into every effect; filter &lt;code&gt;AbortError&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Streaming&lt;/strong&gt; — because correct byte buffering and protocol framing is a reasoning problem AI fails at, and the EventSource-vs-fetch trade-off depends on &lt;em&gt;your&lt;/em&gt; auth needs. Use a single streaming decoder and a cross-chunk buffer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resilience&lt;/strong&gt; — because what to retry, when, and how hard depends entirely on &lt;em&gt;your&lt;/em&gt; idempotency guarantees and failure modes, and the unsafe defaults AI ships can amplify an outage. Retry only idempotent, transient failures; back off with jitter; honor &lt;code&gt;Retry-After&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The semantics&lt;/strong&gt; — body lifecycle (&lt;code&gt;clone()&lt;/code&gt; before reading twice), CORS/credentials, Content-Type rules, redirects, and runtime differences, because these are counterintuitive details AI flattens and your environment determines.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;A practical workflow:&lt;/strong&gt; generate the call, then run a checklist against it — &lt;em&gt;Does it check &lt;code&gt;response.ok&lt;/code&gt;? Is there a signal wired in and an abort path? If it streams, does it buffer across chunks? If it retries, what exactly does it retry and is that method idempotent? Are credentials/Content-Type correct for this origin and body type? Does this run in Node, the browser, or both?&lt;/em&gt; That checklist is the part of the job that doesn't transfer to the model.&lt;/p&gt;

&lt;p&gt;The unifying theme, the same one from the React Query piece: &lt;strong&gt;AI optimizes for code that looks plausible and probably compiles, not for code with correct semantics on the actual web platform.&lt;/strong&gt; The Fetch API is a low-level primitive that deliberately hands you the hard decisions — and those decisions require context about your specific API, auth setup, idempotency, and failure modes that simply does not exist in any codebase the model was trained on. That's not a limitation that a better model fixes. It's a category of work that is, definitionally, yours.&lt;/p&gt;

&lt;p&gt;Use AI to write the fetch. Reason about everything that happens after it resolves yourself.&lt;/p&gt;

</description>
      <category>api</category>
      <category>webdev</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>(Alert!)5 Things Even AI Can't Do, React Query</title>
      <dc:creator>DevUnionX</dc:creator>
      <pubDate>Sat, 20 Jun 2026 09:37:01 +0000</pubDate>
      <link>https://dev.to/devunionx/alert5-things-even-ai-cant-do-react-query-4j53</link>
      <guid>https://dev.to/devunionx/alert5-things-even-ai-cant-do-react-query-4j53</guid>
      <description>&lt;h2&gt;
  
  
  GraphQL: A Complete Guide for Developers in 2026
&lt;/h2&gt;

&lt;h2&gt;
  
  
  MY GAMES JUST LAUNCHED
&lt;/h2&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://play.google.com/store/apps/details?id=com.flip_duel_card_battle&amp;amp;amp%3Bamp%3Bhl=tr" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fplay-lh.googleusercontent.com%2FQiVSCNak_1Y7ZpkDvhy_7UNbA0BGOpwEFEmEPfMdo7UcRvrgf4s-7_JIj-FT2tRUTVdFfs8FPKVW6CGIGg7Ksg" height="512" class="m-0" width="512"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://play.google.com/store/apps/details?id=com.flip_duel_card_battle&amp;amp;amp%3Bamp%3Bhl=tr" rel="noopener noreferrer" class="c-link"&gt;
            Flip Duel: Online Card Battle - Apps on Google Play
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Outsmart rivals in 1v1 card duels. Joker, bluff, ranked PvP. 5 rounds.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.gstatic.com%2Fandroid%2Fmarket_images%2Fweb%2Ffavicon_v3.ico" width="32" height="32"&gt;
          play.google.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;If you have built more than a couple of APIs, you have probably felt the friction of REST at scale. You ship an endpoint, the frontend team asks for one more field, you version the route, the mobile team needs a &lt;em&gt;different&lt;/em&gt; shape of the same data, and six months later you are maintaining &lt;code&gt;/v3/users/:id/full&lt;/code&gt; next to &lt;code&gt;/v2/users/:id/summary&lt;/code&gt; and nobody remembers which one the Android app actually calls.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GraphQL was built to kill that exact pain.&lt;/strong&gt; It is a query language and runtime that lets clients ask for precisely the data they need — no more, no less — from a single endpoint, against a strongly typed schema that doubles as living documentation.&lt;/p&gt;

&lt;p&gt;This guide walks through GraphQL from first principles to production concerns. It is aimed at working developers, so expect schema definitions, resolvers, real queries, the N+1 problem, federation, security, and the parts of the ecosystem that actually matter in 2026. By the end you should be able to decide whether GraphQL belongs in your stack and how to build it without shooting yourself in the foot.&lt;/p&gt;

&lt;h2&gt;
  
  
  What GraphQL Actually Is
&lt;/h2&gt;

&lt;p&gt;GraphQL is a specification, not a library or a framework. It was created at Facebook in 2012 to power their mobile apps, open-sourced in 2015, and is now governed by the &lt;strong&gt;GraphQL Foundation&lt;/strong&gt; under the Linux Foundation. The spec defines a query language, a type system, and an execution model — but it deliberately says nothing about which database you use, which programming language you implement it in, or how you transport requests over the wire.&lt;/p&gt;

&lt;p&gt;That last point trips people up, so let it sink in: &lt;strong&gt;GraphQL is transport-agnostic and storage-agnostic.&lt;/strong&gt; Most implementations run over HTTP with JSON, but that is a convention, not a requirement. Your resolvers can pull data from PostgreSQL, a REST microservice, a gRPC backend, Redis, a flat file, or three of those at once. GraphQL sits as a thin coordination layer in front of whatever you already have.&lt;/p&gt;

&lt;p&gt;The mental model is simple. You describe your data as a &lt;strong&gt;graph of types&lt;/strong&gt;. Clients write &lt;strong&gt;queries&lt;/strong&gt; that traverse that graph. The server &lt;strong&gt;resolves&lt;/strong&gt; each requested field by running a function. The response mirrors the shape of the query exactly.&lt;/p&gt;

&lt;p&gt;Here is the canonical hello-world. A query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;query&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="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"4"&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="n"&gt;name&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&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="n"&gt;title&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the response:&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;"data"&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;"user"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Mary Watson"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mary@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"posts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"On the Nature of APIs"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Why I Stopped Versioning"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Cursors, Not Offsets"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice three things. The response shape matches the query shape one-to-one. You got exactly the fields you asked for and nothing else. And you fetched a user &lt;em&gt;and&lt;/em&gt; their posts in a single round trip, even though those are almost certainly two different tables or services on the backend.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problems GraphQL Solves
&lt;/h2&gt;

&lt;p&gt;To appreciate GraphQL you have to be honest about where REST hurts. REST is excellent, widely understood, cache-friendly, and probably the right default for most public APIs. But it has three structural weaknesses that GraphQL attacks directly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Over-fetching.&lt;/strong&gt; A REST endpoint returns a fixed payload. &lt;code&gt;GET /users/4&lt;/code&gt; might return forty fields when your screen needs two. On a desktop with fiber this is invisible. On a phone with spotty 4G, those extra kilobytes per request, multiplied across a list view, add up to real latency and battery drain. GraphQL clients request only the fields they render.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Under-fetching and the N+1 round trip.&lt;/strong&gt; The mirror image. To render a user profile with their recent posts and the comment count on each post, REST often forces you into a waterfall: fetch the user, then fetch their posts, then loop over posts fetching comments. Each step waits for the previous one. The frontend becomes a choreography of chained &lt;code&gt;fetch&lt;/code&gt; calls. GraphQL collapses that into one request because the client describes the entire tree it wants up front.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Endpoint proliferation and versioning.&lt;/strong&gt; REST tends to grow an endpoint per view. A new screen needs a slightly different shape, so you add a route, or a &lt;code&gt;?include=&lt;/code&gt; parameter, or a &lt;code&gt;?fields=&lt;/code&gt; filter, and slowly reinvent a query language badly. GraphQL gives clients that flexibility natively. Because clients select fields explicitly, you can add new fields to a type without ever breaking existing clients — they simply do not ask for the new field. &lt;strong&gt;Most GraphQL APIs never version at all&lt;/strong&gt;; they evolve additively and deprecate fields with metadata rather than spinning up &lt;code&gt;/v2&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The flip side, which we will cover, is that GraphQL trades these wins for harder caching, a more involved server setup, and new categories of performance and security concern. There is no free lunch.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Type System and Schema Definition Language
&lt;/h2&gt;

&lt;p&gt;Everything in GraphQL starts with the &lt;strong&gt;schema&lt;/strong&gt;. The schema is a contract written in the &lt;strong&gt;Schema Definition Language (SDL)&lt;/strong&gt; that declares every type, every field, and every operation your API supports. It is strongly typed and introspectable, which is what powers autocomplete in tooling, codegen, and validation before a single resolver runs.&lt;/p&gt;

&lt;p&gt;Let's build out a small blog API to make the type system concrete.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;User&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="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Role&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;posts&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="n"&gt;Post&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="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Post&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="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;published&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;comments&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="n"&gt;Comment&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="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Comment&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="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;User&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="k"&gt;enum&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Role&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="n"&gt;ADMIN&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;EDITOR&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;READER&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;A few things to unpack here, because the syntax is dense with meaning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scalars&lt;/strong&gt; are the leaf values. GraphQL ships with five built-in scalars: &lt;code&gt;Int&lt;/code&gt;, &lt;code&gt;Float&lt;/code&gt;, &lt;code&gt;String&lt;/code&gt;, &lt;code&gt;Boolean&lt;/code&gt;, and &lt;code&gt;ID&lt;/code&gt;. &lt;code&gt;ID&lt;/code&gt; is a string under the hood but signals "this is a unique identifier" to tooling. You can and should define &lt;strong&gt;custom scalars&lt;/strong&gt; like &lt;code&gt;DateTime&lt;/code&gt;, &lt;code&gt;Email&lt;/code&gt;, or &lt;code&gt;URL&lt;/code&gt; to add validation and semantic meaning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The exclamation mark means non-null.&lt;/strong&gt; &lt;code&gt;String!&lt;/code&gt; is a string that will never be null. &lt;code&gt;[Post!]!&lt;/code&gt; is a non-null list of non-null posts — the list itself is always present (possibly empty), and no element inside it is ever null. &lt;code&gt;[Post]&lt;/code&gt; would be a nullable list that may contain nulls. This nullability system is one of GraphQL's quietest strengths: it pushes a huge class of "cannot read property of undefined" bugs into the type checker.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Object types&lt;/strong&gt; like &lt;code&gt;User&lt;/code&gt; and &lt;code&gt;Post&lt;/code&gt; are the nodes of your graph, and the fields that point to other object types are the edges. &lt;code&gt;User.posts&lt;/code&gt; connects a user to its posts; &lt;code&gt;Post.author&lt;/code&gt; connects back. That bidirectional linking is exactly why it is called a &lt;em&gt;graph&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Beyond objects and scalars, the type system has a few more tools you will reach for constantly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enums&lt;/strong&gt; restrict a field to a fixed set of values, as &lt;code&gt;Role&lt;/code&gt; shows above. &lt;strong&gt;Input types&lt;/strong&gt; describe the structured arguments you pass into mutations — they look like object types but use the &lt;code&gt;input&lt;/code&gt; keyword and cannot have fields that resolve to object types. &lt;strong&gt;Interfaces&lt;/strong&gt; define a set of fields that multiple types must implement. &lt;strong&gt;Unions&lt;/strong&gt; say a field can return one of several types that need not share any fields.&lt;/p&gt;

&lt;p&gt;Here is an interface and a union in practice:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;interface&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Node&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="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ID&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="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;implements&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Node&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="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;altText&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&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="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Video&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;implements&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Node&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="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;durationSeconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Int&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="k"&gt;union&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;SearchResult&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="n"&gt;User&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="n"&gt;Post&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="n"&gt;Image&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="k"&gt;input&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CreatePostInput&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="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;published&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Boolean&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;false&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;The &lt;code&gt;Node&lt;/code&gt; interface is the foundation of the &lt;strong&gt;Relay&lt;/strong&gt; specification for global object identification — any type implementing &lt;code&gt;Node&lt;/code&gt; can be refetched by its global &lt;code&gt;id&lt;/code&gt;. Unions are perfect for search results or activity feeds where heterogeneous types share a list. Note the default value &lt;code&gt;published: Boolean = false&lt;/code&gt; in the input — defaults are first-class in SDL.&lt;/p&gt;

&lt;p&gt;Finally, the three special &lt;strong&gt;root operation types&lt;/strong&gt; are the entry points into the entire graph:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Query&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="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;!):&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;published&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Boolean&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="n"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;!]!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;term&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&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="n"&gt;SearchResult&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="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Mutation&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="n"&gt;createPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CreatePostInput&lt;/span&gt;&lt;span class="p"&gt;!):&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;deletePost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;!):&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Boolean&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="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Subscription&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="n"&gt;postPublished&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Post&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;&lt;code&gt;Query&lt;/code&gt; is for reads, &lt;code&gt;Mutation&lt;/code&gt; is for writes, and &lt;code&gt;Subscription&lt;/code&gt; is for real-time streams. Every operation a client can perform must be reachable from one of these three roots. This is the complete public surface of your API in one readable document — which is precisely why a good schema is the single most important artifact in a GraphQL project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Queries: Asking for Data
&lt;/h2&gt;

&lt;p&gt;A query selects fields starting from the &lt;code&gt;Query&lt;/code&gt; root and walking down the graph. The shape you write is the shape you get back.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;query&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;GetDashboard&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="n"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;published&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&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="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;author&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="n"&gt;name&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="n"&gt;comments&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="n"&gt;text&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Arguments&lt;/strong&gt; like &lt;code&gt;published: true&lt;/code&gt; can appear on any field, not just the root. You could ask for &lt;code&gt;comments(last: 5)&lt;/code&gt; deep inside the tree, and each field's resolver receives its own arguments. This is far more powerful than REST query parameters, which only apply to the endpoint as a whole.&lt;/p&gt;

&lt;p&gt;For anything beyond a hardcoded example you want &lt;strong&gt;variables&lt;/strong&gt; rather than string interpolation. Variables keep your query static (which matters for caching and persisted queries) and let the client pass values separately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;query&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;GetUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;!,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$postCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Int&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="mi"&gt;10&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="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$id&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="n"&gt;name&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$postCount&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="n"&gt;title&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"postCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&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;Variables are declared in the operation signature with their types and optional defaults, then referenced with &lt;code&gt;$&lt;/code&gt;. Never build queries with string concatenation — it is the GraphQL equivalent of SQL injection waiting to happen, and it defeats caching.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Aliases&lt;/strong&gt; let you request the same field twice with different arguments, which would otherwise collide in the response object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;query&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="n"&gt;recent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&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="n"&gt;title&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="n"&gt;popular&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orderBy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;VIEWS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&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="n"&gt;title&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;&lt;strong&gt;Fragments&lt;/strong&gt; are reusable selection sets. They keep queries DRY and, more importantly, are the mechanism that powers component-colocated data requirements in modern clients:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;fragment&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;PostCard&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Post&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="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;author&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="n"&gt;name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="k"&gt;query&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="n"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;published&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="n"&gt;PostCard&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;At GraphQLConf 2025, fragments were a recurring theme, with the community converging on the idea that &lt;strong&gt;fragments are primarily for describing a UI component's data dependencies, not just for reuse&lt;/strong&gt;. A &lt;code&gt;&amp;lt;PostCard /&amp;gt;&lt;/code&gt; React component declares exactly the data it needs as a fragment, and the page query composes those fragments. This pattern, long used internally at Meta with Relay, has now spread across Apollo, urql, and the GraphQL Code Generator client preset.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Directives&lt;/strong&gt; modify execution. The two built into the spec are &lt;code&gt;@include&lt;/code&gt; and &lt;code&gt;@skip&lt;/code&gt;, which conditionally add or remove fields:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;query&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;GetUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;!,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$withPosts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Boolean&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="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$id&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="n"&gt;name&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;posts&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;@include&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;if&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$withPosts&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="n"&gt;title&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are also incremental-delivery directives like &lt;code&gt;@defer&lt;/code&gt; and &lt;code&gt;@stream&lt;/code&gt; for streaming parts of a response as they become available — useful when one field is slow and you do not want it to block the rest. Worth noting: at the 2025 conference, Meta unveiled an &lt;code&gt;@async&lt;/code&gt; directive precisely because &lt;code&gt;@defer&lt;/code&gt; carries hidden overhead, so the streaming story is still actively evolving.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mutations: Changing Data
&lt;/h2&gt;

&lt;p&gt;Mutations look like queries but live under the &lt;code&gt;Mutation&lt;/code&gt; root and signal intent to write. Critically, &lt;strong&gt;top-level mutation fields execute serially&lt;/strong&gt;, one after another, while query fields may run in parallel. This guarantees that if you fire two mutations in one request, the first finishes before the second begins.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;mutation&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;PublishPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CreatePostInput&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="n"&gt;createPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$input&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="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;published&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;A mutation returns data just like a query — and you should lean into that. &lt;strong&gt;Return the modified object (and anything else that changed) so the client can update its cache without a refetch.&lt;/strong&gt; A common best practice is to return a dedicated payload type that wraps the result alongside metadata and typed errors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CreatePostPayload&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="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Post&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;errors&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="n"&gt;UserError&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="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;UserError&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="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern — modeling &lt;em&gt;expected&lt;/em&gt; errors (validation failures, business-rule violations) as part of the schema rather than throwing them into the top-level &lt;code&gt;errors&lt;/code&gt; array — was explicitly endorsed at GraphQLConf 2025 as the way to design scalable, future-proof APIs. It separates "the user typed an invalid email" (a normal, typed outcome) from "the database is on fire" (a real exception).&lt;/p&gt;

&lt;h2&gt;
  
  
  Subscriptions: Real-Time Data
&lt;/h2&gt;

&lt;p&gt;Subscriptions push data to the client when an event occurs, rather than the client polling. They are the basis for live chat, notifications, collaborative editing, and dashboards.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;subscription&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;OnPostPublished&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="n"&gt;postPublished&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="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;author&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="n"&gt;name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under the hood subscriptions usually run over WebSockets (via the &lt;code&gt;graphql-ws&lt;/code&gt; protocol) or Server-Sent Events. The server holds the connection open and emits a payload each time the underlying event fires. Be aware that subscriptions are the hardest part of GraphQL to operate at scale — they hold long-lived connections, complicate horizontal scaling, and historically have been painful in federated setups. Recent work like event-driven federated subscriptions (EDFS) and various SSE-to-WebSocket gateway bridges is actively closing those gaps, but for many teams a simpler polling or &lt;code&gt;@defer&lt;/code&gt; approach is the pragmatic starting point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resolvers: Where the Work Happens
&lt;/h2&gt;

&lt;p&gt;The schema describes &lt;em&gt;what&lt;/em&gt; is possible. &lt;strong&gt;Resolvers&lt;/strong&gt; are the functions that actually produce the data for each field. This is where GraphQL meets your real backend.&lt;/p&gt;

&lt;p&gt;A resolver is a function that receives four arguments, conventionally &lt;code&gt;(parent, args, context, info)&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;parent&lt;/strong&gt; — the result of the parent field's resolver (also called &lt;code&gt;root&lt;/code&gt; or &lt;code&gt;source&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;args&lt;/strong&gt; — the arguments supplied to this field in the query.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;context&lt;/strong&gt; — a per-request object shared across all resolvers, ideal for the authenticated user, database connections, and loaders.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;info&lt;/strong&gt; — metadata about the execution state, including the requested fields. Rarely needed but powerful.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is a minimal server using &lt;strong&gt;Apollo Server&lt;/strong&gt;, the most popular Node.js implementation, wired to the blog schema:&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ApolloServer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@apollo/server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;startStandaloneServer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@apollo/server/standalone&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;typeDefs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`#graphql
  type User {
    id: ID!
    name: String!
    posts: [Post!]!
  }
  type Post {
    id: ID!
    title: String!
    author: User!
  }
  type Query {
    user(id: ID!): User
    posts: [Post!]!
  }
`&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;resolvers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getPosts&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;User&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getPostsByAuthor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authorId&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;server&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;ApolloServer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;typeDefs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;resolvers&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;startStandaloneServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;req&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="na"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;database&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;authenticate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authorization&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4000&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="s2"&gt;`Server ready at &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;url&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 key insight: &lt;strong&gt;resolvers are nested and lazy.&lt;/strong&gt; When a query asks for &lt;code&gt;user.posts.author&lt;/code&gt;, GraphQL first runs &lt;code&gt;Query.user&lt;/code&gt;, passes that result as &lt;code&gt;parent&lt;/code&gt; into &lt;code&gt;User.posts&lt;/code&gt;, then for each post passes &lt;em&gt;it&lt;/em&gt; as &lt;code&gt;parent&lt;/code&gt; into &lt;code&gt;Post.author&lt;/code&gt;. Fields you do not request never run their resolvers. You did not write a single &lt;code&gt;JOIN&lt;/code&gt; — the execution engine walked the graph for you.&lt;/p&gt;

&lt;p&gt;If a field's value can simply be read off the parent object (like &lt;code&gt;User.name&lt;/code&gt; from a row that already has a &lt;code&gt;name&lt;/code&gt; column), you do not even need to write a resolver. GraphQL provides a &lt;strong&gt;default resolver&lt;/strong&gt; that returns &lt;code&gt;parent[fieldName]&lt;/code&gt;. You only write explicit resolvers for fields that require computation, a database hit, or a call to another service.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Request Lifecycle
&lt;/h2&gt;

&lt;p&gt;Understanding what the server does with an incoming operation demystifies a lot of behavior and performance characteristics. Every request goes through three phases.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Parsing.&lt;/strong&gt; The raw query string is tokenized and turned into an Abstract Syntax Tree (AST). Syntax errors are caught here before anything else runs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Validation.&lt;/strong&gt; The AST is checked against the schema. Does that field exist on that type? Are the argument types correct? Is a non-null variable actually provided? Are fragments used on compatible types? Because the schema is strongly typed, an enormous class of errors is rejected here — &lt;em&gt;before&lt;/em&gt; a single resolver or database query executes. This is a major reliability advantage over REST, where a malformed request often only fails deep inside business logic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Execution.&lt;/strong&gt; The validated AST is walked, resolvers fire in dependency order, and results are assembled into a response that mirrors the query shape. Query fields can resolve in parallel; mutation fields at the top level run in series.&lt;/p&gt;

&lt;p&gt;This pipeline is also where you hook in cross-cutting concerns. &lt;strong&gt;Validation rules&lt;/strong&gt; can reject queries that are too deep or too expensive (more on this under security). The September 2025 spec edition also clarified execution and deprecation semantics, making rolling schema changes more predictable in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  The N+1 Problem and DataLoader
&lt;/h2&gt;

&lt;p&gt;Now the most important performance pitfall in GraphQL, the one that bites every team eventually.&lt;/p&gt;

&lt;p&gt;Recall the nested resolver model. Consider this query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;query&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="n"&gt;posts&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="c"&gt;# 1 query: fetch 50 posts&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;author&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="c"&gt;# runs once PER post: 50 more queries!&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;posts&lt;/code&gt; resolver runs once and returns fifty posts. Then GraphQL runs the &lt;code&gt;Post.author&lt;/code&gt; resolver &lt;em&gt;once for each of those fifty posts&lt;/em&gt;. If each invocation does &lt;code&gt;SELECT * FROM users WHERE id = ?&lt;/code&gt;, you have just fired &lt;strong&gt;51 database queries&lt;/strong&gt; to render one list. This is the &lt;strong&gt;N+1 problem&lt;/strong&gt;, and naive GraphQL servers are exceptionally prone to it because the nested resolver model makes it so easy to write.&lt;/p&gt;

&lt;p&gt;The standard solution is &lt;strong&gt;batching and caching per request&lt;/strong&gt;, implemented by the &lt;code&gt;DataLoader&lt;/code&gt; library (originally from Facebook). A DataLoader collects all the individual &lt;code&gt;.load(id)&lt;/code&gt; calls that happen within a single tick of the event loop, then dispatches them as one batched request:&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;DataLoader&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;dataloader&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createUserLoader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;db&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DataLoader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userIds&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="c1"&gt;// userIds = ['1', '7', '7', '12', ...] collected across all 50 author resolvers&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;users&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUsersByIds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userIds&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;byId&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;Map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;u&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;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;]));&lt;/span&gt;
    &lt;span class="c1"&gt;// must return results in the SAME ORDER as the input keys&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;userIds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;byId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&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="c1"&gt;// create a fresh loader per request, in context&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;server&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;ApolloServer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;typeDefs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;resolvers&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nf"&gt;startStandaloneServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;userLoader&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;createUserLoader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;database&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="c1"&gt;// resolver now uses the loader&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resolvers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userLoader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authorId&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;Now those fifty &lt;code&gt;author&lt;/code&gt; resolvers each call &lt;code&gt;userLoader.load(authorId)&lt;/code&gt;, DataLoader batches them into a &lt;em&gt;single&lt;/em&gt; &lt;code&gt;WHERE id IN (...)&lt;/code&gt; query, and the per-request cache means duplicate IDs are deduplicated for free. Fifty-one queries become two.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two rules to remember:&lt;/strong&gt; create a new DataLoader for every request (the cache must not leak between users), and the batch function must return results in exactly the same order as the input keys. Getting the ordering wrong silently mismatches data across records, which is a nasty bug to track down.&lt;/p&gt;

&lt;h2&gt;
  
  
  GraphQL vs REST: An Honest Comparison
&lt;/h2&gt;

&lt;p&gt;Neither wins universally. Here is how they actually stack up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GraphQL's advantages.&lt;/strong&gt; Clients fetch exactly what they need in one round trip, eliminating over- and under-fetching. The strongly typed, introspectable schema gives you free interactive documentation, autocomplete, and end-to-end type safety via codegen. The API evolves additively, so you rarely version. A single endpoint can aggregate many backend services, making it a natural fit for a backend-for-frontend layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GraphQL's costs.&lt;/strong&gt; HTTP caching is harder — since everything is usually a &lt;code&gt;POST&lt;/code&gt; to one URL, you lose the free CDN and browser caching that REST's distinct &lt;code&gt;GET&lt;/code&gt; URLs give you, and you push caching into the client and application layers instead. The server is more complex to build correctly (resolvers, DataLoader, depth limiting). File uploads and binary data are awkward. And the flexibility that delights clients also opens the door to expensive or malicious queries, so you &lt;em&gt;must&lt;/em&gt; add query-cost protections that REST gets somewhat for free.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When REST is the better call.&lt;/strong&gt; Simple CRUD APIs with stable, predictable shapes. Public APIs where aggressive HTTP/CDN caching is critical. Heavy file transfer. Teams who value the operational simplicity and universal tooling of plain HTTP. There is zero shame in REST; for a huge share of services it remains the right default.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When GraphQL shines.&lt;/strong&gt; Mobile and SPA frontends with diverse, rapidly changing data needs. Aggregating multiple microservices or third-party APIs behind one graph. Products where many client teams consume one backend and you want them to move independently. Anywhere the cost of round trips and over-fetching is high.&lt;/p&gt;

&lt;p&gt;Many mature organizations run both: REST for simple service-to-service and public endpoints, GraphQL as the client-facing aggregation layer. It is not a religious war.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pagination Done Right
&lt;/h2&gt;

&lt;p&gt;Offset pagination (&lt;code&gt;?page=2&amp;amp;limit=20&lt;/code&gt;) is simple but breaks under concurrent writes — insert a row while a user paginates and items shift or duplicate across pages. The GraphQL community has largely standardized on &lt;strong&gt;cursor-based pagination&lt;/strong&gt; via the &lt;strong&gt;Relay Connections&lt;/strong&gt; specification.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;PostConnection&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="n"&gt;edges&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="n"&gt;PostEdge&lt;/span&gt;&lt;span class="p"&gt;!]!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;pageInfo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;PageInfo&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="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;PostEdge&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="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Post&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="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;PageInfo&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="n"&gt;hasNextPage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;hasPreviousPage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;startCursor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;endCursor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&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="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Query&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="n"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;after&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;before&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;PostConnection&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;A cursor is an opaque pointer to a position in the list (often a base64-encoded ID or timestamp). The client asks for &lt;code&gt;first: 10, after: "cursor123"&lt;/code&gt; and the server returns the next ten plus a new &lt;code&gt;endCursor&lt;/code&gt;. Because cursors point at stable positions rather than numeric offsets, inserts and deletes elsewhere in the list do not corrupt pagination. The &lt;code&gt;edges&lt;/code&gt;/&lt;code&gt;node&lt;/code&gt; indirection looks verbose, but it gives you a clean place to hang edge-specific metadata (like &lt;code&gt;addedAt&lt;/code&gt; on a membership). Notably, the September 2025 conference introduced &lt;strong&gt;relative cursors&lt;/strong&gt;, which aim to bring familiar "jump to page N" UX back to cursor pagination — a nice acknowledgment that pure cursors lost something offset pagination had.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security: The Part You Cannot Skip
&lt;/h2&gt;

&lt;p&gt;GraphQL's flexibility is a genuine attack surface. A client can request a deeply nested, wildly expensive query that REST's fixed endpoints would never allow. Treat these as non-negotiable for any public-facing graph.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Depth limiting.&lt;/strong&gt; Reject queries nested beyond a sane threshold to stop pathological recursion like &lt;code&gt;posts { author { posts { author { posts ... }}}}&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Query complexity / cost analysis.&lt;/strong&gt; Assign a cost to fields and reject queries whose total exceeds a budget. Apollo Federation standardized a &lt;code&gt;@cost(weight: Int!)&lt;/code&gt; directive and a &lt;code&gt;@listSize&lt;/code&gt; directive to inform this analysis, so the gateway can score a query before executing it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rate limiting.&lt;/strong&gt; Per-client limits, ideally weighted by query cost rather than raw request count, since one GraphQL request can do the work of fifty.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disable introspection in production&lt;/strong&gt; (or restrict it) so attackers cannot trivially map your entire schema, and turn off field suggestions that hint at valid field names.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Persisted (trusted) documents.&lt;/strong&gt; Instead of accepting arbitrary queries, the client registers its queries ahead of time and at runtime sends only a hash. The server executes only known-good operations. At GraphQLConf 2025 the consensus was blunt: &lt;strong&gt;everyone serious is now using persisted/trusted documents&lt;/strong&gt;, with raw arbitrary-query endpoints reserved for genuinely public APIs that accept the risk. This both shrinks the attack surface and slims the request payload.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Authentication and authorization&lt;/strong&gt; belong in resolvers and &lt;code&gt;context&lt;/code&gt;, not in the schema's existence. Authenticate the request once (populate &lt;code&gt;context.user&lt;/code&gt;), then authorize per field or per resolver. Field-level auth — checking that the current user may see &lt;em&gt;this specific&lt;/em&gt; field on &lt;em&gt;this specific&lt;/em&gt; object — is more granular than REST's typical endpoint-level checks, which is both a feature and more code to get right.&lt;/p&gt;

&lt;h2&gt;
  
  
  Federation: GraphQL for Microservices
&lt;/h2&gt;

&lt;p&gt;A single monolithic schema works until many teams need to own different parts of it. &lt;strong&gt;Federation&lt;/strong&gt; lets you split one unified graph across multiple independently deployed services (&lt;strong&gt;subgraphs&lt;/strong&gt;), composed by a &lt;strong&gt;gateway&lt;/strong&gt; or &lt;strong&gt;router&lt;/strong&gt; into a single graph the client sees.&lt;/p&gt;

&lt;p&gt;Each subgraph owns its types and can extend types owned by others using directives like &lt;code&gt;@key&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="c"&gt;# Users subgraph&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&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="c"&gt;# Reviews subgraph — extends User without owning it&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;reviews&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="n"&gt;Review&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="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Review&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="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;User&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;The router reads the query, figures out which subgraphs can resolve which fields, dispatches sub-queries to each, and stitches the results — all invisibly to the client, who just sees one &lt;code&gt;User&lt;/code&gt; type with both &lt;code&gt;name&lt;/code&gt; and &lt;code&gt;reviews&lt;/code&gt;. Apollo Federation popularized this, and it has matured fast: recent Federation versions added directives like &lt;code&gt;@cost&lt;/code&gt;, &lt;code&gt;@listSize&lt;/code&gt;, and &lt;code&gt;@cacheTag&lt;/code&gt; for response caching, and there is now a vendor-neutral &lt;strong&gt;Composite Schema Specification&lt;/strong&gt; working group aiming to standardize federation so routers from different vendors interoperate. The router landscape itself is heating up, with high-performance Rust-based routers (Hive Router, Grafbase, Apollo's own) competing on latency and throughput.&lt;/p&gt;

&lt;p&gt;Federation is powerful but it is not free — it adds a routing layer, schema composition checks in CI, and operational complexity. Reach for it when you genuinely have multiple teams owning distinct domains, not just because microservices sound modern.&lt;/p&gt;

&lt;h2&gt;
  
  
  Client-Side GraphQL
&lt;/h2&gt;

&lt;p&gt;You can consume GraphQL with nothing but &lt;code&gt;fetch&lt;/code&gt; — it is just a &lt;code&gt;POST&lt;/code&gt; with a JSON body:&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;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.example.com/graphql&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="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-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;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`query($id: ID!) { user(id: $id) { name } }`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;4&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;errors&lt;/span&gt; &lt;span class="p"&gt;}&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For real apps you want a client that handles caching, normalization, and request deduplication. &lt;strong&gt;Apollo Client&lt;/strong&gt; is the heavyweight, with a normalized cache, React hooks, and (as of recent releases showcased in 2025) query preloading, Suspense integration, fragment APIs, and data masking. &lt;strong&gt;urql&lt;/strong&gt; is lighter and more composable. &lt;strong&gt;Relay&lt;/strong&gt; is the most opinionated and most powerful at scale, built around fragments and the &lt;code&gt;Node&lt;/code&gt; interface, and is what Meta runs. Newer entrants like &lt;strong&gt;Houdini&lt;/strong&gt; (Svelte-first) and &lt;strong&gt;Isograph&lt;/strong&gt; push the component-data-colocation idea even further.&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useQuery&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;gql&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@apollo/client&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;GET_USER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;gql&lt;/span&gt;&lt;span class="s2"&gt;`
  query GetUser($id: ID!) {
    user(id: $id) {
      name
      posts { title }
    }
  }
`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Profile&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;id&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;loading&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="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;GET_USER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&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;loading&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Spinner&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &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="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Error&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="o"&gt;=&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="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;;
&lt;/span&gt;  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;h1&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;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/h1&amp;gt;&lt;/span&gt;&lt;span class="err"&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 single biggest client-side productivity win is &lt;strong&gt;code generation&lt;/strong&gt;. Tools like GraphQL Code Generator introspect your schema and your operations to produce fully typed hooks and result types. Your editor then autocompletes field names and your build fails if you query a field that does not exist. End-to-end type safety from database to UI, with the schema as the single source of truth, is a large part of why teams fall in love with GraphQL. The September 2025 major release of GraphQL Code Generator notably strengthened typing for both standard and federated servers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tooling and the 2026 Ecosystem
&lt;/h2&gt;

&lt;p&gt;A quick tour of what you will actually touch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GraphiQL&lt;/strong&gt; and the embedded explorers in &lt;strong&gt;Apollo Studio / GraphOS&lt;/strong&gt; give you an interactive, autocompleting query IDE backed by introspection — point it at any endpoint and start exploring. &lt;strong&gt;Schema registries&lt;/strong&gt; track your schema over time and run composition and breaking-change checks in CI. The new &lt;strong&gt;Schema Coordinates&lt;/strong&gt; feature from the September 2025 spec gives tooling a stable, canonical address for every field and type, which makes diffs, linting, and automated PR comments far more reliable.&lt;/p&gt;

&lt;p&gt;Server libraries exist for essentially every language: Apollo Server, GraphQL Yoga, and Mercurius in JavaScript; &lt;strong&gt;graphql-java&lt;/strong&gt; and &lt;strong&gt;DGS&lt;/strong&gt; in Java; &lt;strong&gt;Strawberry&lt;/strong&gt; and &lt;strong&gt;Graphene&lt;/strong&gt; in Python; &lt;strong&gt;gqlgen&lt;/strong&gt; in Go; &lt;strong&gt;async-graphql&lt;/strong&gt; and &lt;strong&gt;Juniper&lt;/strong&gt; in Rust; &lt;strong&gt;Hot Chocolate&lt;/strong&gt; in .NET. Whatever your backend, there is a mature, idiomatic option.&lt;/p&gt;

&lt;p&gt;One genuinely new development worth flagging: the September 2025 spec added &lt;strong&gt;descriptions on executable documents&lt;/strong&gt; (queries, mutations, fragments — not just schema types). This sounds minor but it directly enabled GraphQL operations to be exposed as &lt;strong&gt;MCP tools for AI agents&lt;/strong&gt; without custom infrastructure, since an operation can now carry a standardized, introspectable description of what it does. As AI tooling consumes APIs, a self-describing, strongly typed graph turns out to be an exceptionally good interface for machines as well as humans.&lt;/p&gt;

&lt;h2&gt;
  
  
  Best Practices and Common Pitfalls
&lt;/h2&gt;

&lt;p&gt;A condensed field guide from teams who have shipped this in anger.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Design the schema for the client, not the database.&lt;/strong&gt; Your schema is a product surface, not an ORM dump. Model the domain the way consumers think about it. Do not expose a &lt;code&gt;users_posts_join&lt;/code&gt; table as a type.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Always solve N+1 from day one&lt;/strong&gt; with DataLoader or your library's equivalent. It is not premature optimization; it is the default failure mode.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Model expected errors as typed schema results&lt;/strong&gt;, reserve the top-level &lt;code&gt;errors&lt;/code&gt; array for genuine exceptions, and never leak stack traces or internal messages to clients.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use cursor-based pagination&lt;/strong&gt; for any list that can grow, and adopt the Relay Connections shape so clients and tooling have a consistent contract.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lock down public graphs&lt;/strong&gt; with depth limits, cost analysis, persisted documents, and disabled introspection in production. Do this before launch, not after the incident.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Evolve additively and deprecate with &lt;code&gt;@deprecated&lt;/code&gt;&lt;/strong&gt; rather than versioning. Watch field usage in your registry before removing anything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Adopt fragment colocation and codegen.&lt;/strong&gt; Let components declare their own data needs and let generated types keep frontend and backend honest. This is where the developer-experience payoff compounds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not reach for federation or subscriptions prematurely.&lt;/strong&gt; Both are powerful and both add real operational weight. Start with a single well-designed schema and polling; graduate to federation when multiple teams demand ownership boundaries, and to subscriptions when polling genuinely cannot meet your real-time needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to Adopt GraphQL
&lt;/h2&gt;

&lt;p&gt;GraphQL is not a replacement for REST, a silver bullet, or a fad — it is now a mature, foundation-governed standard that received its first full spec edition since 2021 in September 2025, complete with input unions (&lt;code&gt;@oneOf&lt;/code&gt;), schema coordinates, and operation descriptions. The ecosystem around it — Apollo, Relay, urql, federation routers in Rust, codegen, schema registries — is deep and production-hardened.&lt;/p&gt;

&lt;p&gt;Reach for GraphQL when you have &lt;strong&gt;diverse clients with varied data needs&lt;/strong&gt;, when you are &lt;strong&gt;aggregating multiple services&lt;/strong&gt; behind one interface, when &lt;strong&gt;round-trip cost and over-fetching genuinely hurt&lt;/strong&gt;, or when you want &lt;strong&gt;end-to-end type safety&lt;/strong&gt; with the schema as the contract between teams. Stay with REST when your shapes are stable and simple, when HTTP/CDN caching is mission-critical, or when operational simplicity outweighs client flexibility.&lt;/p&gt;

&lt;p&gt;If you do adopt it, internalize three things and you will avoid most of the pain: &lt;strong&gt;the schema is the contract, resolvers are where performance lives, and a public graph must be defended.&lt;/strong&gt; Get those right and GraphQL delivers exactly what it promised back in 2012 — clients that ask for precisely what they need, and an API that grows without breaking.&lt;/p&gt;

&lt;p&gt;Now go build something. Spin up Apollo Server with the blog schema above, point GraphiQL at it, and write your first query. The fastest way to understand the graph is to traverse it yourself.&lt;/p&gt;

</description>
      <category>react</category>
      <category>marko</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>GOOD NEWS FOR DEW</title>
      <dc:creator>DevUnionX</dc:creator>
      <pubDate>Wed, 17 Jun 2026 21:20:44 +0000</pubDate>
      <link>https://dev.to/devunionx/good-news-for-dew-4m84</link>
      <guid>https://dev.to/devunionx/good-news-for-dew-4m84</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/devunionx/alert5-things-even-ai-cant-do-graphql-1340" class="crayons-story__hidden-navigation-link"&gt;(Alert!)5 Things Even AI Can't Do, GraphQL&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/devunionx" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3180316%2F804c7ae5-1a93-4c38-b9ec-023a59a621a8.jpg" alt="devunionx profile" class="crayons-avatar__image" width="400" height="400"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/devunionx" class="crayons-story__secondary fw-medium m:hidden"&gt;
              DevUnionX
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                DevUnionX
                
              
              &lt;div id="story-author-preview-content-3927313" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/devunionx" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3180316%2F804c7ae5-1a93-4c38-b9ec-023a59a621a8.jpg" class="crayons-avatar__image" alt="" width="400" height="400"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;DevUnionX&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/devunionx/alert5-things-even-ai-cant-do-graphql-1340" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Jun 17&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/devunionx/alert5-things-even-ai-cant-do-graphql-1340" id="article-link-3927313"&gt;
          (Alert!)5 Things Even AI Can't Do, GraphQL
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/graphql"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;graphql&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ai"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ai&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/webdev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;webdev&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/programming"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;programming&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/devunionx/alert5-things-even-ai-cant-do-graphql-1340" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/exploding-head-daceb38d627e6ae9b730f36a1e390fca556a4289d5a41abb2c35068ad3e2c4b5.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;5&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/devunionx/alert5-things-even-ai-cant-do-graphql-1340#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            19 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
    </item>
    <item>
      <title>(Alert!)5 Things Even AI Can't Do, GraphQL</title>
      <dc:creator>DevUnionX</dc:creator>
      <pubDate>Wed, 17 Jun 2026 21:19:59 +0000</pubDate>
      <link>https://dev.to/devunionx/alert5-things-even-ai-cant-do-graphql-1340</link>
      <guid>https://dev.to/devunionx/alert5-things-even-ai-cant-do-graphql-1340</guid>
      <description>&lt;h2&gt;
  
  
  GraphQL: A Complete Guide for Developers in 2026
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;NEWS: MY GAME JUST LAUNCHED&lt;/p&gt;
&lt;/blockquote&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://play.google.com/store/apps/details?id=com.flip_duel_card_battle&amp;amp;amp%3Bhl=tr" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fplay-lh.googleusercontent.com%2FQiVSCNak_1Y7ZpkDvhy_7UNbA0BGOpwEFEmEPfMdo7UcRvrgf4s-7_JIj-FT2tRUTVdFfs8FPKVW6CGIGg7Ksg" height="512" class="m-0" width="512"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://play.google.com/store/apps/details?id=com.flip_duel_card_battle&amp;amp;amp%3Bhl=tr" rel="noopener noreferrer" class="c-link"&gt;
            Flip Duel Card Battle - Apps on Google Play
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Outsmart rivals in 1v1 card duels. Joker, bluff, ranked PvP. 5 rounds.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.gstatic.com%2Fandroid%2Fmarket_images%2Fweb%2Ffavicon_v3.ico" width="32" height="32"&gt;
          play.google.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;If you have built more than a couple of APIs, you have probably felt the friction of REST at scale. You ship an endpoint, the frontend team asks for one more field, you version the route, the mobile team needs a &lt;em&gt;different&lt;/em&gt; shape of the same data, and six months later you are maintaining &lt;code&gt;/v3/users/:id/full&lt;/code&gt; next to &lt;code&gt;/v2/users/:id/summary&lt;/code&gt; and nobody remembers which one the Android app actually calls.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GraphQL was built to kill that exact pain.&lt;/strong&gt; It is a query language and runtime that lets clients ask for precisely the data they need — no more, no less — from a single endpoint, against a strongly typed schema that doubles as living documentation.&lt;/p&gt;

&lt;p&gt;This guide walks through GraphQL from first principles to production concerns. It is aimed at working developers, so expect schema definitions, resolvers, real queries, the N+1 problem, federation, security, and the parts of the ecosystem that actually matter in 2026. By the end you should be able to decide whether GraphQL belongs in your stack and how to build it without shooting yourself in the foot.&lt;/p&gt;

&lt;h2&gt;
  
  
  What GraphQL Actually Is
&lt;/h2&gt;

&lt;p&gt;GraphQL is a specification, not a library or a framework. It was created at Facebook in 2012 to power their mobile apps, open-sourced in 2015, and is now governed by the &lt;strong&gt;GraphQL Foundation&lt;/strong&gt; under the Linux Foundation. The spec defines a query language, a type system, and an execution model — but it deliberately says nothing about which database you use, which programming language you implement it in, or how you transport requests over the wire.&lt;/p&gt;

&lt;p&gt;That last point trips people up, so let it sink in: &lt;strong&gt;GraphQL is transport-agnostic and storage-agnostic.&lt;/strong&gt; Most implementations run over HTTP with JSON, but that is a convention, not a requirement. Your resolvers can pull data from PostgreSQL, a REST microservice, a gRPC backend, Redis, a flat file, or three of those at once. GraphQL sits as a thin coordination layer in front of whatever you already have.&lt;/p&gt;

&lt;p&gt;The mental model is simple. You describe your data as a &lt;strong&gt;graph of types&lt;/strong&gt;. Clients write &lt;strong&gt;queries&lt;/strong&gt; that traverse that graph. The server &lt;strong&gt;resolves&lt;/strong&gt; each requested field by running a function. The response mirrors the shape of the query exactly.&lt;/p&gt;

&lt;p&gt;Here is the canonical hello-world. A query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;query&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="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"4"&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="n"&gt;name&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&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="n"&gt;title&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the response:&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;"data"&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;"user"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Mary Watson"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mary@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"posts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"On the Nature of APIs"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Why I Stopped Versioning"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Cursors, Not Offsets"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice three things. The response shape matches the query shape one-to-one. You got exactly the fields you asked for and nothing else. And you fetched a user &lt;em&gt;and&lt;/em&gt; their posts in a single round trip, even though those are almost certainly two different tables or services on the backend.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problems GraphQL Solves
&lt;/h2&gt;

&lt;p&gt;To appreciate GraphQL you have to be honest about where REST hurts. REST is excellent, widely understood, cache-friendly, and probably the right default for most public APIs. But it has three structural weaknesses that GraphQL attacks directly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Over-fetching.&lt;/strong&gt; A REST endpoint returns a fixed payload. &lt;code&gt;GET /users/4&lt;/code&gt; might return forty fields when your screen needs two. On a desktop with fiber this is invisible. On a phone with spotty 4G, those extra kilobytes per request, multiplied across a list view, add up to real latency and battery drain. GraphQL clients request only the fields they render.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Under-fetching and the N+1 round trip.&lt;/strong&gt; The mirror image. To render a user profile with their recent posts and the comment count on each post, REST often forces you into a waterfall: fetch the user, then fetch their posts, then loop over posts fetching comments. Each step waits for the previous one. The frontend becomes a choreography of chained &lt;code&gt;fetch&lt;/code&gt; calls. GraphQL collapses that into one request because the client describes the entire tree it wants up front.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Endpoint proliferation and versioning.&lt;/strong&gt; REST tends to grow an endpoint per view. A new screen needs a slightly different shape, so you add a route, or a &lt;code&gt;?include=&lt;/code&gt; parameter, or a &lt;code&gt;?fields=&lt;/code&gt; filter, and slowly reinvent a query language badly. GraphQL gives clients that flexibility natively. Because clients select fields explicitly, you can add new fields to a type without ever breaking existing clients — they simply do not ask for the new field. &lt;strong&gt;Most GraphQL APIs never version at all&lt;/strong&gt;; they evolve additively and deprecate fields with metadata rather than spinning up &lt;code&gt;/v2&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The flip side, which we will cover, is that GraphQL trades these wins for harder caching, a more involved server setup, and new categories of performance and security concern. There is no free lunch.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Type System and Schema Definition Language
&lt;/h2&gt;

&lt;p&gt;Everything in GraphQL starts with the &lt;strong&gt;schema&lt;/strong&gt;. The schema is a contract written in the &lt;strong&gt;Schema Definition Language (SDL)&lt;/strong&gt; that declares every type, every field, and every operation your API supports. It is strongly typed and introspectable, which is what powers autocomplete in tooling, codegen, and validation before a single resolver runs.&lt;/p&gt;

&lt;p&gt;Let's build out a small blog API to make the type system concrete.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;User&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="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Role&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;posts&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="n"&gt;Post&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="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Post&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="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;published&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;comments&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="n"&gt;Comment&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="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Comment&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="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;User&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="k"&gt;enum&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Role&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="n"&gt;ADMIN&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;EDITOR&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;READER&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;A few things to unpack here, because the syntax is dense with meaning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scalars&lt;/strong&gt; are the leaf values. GraphQL ships with five built-in scalars: &lt;code&gt;Int&lt;/code&gt;, &lt;code&gt;Float&lt;/code&gt;, &lt;code&gt;String&lt;/code&gt;, &lt;code&gt;Boolean&lt;/code&gt;, and &lt;code&gt;ID&lt;/code&gt;. &lt;code&gt;ID&lt;/code&gt; is a string under the hood but signals "this is a unique identifier" to tooling. You can and should define &lt;strong&gt;custom scalars&lt;/strong&gt; like &lt;code&gt;DateTime&lt;/code&gt;, &lt;code&gt;Email&lt;/code&gt;, or &lt;code&gt;URL&lt;/code&gt; to add validation and semantic meaning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The exclamation mark means non-null.&lt;/strong&gt; &lt;code&gt;String!&lt;/code&gt; is a string that will never be null. &lt;code&gt;[Post!]!&lt;/code&gt; is a non-null list of non-null posts — the list itself is always present (possibly empty), and no element inside it is ever null. &lt;code&gt;[Post]&lt;/code&gt; would be a nullable list that may contain nulls. This nullability system is one of GraphQL's quietest strengths: it pushes a huge class of "cannot read property of undefined" bugs into the type checker.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Object types&lt;/strong&gt; like &lt;code&gt;User&lt;/code&gt; and &lt;code&gt;Post&lt;/code&gt; are the nodes of your graph, and the fields that point to other object types are the edges. &lt;code&gt;User.posts&lt;/code&gt; connects a user to its posts; &lt;code&gt;Post.author&lt;/code&gt; connects back. That bidirectional linking is exactly why it is called a &lt;em&gt;graph&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Beyond objects and scalars, the type system has a few more tools you will reach for constantly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enums&lt;/strong&gt; restrict a field to a fixed set of values, as &lt;code&gt;Role&lt;/code&gt; shows above. &lt;strong&gt;Input types&lt;/strong&gt; describe the structured arguments you pass into mutations — they look like object types but use the &lt;code&gt;input&lt;/code&gt; keyword and cannot have fields that resolve to object types. &lt;strong&gt;Interfaces&lt;/strong&gt; define a set of fields that multiple types must implement. &lt;strong&gt;Unions&lt;/strong&gt; say a field can return one of several types that need not share any fields.&lt;/p&gt;

&lt;p&gt;Here is an interface and a union in practice:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;interface&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Node&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="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ID&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="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;implements&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Node&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="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;altText&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&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="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Video&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;implements&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Node&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="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;durationSeconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Int&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="k"&gt;union&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;SearchResult&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="n"&gt;User&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="n"&gt;Post&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="n"&gt;Image&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="k"&gt;input&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CreatePostInput&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="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;published&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Boolean&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;false&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;The &lt;code&gt;Node&lt;/code&gt; interface is the foundation of the &lt;strong&gt;Relay&lt;/strong&gt; specification for global object identification — any type implementing &lt;code&gt;Node&lt;/code&gt; can be refetched by its global &lt;code&gt;id&lt;/code&gt;. Unions are perfect for search results or activity feeds where heterogeneous types share a list. Note the default value &lt;code&gt;published: Boolean = false&lt;/code&gt; in the input — defaults are first-class in SDL.&lt;/p&gt;

&lt;p&gt;Finally, the three special &lt;strong&gt;root operation types&lt;/strong&gt; are the entry points into the entire graph:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Query&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="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;!):&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;published&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Boolean&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="n"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;!]!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;term&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&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="n"&gt;SearchResult&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="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Mutation&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="n"&gt;createPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CreatePostInput&lt;/span&gt;&lt;span class="p"&gt;!):&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;deletePost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;!):&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Boolean&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="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Subscription&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="n"&gt;postPublished&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Post&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;&lt;code&gt;Query&lt;/code&gt; is for reads, &lt;code&gt;Mutation&lt;/code&gt; is for writes, and &lt;code&gt;Subscription&lt;/code&gt; is for real-time streams. Every operation a client can perform must be reachable from one of these three roots. This is the complete public surface of your API in one readable document — which is precisely why a good schema is the single most important artifact in a GraphQL project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Queries: Asking for Data
&lt;/h2&gt;

&lt;p&gt;A query selects fields starting from the &lt;code&gt;Query&lt;/code&gt; root and walking down the graph. The shape you write is the shape you get back.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;query&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;GetDashboard&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="n"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;published&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&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="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;author&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="n"&gt;name&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="n"&gt;comments&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="n"&gt;text&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Arguments&lt;/strong&gt; like &lt;code&gt;published: true&lt;/code&gt; can appear on any field, not just the root. You could ask for &lt;code&gt;comments(last: 5)&lt;/code&gt; deep inside the tree, and each field's resolver receives its own arguments. This is far more powerful than REST query parameters, which only apply to the endpoint as a whole.&lt;/p&gt;

&lt;p&gt;For anything beyond a hardcoded example you want &lt;strong&gt;variables&lt;/strong&gt; rather than string interpolation. Variables keep your query static (which matters for caching and persisted queries) and let the client pass values separately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;query&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;GetUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;!,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$postCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Int&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="mi"&gt;10&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="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$id&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="n"&gt;name&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$postCount&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="n"&gt;title&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"postCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&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;Variables are declared in the operation signature with their types and optional defaults, then referenced with &lt;code&gt;$&lt;/code&gt;. Never build queries with string concatenation — it is the GraphQL equivalent of SQL injection waiting to happen, and it defeats caching.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Aliases&lt;/strong&gt; let you request the same field twice with different arguments, which would otherwise collide in the response object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;query&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="n"&gt;recent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&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="n"&gt;title&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="n"&gt;popular&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orderBy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;VIEWS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&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="n"&gt;title&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;&lt;strong&gt;Fragments&lt;/strong&gt; are reusable selection sets. They keep queries DRY and, more importantly, are the mechanism that powers component-colocated data requirements in modern clients:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;fragment&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;PostCard&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Post&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="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;author&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="n"&gt;name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="k"&gt;query&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="n"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;published&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="n"&gt;PostCard&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;At GraphQLConf 2025, fragments were a recurring theme, with the community converging on the idea that &lt;strong&gt;fragments are primarily for describing a UI component's data dependencies, not just for reuse&lt;/strong&gt;. A &lt;code&gt;&amp;lt;PostCard /&amp;gt;&lt;/code&gt; React component declares exactly the data it needs as a fragment, and the page query composes those fragments. This pattern, long used internally at Meta with Relay, has now spread across Apollo, urql, and the GraphQL Code Generator client preset.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Directives&lt;/strong&gt; modify execution. The two built into the spec are &lt;code&gt;@include&lt;/code&gt; and &lt;code&gt;@skip&lt;/code&gt;, which conditionally add or remove fields:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;query&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;GetUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;!,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$withPosts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Boolean&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="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$id&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="n"&gt;name&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;posts&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;@include&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;if&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$withPosts&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="n"&gt;title&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are also incremental-delivery directives like &lt;code&gt;@defer&lt;/code&gt; and &lt;code&gt;@stream&lt;/code&gt; for streaming parts of a response as they become available — useful when one field is slow and you do not want it to block the rest. Worth noting: at the 2025 conference, Meta unveiled an &lt;code&gt;@async&lt;/code&gt; directive precisely because &lt;code&gt;@defer&lt;/code&gt; carries hidden overhead, so the streaming story is still actively evolving.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mutations: Changing Data
&lt;/h2&gt;

&lt;p&gt;Mutations look like queries but live under the &lt;code&gt;Mutation&lt;/code&gt; root and signal intent to write. Critically, &lt;strong&gt;top-level mutation fields execute serially&lt;/strong&gt;, one after another, while query fields may run in parallel. This guarantees that if you fire two mutations in one request, the first finishes before the second begins.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;mutation&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;PublishPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CreatePostInput&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="n"&gt;createPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$input&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="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;published&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;A mutation returns data just like a query — and you should lean into that. &lt;strong&gt;Return the modified object (and anything else that changed) so the client can update its cache without a refetch.&lt;/strong&gt; A common best practice is to return a dedicated payload type that wraps the result alongside metadata and typed errors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CreatePostPayload&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="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Post&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;errors&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="n"&gt;UserError&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="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;UserError&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="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern — modeling &lt;em&gt;expected&lt;/em&gt; errors (validation failures, business-rule violations) as part of the schema rather than throwing them into the top-level &lt;code&gt;errors&lt;/code&gt; array — was explicitly endorsed at GraphQLConf 2025 as the way to design scalable, future-proof APIs. It separates "the user typed an invalid email" (a normal, typed outcome) from "the database is on fire" (a real exception).&lt;/p&gt;

&lt;h2&gt;
  
  
  Subscriptions: Real-Time Data
&lt;/h2&gt;

&lt;p&gt;Subscriptions push data to the client when an event occurs, rather than the client polling. They are the basis for live chat, notifications, collaborative editing, and dashboards.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;subscription&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;OnPostPublished&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="n"&gt;postPublished&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="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;author&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="n"&gt;name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under the hood subscriptions usually run over WebSockets (via the &lt;code&gt;graphql-ws&lt;/code&gt; protocol) or Server-Sent Events. The server holds the connection open and emits a payload each time the underlying event fires. Be aware that subscriptions are the hardest part of GraphQL to operate at scale — they hold long-lived connections, complicate horizontal scaling, and historically have been painful in federated setups. Recent work like event-driven federated subscriptions (EDFS) and various SSE-to-WebSocket gateway bridges is actively closing those gaps, but for many teams a simpler polling or &lt;code&gt;@defer&lt;/code&gt; approach is the pragmatic starting point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resolvers: Where the Work Happens
&lt;/h2&gt;

&lt;p&gt;The schema describes &lt;em&gt;what&lt;/em&gt; is possible. &lt;strong&gt;Resolvers&lt;/strong&gt; are the functions that actually produce the data for each field. This is where GraphQL meets your real backend.&lt;/p&gt;

&lt;p&gt;A resolver is a function that receives four arguments, conventionally &lt;code&gt;(parent, args, context, info)&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;parent&lt;/strong&gt; — the result of the parent field's resolver (also called &lt;code&gt;root&lt;/code&gt; or &lt;code&gt;source&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;args&lt;/strong&gt; — the arguments supplied to this field in the query.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;context&lt;/strong&gt; — a per-request object shared across all resolvers, ideal for the authenticated user, database connections, and loaders.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;info&lt;/strong&gt; — metadata about the execution state, including the requested fields. Rarely needed but powerful.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is a minimal server using &lt;strong&gt;Apollo Server&lt;/strong&gt;, the most popular Node.js implementation, wired to the blog schema:&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ApolloServer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@apollo/server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;startStandaloneServer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@apollo/server/standalone&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;typeDefs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`#graphql
  type User {
    id: ID!
    name: String!
    posts: [Post!]!
  }
  type Post {
    id: ID!
    title: String!
    author: User!
  }
  type Query {
    user(id: ID!): User
    posts: [Post!]!
  }
`&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;resolvers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getPosts&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;User&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getPostsByAuthor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authorId&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;server&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;ApolloServer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;typeDefs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;resolvers&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;startStandaloneServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;req&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="na"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;database&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;authenticate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authorization&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4000&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="s2"&gt;`Server ready at &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;url&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 key insight: &lt;strong&gt;resolvers are nested and lazy.&lt;/strong&gt; When a query asks for &lt;code&gt;user.posts.author&lt;/code&gt;, GraphQL first runs &lt;code&gt;Query.user&lt;/code&gt;, passes that result as &lt;code&gt;parent&lt;/code&gt; into &lt;code&gt;User.posts&lt;/code&gt;, then for each post passes &lt;em&gt;it&lt;/em&gt; as &lt;code&gt;parent&lt;/code&gt; into &lt;code&gt;Post.author&lt;/code&gt;. Fields you do not request never run their resolvers. You did not write a single &lt;code&gt;JOIN&lt;/code&gt; — the execution engine walked the graph for you.&lt;/p&gt;

&lt;p&gt;If a field's value can simply be read off the parent object (like &lt;code&gt;User.name&lt;/code&gt; from a row that already has a &lt;code&gt;name&lt;/code&gt; column), you do not even need to write a resolver. GraphQL provides a &lt;strong&gt;default resolver&lt;/strong&gt; that returns &lt;code&gt;parent[fieldName]&lt;/code&gt;. You only write explicit resolvers for fields that require computation, a database hit, or a call to another service.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Request Lifecycle
&lt;/h2&gt;

&lt;p&gt;Understanding what the server does with an incoming operation demystifies a lot of behavior and performance characteristics. Every request goes through three phases.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Parsing.&lt;/strong&gt; The raw query string is tokenized and turned into an Abstract Syntax Tree (AST). Syntax errors are caught here before anything else runs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Validation.&lt;/strong&gt; The AST is checked against the schema. Does that field exist on that type? Are the argument types correct? Is a non-null variable actually provided? Are fragments used on compatible types? Because the schema is strongly typed, an enormous class of errors is rejected here — &lt;em&gt;before&lt;/em&gt; a single resolver or database query executes. This is a major reliability advantage over REST, where a malformed request often only fails deep inside business logic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Execution.&lt;/strong&gt; The validated AST is walked, resolvers fire in dependency order, and results are assembled into a response that mirrors the query shape. Query fields can resolve in parallel; mutation fields at the top level run in series.&lt;/p&gt;

&lt;p&gt;This pipeline is also where you hook in cross-cutting concerns. &lt;strong&gt;Validation rules&lt;/strong&gt; can reject queries that are too deep or too expensive (more on this under security). The September 2025 spec edition also clarified execution and deprecation semantics, making rolling schema changes more predictable in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  The N+1 Problem and DataLoader
&lt;/h2&gt;

&lt;p&gt;Now the most important performance pitfall in GraphQL, the one that bites every team eventually.&lt;/p&gt;

&lt;p&gt;Recall the nested resolver model. Consider this query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;query&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="n"&gt;posts&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="c"&gt;# 1 query: fetch 50 posts&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;author&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="c"&gt;# runs once PER post: 50 more queries!&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;posts&lt;/code&gt; resolver runs once and returns fifty posts. Then GraphQL runs the &lt;code&gt;Post.author&lt;/code&gt; resolver &lt;em&gt;once for each of those fifty posts&lt;/em&gt;. If each invocation does &lt;code&gt;SELECT * FROM users WHERE id = ?&lt;/code&gt;, you have just fired &lt;strong&gt;51 database queries&lt;/strong&gt; to render one list. This is the &lt;strong&gt;N+1 problem&lt;/strong&gt;, and naive GraphQL servers are exceptionally prone to it because the nested resolver model makes it so easy to write.&lt;/p&gt;

&lt;p&gt;The standard solution is &lt;strong&gt;batching and caching per request&lt;/strong&gt;, implemented by the &lt;code&gt;DataLoader&lt;/code&gt; library (originally from Facebook). A DataLoader collects all the individual &lt;code&gt;.load(id)&lt;/code&gt; calls that happen within a single tick of the event loop, then dispatches them as one batched request:&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;DataLoader&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;dataloader&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createUserLoader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;db&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DataLoader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userIds&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="c1"&gt;// userIds = ['1', '7', '7', '12', ...] collected across all 50 author resolvers&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;users&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUsersByIds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userIds&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;byId&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;Map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;u&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;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;]));&lt;/span&gt;
    &lt;span class="c1"&gt;// must return results in the SAME ORDER as the input keys&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;userIds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;byId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&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="c1"&gt;// create a fresh loader per request, in context&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;server&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;ApolloServer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;typeDefs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;resolvers&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nf"&gt;startStandaloneServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;userLoader&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;createUserLoader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;database&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="c1"&gt;// resolver now uses the loader&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resolvers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userLoader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authorId&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;Now those fifty &lt;code&gt;author&lt;/code&gt; resolvers each call &lt;code&gt;userLoader.load(authorId)&lt;/code&gt;, DataLoader batches them into a &lt;em&gt;single&lt;/em&gt; &lt;code&gt;WHERE id IN (...)&lt;/code&gt; query, and the per-request cache means duplicate IDs are deduplicated for free. Fifty-one queries become two.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two rules to remember:&lt;/strong&gt; create a new DataLoader for every request (the cache must not leak between users), and the batch function must return results in exactly the same order as the input keys. Getting the ordering wrong silently mismatches data across records, which is a nasty bug to track down.&lt;/p&gt;

&lt;h2&gt;
  
  
  GraphQL vs REST: An Honest Comparison
&lt;/h2&gt;

&lt;p&gt;Neither wins universally. Here is how they actually stack up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GraphQL's advantages.&lt;/strong&gt; Clients fetch exactly what they need in one round trip, eliminating over- and under-fetching. The strongly typed, introspectable schema gives you free interactive documentation, autocomplete, and end-to-end type safety via codegen. The API evolves additively, so you rarely version. A single endpoint can aggregate many backend services, making it a natural fit for a backend-for-frontend layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GraphQL's costs.&lt;/strong&gt; HTTP caching is harder — since everything is usually a &lt;code&gt;POST&lt;/code&gt; to one URL, you lose the free CDN and browser caching that REST's distinct &lt;code&gt;GET&lt;/code&gt; URLs give you, and you push caching into the client and application layers instead. The server is more complex to build correctly (resolvers, DataLoader, depth limiting). File uploads and binary data are awkward. And the flexibility that delights clients also opens the door to expensive or malicious queries, so you &lt;em&gt;must&lt;/em&gt; add query-cost protections that REST gets somewhat for free.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When REST is the better call.&lt;/strong&gt; Simple CRUD APIs with stable, predictable shapes. Public APIs where aggressive HTTP/CDN caching is critical. Heavy file transfer. Teams who value the operational simplicity and universal tooling of plain HTTP. There is zero shame in REST; for a huge share of services it remains the right default.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When GraphQL shines.&lt;/strong&gt; Mobile and SPA frontends with diverse, rapidly changing data needs. Aggregating multiple microservices or third-party APIs behind one graph. Products where many client teams consume one backend and you want them to move independently. Anywhere the cost of round trips and over-fetching is high.&lt;/p&gt;

&lt;p&gt;Many mature organizations run both: REST for simple service-to-service and public endpoints, GraphQL as the client-facing aggregation layer. It is not a religious war.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pagination Done Right
&lt;/h2&gt;

&lt;p&gt;Offset pagination (&lt;code&gt;?page=2&amp;amp;limit=20&lt;/code&gt;) is simple but breaks under concurrent writes — insert a row while a user paginates and items shift or duplicate across pages. The GraphQL community has largely standardized on &lt;strong&gt;cursor-based pagination&lt;/strong&gt; via the &lt;strong&gt;Relay Connections&lt;/strong&gt; specification.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;PostConnection&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="n"&gt;edges&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="n"&gt;PostEdge&lt;/span&gt;&lt;span class="p"&gt;!]!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;pageInfo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;PageInfo&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="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;PostEdge&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="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Post&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="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;PageInfo&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="n"&gt;hasNextPage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;hasPreviousPage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;startCursor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;endCursor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&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="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Query&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="n"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;after&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;before&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;PostConnection&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;A cursor is an opaque pointer to a position in the list (often a base64-encoded ID or timestamp). The client asks for &lt;code&gt;first: 10, after: "cursor123"&lt;/code&gt; and the server returns the next ten plus a new &lt;code&gt;endCursor&lt;/code&gt;. Because cursors point at stable positions rather than numeric offsets, inserts and deletes elsewhere in the list do not corrupt pagination. The &lt;code&gt;edges&lt;/code&gt;/&lt;code&gt;node&lt;/code&gt; indirection looks verbose, but it gives you a clean place to hang edge-specific metadata (like &lt;code&gt;addedAt&lt;/code&gt; on a membership). Notably, the September 2025 conference introduced &lt;strong&gt;relative cursors&lt;/strong&gt;, which aim to bring familiar "jump to page N" UX back to cursor pagination — a nice acknowledgment that pure cursors lost something offset pagination had.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security: The Part You Cannot Skip
&lt;/h2&gt;

&lt;p&gt;GraphQL's flexibility is a genuine attack surface. A client can request a deeply nested, wildly expensive query that REST's fixed endpoints would never allow. Treat these as non-negotiable for any public-facing graph.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Depth limiting.&lt;/strong&gt; Reject queries nested beyond a sane threshold to stop pathological recursion like &lt;code&gt;posts { author { posts { author { posts ... }}}}&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Query complexity / cost analysis.&lt;/strong&gt; Assign a cost to fields and reject queries whose total exceeds a budget. Apollo Federation standardized a &lt;code&gt;@cost(weight: Int!)&lt;/code&gt; directive and a &lt;code&gt;@listSize&lt;/code&gt; directive to inform this analysis, so the gateway can score a query before executing it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rate limiting.&lt;/strong&gt; Per-client limits, ideally weighted by query cost rather than raw request count, since one GraphQL request can do the work of fifty.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disable introspection in production&lt;/strong&gt; (or restrict it) so attackers cannot trivially map your entire schema, and turn off field suggestions that hint at valid field names.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Persisted (trusted) documents.&lt;/strong&gt; Instead of accepting arbitrary queries, the client registers its queries ahead of time and at runtime sends only a hash. The server executes only known-good operations. At GraphQLConf 2025 the consensus was blunt: &lt;strong&gt;everyone serious is now using persisted/trusted documents&lt;/strong&gt;, with raw arbitrary-query endpoints reserved for genuinely public APIs that accept the risk. This both shrinks the attack surface and slims the request payload.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Authentication and authorization&lt;/strong&gt; belong in resolvers and &lt;code&gt;context&lt;/code&gt;, not in the schema's existence. Authenticate the request once (populate &lt;code&gt;context.user&lt;/code&gt;), then authorize per field or per resolver. Field-level auth — checking that the current user may see &lt;em&gt;this specific&lt;/em&gt; field on &lt;em&gt;this specific&lt;/em&gt; object — is more granular than REST's typical endpoint-level checks, which is both a feature and more code to get right.&lt;/p&gt;

&lt;h2&gt;
  
  
  Federation: GraphQL for Microservices
&lt;/h2&gt;

&lt;p&gt;A single monolithic schema works until many teams need to own different parts of it. &lt;strong&gt;Federation&lt;/strong&gt; lets you split one unified graph across multiple independently deployed services (&lt;strong&gt;subgraphs&lt;/strong&gt;), composed by a &lt;strong&gt;gateway&lt;/strong&gt; or &lt;strong&gt;router&lt;/strong&gt; into a single graph the client sees.&lt;/p&gt;

&lt;p&gt;Each subgraph owns its types and can extend types owned by others using directives like &lt;code&gt;@key&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="c"&gt;# Users subgraph&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&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="c"&gt;# Reviews subgraph — extends User without owning it&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;reviews&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="n"&gt;Review&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="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Review&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="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;User&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;The router reads the query, figures out which subgraphs can resolve which fields, dispatches sub-queries to each, and stitches the results — all invisibly to the client, who just sees one &lt;code&gt;User&lt;/code&gt; type with both &lt;code&gt;name&lt;/code&gt; and &lt;code&gt;reviews&lt;/code&gt;. Apollo Federation popularized this, and it has matured fast: recent Federation versions added directives like &lt;code&gt;@cost&lt;/code&gt;, &lt;code&gt;@listSize&lt;/code&gt;, and &lt;code&gt;@cacheTag&lt;/code&gt; for response caching, and there is now a vendor-neutral &lt;strong&gt;Composite Schema Specification&lt;/strong&gt; working group aiming to standardize federation so routers from different vendors interoperate. The router landscape itself is heating up, with high-performance Rust-based routers (Hive Router, Grafbase, Apollo's own) competing on latency and throughput.&lt;/p&gt;

&lt;p&gt;Federation is powerful but it is not free — it adds a routing layer, schema composition checks in CI, and operational complexity. Reach for it when you genuinely have multiple teams owning distinct domains, not just because microservices sound modern.&lt;/p&gt;

&lt;h2&gt;
  
  
  Client-Side GraphQL
&lt;/h2&gt;

&lt;p&gt;You can consume GraphQL with nothing but &lt;code&gt;fetch&lt;/code&gt; — it is just a &lt;code&gt;POST&lt;/code&gt; with a JSON body:&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;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.example.com/graphql&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="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-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;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`query($id: ID!) { user(id: $id) { name } }`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;4&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;errors&lt;/span&gt; &lt;span class="p"&gt;}&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For real apps you want a client that handles caching, normalization, and request deduplication. &lt;strong&gt;Apollo Client&lt;/strong&gt; is the heavyweight, with a normalized cache, React hooks, and (as of recent releases showcased in 2025) query preloading, Suspense integration, fragment APIs, and data masking. &lt;strong&gt;urql&lt;/strong&gt; is lighter and more composable. &lt;strong&gt;Relay&lt;/strong&gt; is the most opinionated and most powerful at scale, built around fragments and the &lt;code&gt;Node&lt;/code&gt; interface, and is what Meta runs. Newer entrants like &lt;strong&gt;Houdini&lt;/strong&gt; (Svelte-first) and &lt;strong&gt;Isograph&lt;/strong&gt; push the component-data-colocation idea even further.&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useQuery&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;gql&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@apollo/client&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;GET_USER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;gql&lt;/span&gt;&lt;span class="s2"&gt;`
  query GetUser($id: ID!) {
    user(id: $id) {
      name
      posts { title }
    }
  }
`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Profile&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;id&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;loading&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="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;GET_USER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&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;loading&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Spinner&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &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="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Error&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="o"&gt;=&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="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;;
&lt;/span&gt;  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;h1&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;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/h1&amp;gt;&lt;/span&gt;&lt;span class="err"&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 single biggest client-side productivity win is &lt;strong&gt;code generation&lt;/strong&gt;. Tools like GraphQL Code Generator introspect your schema and your operations to produce fully typed hooks and result types. Your editor then autocompletes field names and your build fails if you query a field that does not exist. End-to-end type safety from database to UI, with the schema as the single source of truth, is a large part of why teams fall in love with GraphQL. The September 2025 major release of GraphQL Code Generator notably strengthened typing for both standard and federated servers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tooling and the 2026 Ecosystem
&lt;/h2&gt;

&lt;p&gt;A quick tour of what you will actually touch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GraphiQL&lt;/strong&gt; and the embedded explorers in &lt;strong&gt;Apollo Studio / GraphOS&lt;/strong&gt; give you an interactive, autocompleting query IDE backed by introspection — point it at any endpoint and start exploring. &lt;strong&gt;Schema registries&lt;/strong&gt; track your schema over time and run composition and breaking-change checks in CI. The new &lt;strong&gt;Schema Coordinates&lt;/strong&gt; feature from the September 2025 spec gives tooling a stable, canonical address for every field and type, which makes diffs, linting, and automated PR comments far more reliable.&lt;/p&gt;

&lt;p&gt;Server libraries exist for essentially every language: Apollo Server, GraphQL Yoga, and Mercurius in JavaScript; &lt;strong&gt;graphql-java&lt;/strong&gt; and &lt;strong&gt;DGS&lt;/strong&gt; in Java; &lt;strong&gt;Strawberry&lt;/strong&gt; and &lt;strong&gt;Graphene&lt;/strong&gt; in Python; &lt;strong&gt;gqlgen&lt;/strong&gt; in Go; &lt;strong&gt;async-graphql&lt;/strong&gt; and &lt;strong&gt;Juniper&lt;/strong&gt; in Rust; &lt;strong&gt;Hot Chocolate&lt;/strong&gt; in .NET. Whatever your backend, there is a mature, idiomatic option.&lt;/p&gt;

&lt;p&gt;One genuinely new development worth flagging: the September 2025 spec added &lt;strong&gt;descriptions on executable documents&lt;/strong&gt; (queries, mutations, fragments — not just schema types). This sounds minor but it directly enabled GraphQL operations to be exposed as &lt;strong&gt;MCP tools for AI agents&lt;/strong&gt; without custom infrastructure, since an operation can now carry a standardized, introspectable description of what it does. As AI tooling consumes APIs, a self-describing, strongly typed graph turns out to be an exceptionally good interface for machines as well as humans.&lt;/p&gt;

&lt;h2&gt;
  
  
  Best Practices and Common Pitfalls
&lt;/h2&gt;

&lt;p&gt;A condensed field guide from teams who have shipped this in anger.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Design the schema for the client, not the database.&lt;/strong&gt; Your schema is a product surface, not an ORM dump. Model the domain the way consumers think about it. Do not expose a &lt;code&gt;users_posts_join&lt;/code&gt; table as a type.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Always solve N+1 from day one&lt;/strong&gt; with DataLoader or your library's equivalent. It is not premature optimization; it is the default failure mode.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Model expected errors as typed schema results&lt;/strong&gt;, reserve the top-level &lt;code&gt;errors&lt;/code&gt; array for genuine exceptions, and never leak stack traces or internal messages to clients.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use cursor-based pagination&lt;/strong&gt; for any list that can grow, and adopt the Relay Connections shape so clients and tooling have a consistent contract.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lock down public graphs&lt;/strong&gt; with depth limits, cost analysis, persisted documents, and disabled introspection in production. Do this before launch, not after the incident.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Evolve additively and deprecate with &lt;code&gt;@deprecated&lt;/code&gt;&lt;/strong&gt; rather than versioning. Watch field usage in your registry before removing anything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Adopt fragment colocation and codegen.&lt;/strong&gt; Let components declare their own data needs and let generated types keep frontend and backend honest. This is where the developer-experience payoff compounds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not reach for federation or subscriptions prematurely.&lt;/strong&gt; Both are powerful and both add real operational weight. Start with a single well-designed schema and polling; graduate to federation when multiple teams demand ownership boundaries, and to subscriptions when polling genuinely cannot meet your real-time needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to Adopt GraphQL
&lt;/h2&gt;

&lt;p&gt;GraphQL is not a replacement for REST, a silver bullet, or a fad — it is now a mature, foundation-governed standard that received its first full spec edition since 2021 in September 2025, complete with input unions (&lt;code&gt;@oneOf&lt;/code&gt;), schema coordinates, and operation descriptions. The ecosystem around it — Apollo, Relay, urql, federation routers in Rust, codegen, schema registries — is deep and production-hardened.&lt;/p&gt;

&lt;p&gt;Reach for GraphQL when you have &lt;strong&gt;diverse clients with varied data needs&lt;/strong&gt;, when you are &lt;strong&gt;aggregating multiple services&lt;/strong&gt; behind one interface, when &lt;strong&gt;round-trip cost and over-fetching genuinely hurt&lt;/strong&gt;, or when you want &lt;strong&gt;end-to-end type safety&lt;/strong&gt; with the schema as the contract between teams. Stay with REST when your shapes are stable and simple, when HTTP/CDN caching is mission-critical, or when operational simplicity outweighs client flexibility.&lt;/p&gt;

&lt;p&gt;If you do adopt it, internalize three things and you will avoid most of the pain: &lt;strong&gt;the schema is the contract, resolvers are where performance lives, and a public graph must be defended.&lt;/strong&gt; Get those right and GraphQL delivers exactly what it promised back in 2012 — clients that ask for precisely what they need, and an API that grows without breaking.&lt;/p&gt;

&lt;p&gt;Now go build something. Spin up Apollo Server with the blog schema above, point GraphiQL at it, and write your first query. The fastest way to understand the graph is to traverse it yourself.&lt;/p&gt;

</description>
      <category>graphql</category>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>5 Things Even AI Can't Do, REST API</title>
      <dc:creator>DevUnionX</dc:creator>
      <pubDate>Wed, 27 May 2026 01:22:59 +0000</pubDate>
      <link>https://dev.to/devunionx/5-things-even-ai-cant-dorest-api-plk</link>
      <guid>https://dev.to/devunionx/5-things-even-ai-cant-dorest-api-plk</guid>
      <description>&lt;h2&gt;
  
  
  Roy Fielding Would Like a Word About Your "REST API"
&lt;/h2&gt;

&lt;p&gt;In October 2008, Roy Fielding — the person who literally invented REST in a year 2000 PhD dissertation, the same person who co-authored HTTP — opened a blog post with this sentence:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I am getting frustrated by the number of people calling any HTTP-based interface a REST API.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;He then proceeded, over about a thousand words, to roast the entire industry for calling things REST that were not, in fact, REST. The line that everyone still quotes, eighteen years later:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;That is RPC. It screams RPC. There is so much coupling on display that it should be given an X rating.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Reader, I have built dozens of APIs in my career that I called REST APIs, that were not REST APIs by Fielding's definition. You probably have too. Almost every "REST API" you have ever used is, by the inventor's own standard, not REST. And the industry has collectively decided that's fine, and we use the word REST anyway, and Fielding is presumably still grumpy about it from his office in California.&lt;/p&gt;

&lt;p&gt;This article is the long version of what REST actually is, what people &lt;em&gt;mean&lt;/em&gt; when they say REST in 2026, the difference between the two, and the daily-practice details — status codes, idempotency, pagination, error formats, versioning — that you actually need to get right whether you call your API REST or REST-ish or HTTP+JSON or whatever you want to call it. Get coffee. There's a lot of ground to cover.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Dissertation Nobody Read
&lt;/h2&gt;

&lt;p&gt;Roy Thomas Fielding's PhD dissertation, &lt;em&gt;Architectural Styles and the Design of Network-based Software Architectures&lt;/em&gt;, was published at UC Irvine in 2000. It is 180 pages long. Chapter 5 is titled "Representational State Transfer (REST)" and it's where the term came from. If you've never read it, that's fine — almost nobody has. But you should know it exists, because every argument about whether something is "really REST" eventually comes back to this document.&lt;/p&gt;

&lt;p&gt;The setup matters. Fielding wrote the dissertation while he was simultaneously co-authoring the HTTP/1.1 specification and the URI standards at the IETF. He wasn't theorizing about the web from the outside. He was &lt;em&gt;building&lt;/em&gt; the web while he was writing about how the web should work. That's why REST has the authority it has — the person who defined the style was also one of the people defining the protocol it runs on.&lt;/p&gt;

&lt;p&gt;REST, per Fielding, is defined by six constraints. Five are mandatory, one is optional:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Client–Server&lt;/strong&gt; — separation of concerns. The client and the server evolve independently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stateless&lt;/strong&gt; — every request contains all the information needed to understand it. The server stores no client session state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cacheable&lt;/strong&gt; — responses must declare themselves cacheable or not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Uniform Interface&lt;/strong&gt; — this is the big one. We'll get to it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Layered System&lt;/strong&gt; — clients can't tell whether they're talking to the origin server or an intermediary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code on Demand (optional)&lt;/strong&gt; — the server can send executable code to extend the client.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That fourth one, the uniform interface, has four sub-constraints, and this is where the industry quietly stopped following the rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Identification of resources&lt;/li&gt;
&lt;li&gt;Manipulation of resources through representations&lt;/li&gt;
&lt;li&gt;Self-descriptive messages&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hypermedia as the engine of application state&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last sub-constraint has an acronym: &lt;strong&gt;HATEOAS&lt;/strong&gt;. It means that a REST API's responses should contain links — hypermedia — telling the client what it can do next. Like a webpage. You don't memorize URLs to navigate a website; you click links. A real REST API works the same way: the client starts at one entry point, gets back a response with links, and follows those links to navigate the application.&lt;/p&gt;

&lt;p&gt;Almost no API you have ever used works this way.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 2008 Rant That Aged Like Wine
&lt;/h2&gt;

&lt;p&gt;By 2008, "REST" had become an industry buzzword. Every company was launching a "REST API" — by which they meant they had endpoints that returned JSON. Fielding watched this happen for a few years and then snapped. The October 20, 2008 post titled "REST APIs must be hypertext-driven" is the foundational document of every "well actually, that's not really REST" argument that has happened on Twitter since.&lt;/p&gt;

&lt;p&gt;Some choice cuts:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;if the engine of application state (and hence the API) is not being driven by hypertext, then it cannot be RESTful and cannot be a REST API. Period.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In the comments, he doubled down:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A truly RESTful API looks like hypertext.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And the line that I think about the most, also from the comments:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;REST is software design on the scale of decades: every detail is intended to promote software longevity and independent evolution. Many of the constraints are directly opposed to short-term efficiency.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That last sentence is, I think, the real reason HATEOAS lost. Engineers under deadline pressure don't optimize for decades. They optimize for the next sprint. HATEOAS makes APIs more resilient over very long time horizons at the cost of immediate complexity. So we all just shipped Level 2.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Richardson Maturity Model
&lt;/h2&gt;

&lt;p&gt;A better way to think about REST in practice comes from Leonard Richardson, who presented the Richardson Maturity Model in 2008 — coincidentally the same year as Fielding's rant. Martin Fowler wrote it up on his blog and it stuck. It's the most useful diagnostic tool in the industry.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Level 0 — "The Swamp of POX."&lt;/strong&gt; You have one URL. Everything is a POST. The request body decides what happens. This is SOAP. This is &lt;code&gt;/api/endpoint&lt;/code&gt; taking &lt;code&gt;{ "action": "getUser", "id": 123 }&lt;/code&gt;. This is the bottom.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Level 1 — Resources.&lt;/strong&gt; You have distinct URIs for distinct things. &lt;code&gt;/users&lt;/code&gt;, &lt;code&gt;/orders/42&lt;/code&gt;. You might still POST everything, but at least the URLs identify resources.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Level 2 — HTTP Verbs.&lt;/strong&gt; You use GET for reading, POST for creating, PUT for replacing, PATCH for partial updates, DELETE for deleting. You return proper status codes — 200 for success, 404 for not found, 500 when you've broken. This is where Stripe lives. This is where GitHub lives. This is where every "REST API" you've used lives. This is what 99% of the industry calls "REST."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Level 3 — Hypermedia controls (HATEOAS).&lt;/strong&gt; Every response embeds links telling the client what actions are possible next. The client navigates the API by following links, not by constructing URLs from memory.&lt;/p&gt;

&lt;p&gt;Fielding's 2008 post is, basically, a protest that &lt;em&gt;nothing below Level 3 should be called REST&lt;/em&gt;. Richardson's model is more diplomatic — it gives you a ladder, lets you see where you are, and doesn't insist you climb all the way up.&lt;/p&gt;

&lt;p&gt;The industry stopped at Level 2 and renamed it REST. That's the whole story. Everything from here on out is just the engineering details of doing Level 2 well.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "REST API" Actually Means In 2026
&lt;/h2&gt;

&lt;p&gt;Let me give you the honest, working developer's definition. A "REST API" in 2026 is an API that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Uses HTTP as its transport&lt;/li&gt;
&lt;li&gt;Returns JSON (almost never XML anymore)&lt;/li&gt;
&lt;li&gt;Has resource-oriented URLs — nouns, not verbs (&lt;code&gt;/users/123&lt;/code&gt;, not &lt;code&gt;/getUser?id=123&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Uses HTTP verbs correctly (GET reads, POST creates, etc.)&lt;/li&gt;
&lt;li&gt;Returns standard HTTP status codes that aren't lying&lt;/li&gt;
&lt;li&gt;Is documented, ideally with OpenAPI&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's it. That's the definition that 99% of engineers carry in their heads and 99% of working APIs satisfy. It is, strictly speaking, Level 2 on the Richardson scale. It is, strictly speaking, not REST per Fielding. Nobody cares.&lt;/p&gt;

&lt;p&gt;The reason we still call it REST is that the alternative names are worse. "HTTP+JSON API" is accurate but clumsy. "REST-ish" is honest but apologetic. "Web API" is too broad. "RESTful" is a fig leaf — it signals "we know we're not quite REST but please give us partial credit." So we say REST, everyone knows what we mean, and we move on.&lt;/p&gt;

&lt;h2&gt;
  
  
  HTTP Verbs and the Idempotency Religion
&lt;/h2&gt;

&lt;p&gt;Here's the verb cheat sheet you actually need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GET&lt;/strong&gt; — read a resource. Idempotent. Safe. Cacheable. Don't put side effects here. Yes, I know your tracking pixel does. Don't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;POST&lt;/strong&gt; — create a resource, or "do a thing." Not idempotent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PUT&lt;/strong&gt; — replace a resource entirely. Idempotent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PATCH&lt;/strong&gt; — partially update a resource. Can be idempotent if you design it that way; often isn't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DELETE&lt;/strong&gt; — delete a resource. Idempotent.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The word "idempotent" gets thrown around like everyone agrees what it means, so let me state it cleanly: &lt;strong&gt;a request is idempotent if making it N times produces the same server state as making it once.&lt;/strong&gt; Deleting the user with ID 42 ten times leaves you with the same state as deleting them once — they're gone. So DELETE is idempotent. POSTing "create a new order" ten times creates ten orders. So POST is not idempotent.&lt;/p&gt;

&lt;p&gt;Why does this matter? Because networks are unreliable. Your client sends a request, the network drops the response, the client doesn't know if the request succeeded or not. If the request was idempotent, retrying is safe. If not, retrying might charge your customer twice.&lt;/p&gt;

&lt;p&gt;Stripe has the canonical solution to this and it's worth knowing by heart. They use an HTTP header called &lt;code&gt;Idempotency-Key&lt;/code&gt;. The client generates a unique key (a UUID, typically) and sends it with the request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://api.stripe.com/v1/charges &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-u&lt;/span&gt; sk_test_BQokikJOvBiI2HlWgH4olfQ2: &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Idempotency-Key: AGJ6FJMkGQIpHUTX"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2000 &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;currency&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;usd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server stores the result of the first request under that key for a window (24 hours for Stripe v1, 30 days for v2). If the same key comes in again, the server returns the cached result instead of charging the customer again. The client can retry as many times as it wants without fear of double-charging.&lt;/p&gt;

&lt;p&gt;Brandur Leach's "Designing robust and predictable APIs with idempotency" on the Stripe engineering blog is required reading if you're building anything that touches money. The pattern works for any non-idempotent operation, not just payments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Status Codes That Actually Matter
&lt;/h2&gt;

&lt;p&gt;There are sixty-something HTTP status codes. You need to know about fifteen of them. Here's the working set:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;200 OK&lt;/strong&gt; — Success, with a body. The default for GET, PUT, and PATCH responses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;201 Created&lt;/strong&gt; — A new resource was created. Should include a &lt;code&gt;Location&lt;/code&gt; header pointing to the new resource. Useful for POSTs that create something.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;204 No Content&lt;/strong&gt; — Success, intentionally empty body. The spec says you cannot send a body with a 204. The natural answer for DELETE and for PUTs that don't need to echo back the updated resource. People argue endlessly about whether DELETE should return 200 (with the deleted entity in the body) or 204 (with nothing). Both are defensible. Pick one and be consistent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;301 Moved Permanently&lt;/strong&gt; — Useful for URL versioning migrations. The browser caches this aggressively, so be careful.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;400 Bad Request&lt;/strong&gt; — The client sent garbage. Malformed JSON, missing required fields, that sort of thing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;401 Unauthorized&lt;/strong&gt; — You aren't authenticated. The spec calls this "Unauthorized" but it really means "Unauthenticated." Everyone gets this wrong because the name is bad.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;403 Forbidden&lt;/strong&gt; — You ARE authenticated, but you don't have permission to do this thing. The "you can't sit with us" of HTTP.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;404 Not Found&lt;/strong&gt; — The resource doesn't exist. GitHub famously returns 404 for private repositories you don't have access to, instead of 403, to avoid leaking information about what exists. That's a defensible third path between 403 and 404.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;409 Conflict&lt;/strong&gt; — State conflict. Duplicate keys, version mismatches, "this email is already taken."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;422 Unprocessable Content&lt;/strong&gt; — Your JSON parsed fine, but it's semantically wrong. Validation failures, basically. This started as a WebDAV-specific code and got repurposed by the industry because there was a gap between "syntactically broken" (400) and "logically wrong" (which previously had no good code).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;429 Too Many Requests&lt;/strong&gt; — Rate limited. Defined by RFC 6585 in April 2012. You should include a &lt;code&gt;Retry-After&lt;/code&gt; header telling the client how long to wait.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;500 Internal Server Error&lt;/strong&gt; — We broke.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;502 Bad Gateway&lt;/strong&gt; — Our upstream broke.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;503 Service Unavailable&lt;/strong&gt; — We're down or overloaded. Include &lt;code&gt;Retry-After&lt;/code&gt; if you know when you'll be back.&lt;/p&gt;

&lt;p&gt;The 401 vs 403 confusion is something I've watched senior engineers get wrong, repeatedly, on production systems. The mnemonic: 401 means "who are you," 403 means "I know who you are, no."&lt;/p&gt;

&lt;h2&gt;
  
  
  URL Design: The Trailing Slash and Other Religious Debates
&lt;/h2&gt;

&lt;p&gt;Resource-oriented URLs use nouns, not verbs. &lt;code&gt;GET /users/123/posts&lt;/code&gt; is good. &lt;code&gt;GET /getUserPosts?id=123&lt;/code&gt; is bad. This is one of the few things the entire industry agrees on.&lt;/p&gt;

&lt;p&gt;Use plurals for collections. &lt;code&gt;/users&lt;/code&gt;, not &lt;code&gt;/user&lt;/code&gt;. The Microsoft API guidelines, Google's API guidelines, Zalando's guidelines, and basically everyone else agree. Yes, "/people" is more grammatically correct than "/persons," but consistency beats correctness — pick the rule "always plural" and apply it uniformly.&lt;/p&gt;

&lt;p&gt;For nested resources, prefer two-level nesting at most. &lt;code&gt;/users/123/posts&lt;/code&gt; is fine. &lt;code&gt;/users/123/posts/456/comments/789/reactions/abc&lt;/code&gt; is hardcoding your data model into your URLs in a way you will regret. The pragmatic alternative: &lt;code&gt;/comments/789/reactions&lt;/code&gt; or &lt;code&gt;/reactions?comment_id=789&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Now the trailing slash. &lt;code&gt;/users&lt;/code&gt; versus &lt;code&gt;/users/&lt;/code&gt;. People have died on this hill. John Sheehan of Runscope said it best, and the API Evangelist quoted him: save the byte, drop the slash, and 301-redirect from the slashed version to the non-slashed version if anyone hits it. That's the pragmatic answer. By the strict reading of Fielding's dissertation, URIs are character-by-character identifiers, which means &lt;code&gt;/users&lt;/code&gt; and &lt;code&gt;/users/&lt;/code&gt; are &lt;em&gt;different resources&lt;/em&gt; unless the server normalizes them. In practice, your framework probably normalizes them and nobody on your team will ever notice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Versioning Is A Mess
&lt;/h2&gt;

&lt;p&gt;There is no good answer here. There are three answers, all bad in different ways.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;URL versioning&lt;/strong&gt; (&lt;code&gt;/v1/users&lt;/code&gt;, &lt;code&gt;/v2/users&lt;/code&gt;) is what Stripe does, what most pragmatic teams do, and what I'd recommend for new APIs. It's discoverable. It's debuggable in a browser tab. It's obvious in logs. The downside is that the URL of a resource is supposed to be the identity of the resource, and &lt;code&gt;/v1/users/42&lt;/code&gt; and &lt;code&gt;/v2/users/42&lt;/code&gt; are arguably the same user with different representations, which is what content negotiation is for. But almost nobody cares about this in practice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Header versioning&lt;/strong&gt; (&lt;code&gt;X-API-Version: 2&lt;/code&gt;) keeps URLs clean and lets you handle versions via middleware. GitHub does this with calendar-based versioning — their header is &lt;code&gt;X-GitHub-Api-Version: 2022-11-28&lt;/code&gt;, and in March 2026 they released their first calendar version with breaking changes, &lt;code&gt;2026-03-10&lt;/code&gt;. Stripe also uses dated versions, like &lt;code&gt;2025-12-15.clover&lt;/code&gt;. The calendar-based approach has the nice property that "v2" doesn't have to mean "we changed everything" — each release is a small, dated diff from the previous.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Content negotiation&lt;/strong&gt; (&lt;code&gt;Accept: application/vnd.myapi.v2+json&lt;/code&gt;) is the "by the book" REST answer. Almost nobody uses it because debugging an API in a browser tab is impossible when you have to set custom Accept headers. It exists. You can ignore it.&lt;/p&gt;

&lt;p&gt;If I'm building a new API in 2026 and I have no constraints, I pick calendar-based versioning in a header, like GitHub. If I'm building something simpler and I want minimum friction, I pick &lt;code&gt;/v1/&lt;/code&gt; in the URL and live with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pagination Has No Standard
&lt;/h2&gt;

&lt;p&gt;There is no RFC for pagination. There is no IETF spec. There is no industry agreement. Every API does it differently. This is genuinely embarrassing and I don't expect it to be fixed in my career.&lt;/p&gt;

&lt;p&gt;The four approaches in the wild:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Offset/limit&lt;/strong&gt; (&lt;code&gt;?offset=20&amp;amp;limit=10&lt;/code&gt;). Simple. You can jump to page 47. But if data shifts under you — someone inserts or deletes a row — you'll see duplicates or skips. Performance falls off a cliff at high offsets because the database has to count past all the previous rows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cursor-based&lt;/strong&gt; (&lt;code&gt;?cursor=eyJpZCI6MTAwfQ&amp;amp;limit=10&lt;/code&gt;). The cursor is an opaque base64-encoded token that the server understands. Stable under inserts. You can't jump to an arbitrary page, but that's usually fine — infinite scroll doesn't need page jumping. Stripe uses this. Slack uses this. GitHub uses this for several endpoints. This is the right answer for most APIs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keyset&lt;/strong&gt; (&lt;code&gt;?after_id=100&lt;/code&gt;). A non-opaque cursor based on an indexed column. Slightly less flexible than opaque cursors but easier to debug.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Page-based&lt;/strong&gt; (&lt;code&gt;?page=3&amp;amp;per_page=10&lt;/code&gt;). Cosmetic offset/limit. Same problems.&lt;/p&gt;

&lt;p&gt;Zalando's API guidelines recommend cursors for anything more than a few hundred items. I agree. If your collection might exceed a thousand items, use cursors. If it's always small (a user's recent orders, say), offset/limit is fine.&lt;/p&gt;

&lt;p&gt;There's a beautiful idea, almost never implemented in practice, of putting pagination links in HTTP headers via RFC 8288 (the Web Linking spec):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Link: &amp;lt;https://api.example.com/items?page=3&amp;gt;; rel="next",
      &amp;lt;https://api.example.com/items?page=10&amp;gt;; rel="last"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub has done this since their V3 days. It's elegant — the URLs are right there, the client doesn't have to construct them. Nobody else does it because most clients don't parse &lt;code&gt;Link&lt;/code&gt; headers automatically and most developers don't think to look.&lt;/p&gt;

&lt;h2&gt;
  
  
  Error Responses: RFC 7807 Exists And Almost Nobody Uses It
&lt;/h2&gt;

&lt;p&gt;In March 2016, Mark Nottingham (of Akamai, also known for the HTTP cache header &lt;code&gt;Cache-Control&lt;/code&gt;) and Erik Wilde published RFC 7807, "Problem Details for HTTP APIs." In July 2023 it was updated as RFC 9457. The media type is &lt;code&gt;application/problem+json&lt;/code&gt;. Here's what an RFC-7807-compliant error response looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt; &lt;span class="m"&gt;404&lt;/span&gt; &lt;span class="ne"&gt;Not Found&lt;/span&gt;
&lt;span class="na"&gt;Content-Type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;application/problem+json&lt;/span&gt;

&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://api.example.com/problems/user-not-found"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"User Not Found"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"detail"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"User with ID 12345 does not exist"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"instance"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/users/12345"&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;Five fields. &lt;code&gt;type&lt;/code&gt; is a URI identifying the &lt;em&gt;kind&lt;/em&gt; of problem (which the client can recognize and handle programmatically). &lt;code&gt;title&lt;/code&gt; is a short human-readable name. &lt;code&gt;status&lt;/code&gt; echoes the HTTP status. &lt;code&gt;detail&lt;/code&gt; is a longer human-readable explanation. &lt;code&gt;instance&lt;/code&gt; is the URI of &lt;em&gt;this specific occurrence&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;It's elegant. It's standardized. It's been around for almost a decade. ASP.NET Core ships it by default. Spring Boot has built-in support. And almost nobody outside those two ecosystems uses it. Instead, every API invents its own error format:&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="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Common&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;"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;"User not found"&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="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Also&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;common&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;"errors"&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;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USER_NOT_FOUND"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Why&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;did&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;anyone&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;do&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;this&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;"success"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"msg"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;The&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;cardinal&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;sin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;—&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;OK&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;with&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;this&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;body&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;"ok"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&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;"Something went wrong"&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 last one — returning HTTP 200 with an error in the body — is the worst pattern in REST and Facebook's Graph API did it for years. Every middleware, every load balancer, every monitoring tool understands HTTP status codes. When you return 200 for an error, you've thrown away your ability to alert on errors, retry intelligently, or cache responses. You've made every client write a custom error handler that ignores the status code and reads the body. Please don't.&lt;/p&gt;

&lt;p&gt;If you're building a new API, use RFC 7807 / 9457. The format is good. The standard exists. You don't have to invent error handling from scratch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Authentication: The Real Cheat Sheet
&lt;/h2&gt;

&lt;p&gt;You have basically five options. Pick the right one for your situation:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;API keys&lt;/strong&gt; are the simplest thing that works. Send &lt;code&gt;Authorization: Bearer sk_live_abc123&lt;/code&gt; in the header. Stripe does this. It works. The downside is that the key has all-or-nothing access (no scopes), and if it leaks, you have a problem. Don't put keys in URLs — they end up in server logs, browser history, and analytics tools. Header only.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Basic Auth&lt;/strong&gt; (&lt;code&gt;Authorization: Basic &amp;lt;base64(user:pass)&amp;gt;&lt;/code&gt;) is older than the web you grew up with. It's still everywhere, especially for internal services and webhook signing. It's fine. It's not great. Use HTTPS or die.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OAuth 2.0&lt;/strong&gt; is the right answer for any API that third parties will integrate with. The flows that actually matter in 2026:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Authorization Code with PKCE&lt;/em&gt; — the default for SPAs and mobile apps. PKCE (Proof Key for Code Exchange, RFC 7636) is now effectively mandatory under the OAuth 2.1 draft and RFC 9700's security best practices. Even Anthropic's Model Context Protocol adopted OAuth 2.1 for tool authorization.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Client Credentials&lt;/em&gt; — for server-to-server, where there's no user.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Refresh Token Rotation&lt;/em&gt; — for keeping SPAs logged in despite browser cookie restrictions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;JWT bearer tokens&lt;/strong&gt; are a love/hate relationship. They're self-contained (no DB lookup needed to validate), but you can't revoke them without building a denylist, which defeats the point of having them be self-contained. The right framing: JWTs are great for short-lived access tokens (15 minutes), terrible as long-lived session tokens. If your JWT lives for 30 days, you've reinvented session cookies, badly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Session cookies&lt;/strong&gt; are still completely valid in 2026, often the right answer for first-party web apps, and unjustly maligned by everyone who reaches for JWTs out of habit. They have a security story (HttpOnly, Secure, SameSite) that's been honed for thirty years. Use them when they fit.&lt;/p&gt;

&lt;h2&gt;
  
  
  PUT vs PATCH: The Confusing One
&lt;/h2&gt;

&lt;p&gt;PUT replaces the resource entirely. If the resource was &lt;code&gt;{ name: "Jane", email: "j@example.com", role: "admin" }&lt;/code&gt; and you PUT &lt;code&gt;{ name: "Jane Doe" }&lt;/code&gt;, the resource is now &lt;code&gt;{ name: "Jane Doe" }&lt;/code&gt; — the email and role are gone, because PUT replaces.&lt;/p&gt;

&lt;p&gt;PATCH applies a partial update. If you PATCH &lt;code&gt;{ name: "Jane Doe" }&lt;/code&gt; to the same resource, the result is &lt;code&gt;{ name: "Jane Doe", email: "j@example.com", role: "admin" }&lt;/code&gt; — only the name changed.&lt;/p&gt;

&lt;p&gt;That's the conceptual difference. The complication is that there are two competing formats for PATCH, and APIs that don't tell you which one they use will make you cry.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;JSON Patch&lt;/strong&gt; (RFC 6902), media type &lt;code&gt;application/json-patch+json&lt;/code&gt;, is an array of operations:&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"op"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"replace"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"new@example.com"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"op"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"remove"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/nickname"&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;It's atomic, expressive, and lets you address individual array indices. It's also unreadable. Nobody writes these by hand.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;JSON Merge Patch&lt;/strong&gt; (RFC 7396), media type &lt;code&gt;application/merge-patch+json&lt;/code&gt;, looks like the object you want, with &lt;code&gt;null&lt;/code&gt; meaning "delete":&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;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"new@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"nickname"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&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;It's intuitive. You can write it by hand. But you cannot use it to set a field to literal &lt;code&gt;null&lt;/code&gt;, and you cannot update individual array indices — you can only replace the whole array.&lt;/p&gt;

&lt;p&gt;GitHub uses Merge Patch–style. Kubernetes supports three different patch types — JSON Patch, Merge Patch, and a custom Strategic Merge Patch — because of course Kubernetes does. If you're building a new API and don't have strong opinions, use Merge Patch. It's simpler, and the limitations rarely matter in practice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rate Limiting and Caching
&lt;/h2&gt;

&lt;p&gt;Rate limiting in HTTP is &lt;strong&gt;429 Too Many Requests&lt;/strong&gt; plus the &lt;code&gt;Retry-After&lt;/code&gt; header. The RFC 6585 definition is straightforward, but the &lt;em&gt;headers&lt;/em&gt; used to communicate quota state are not standardized. The convention everyone uses (without an RFC) is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 47
X-RateLimit-Reset: 1714232400
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's an IETF draft (&lt;code&gt;draft-ietf-httpapi-ratelimit-headers&lt;/code&gt;) that proposes the same headers without the &lt;code&gt;X-&lt;/code&gt; prefix, but it's been a draft for years. If you implement rate limiting, set both &lt;code&gt;Retry-After&lt;/code&gt; (which is standardized) and the X-RateLimit headers (which are conventional). Your clients will thank you.&lt;/p&gt;

&lt;p&gt;The right retry strategy on the client side is exponential backoff with jitter: &lt;code&gt;delay = min(cap, base * 2^attempt) + random(0, jitter)&lt;/code&gt;. The jitter matters more than you'd think — without it, all your clients retry at exactly the same moment after a 429 storm, and you get a thundering herd.&lt;/p&gt;

&lt;p&gt;Caching is &lt;code&gt;Cache-Control&lt;/code&gt;, &lt;code&gt;ETag&lt;/code&gt;, &lt;code&gt;Last-Modified&lt;/code&gt;, and the conditional request headers (&lt;code&gt;If-None-Match&lt;/code&gt;, &lt;code&gt;If-Modified-Since&lt;/code&gt;). The honest truth is that almost nobody uses HTTP caching properly for JSON APIs. The reason is that in a modern stack, you have a CDN in front (handling its own caching), an application cache in Redis (handling server-side), and a client-side cache in the browser (React Query, SWR, Apollo, TanStack Query). HTTP-level caching feels like a fourth layer that doesn't pay off, and most APIs ship &lt;code&gt;Cache-Control: no-store&lt;/code&gt; for everything authenticated.&lt;/p&gt;

&lt;p&gt;It's a missed opportunity but I understand why it happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Competing Paradigms (Brief Tour)
&lt;/h2&gt;

&lt;p&gt;REST isn't the only game in town. Quick survey of the alternatives, why they exist, and why REST is still the default:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GraphQL&lt;/strong&gt; (Facebook, 2015) solves over-fetching and under-fetching. You ask for exactly the fields you want, you get exactly those fields. Great for complex frontends with diverse data needs. Terrible operational story: caching is hard, query complexity attacks are a real concern, N+1 resolver storms unless you use DataLoader, and the bundle weight of Apollo Client is non-trivial. Use it when your client teams genuinely need flexible queries. Skip it when you're just doing CRUD.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;gRPC&lt;/strong&gt; (Google, 2015) is Protocol Buffers over HTTP/2. Binary, fast, schema-first, with code generation for every major language. The current stable version as of early 2026 is gRPC 1.80.0 (released March 30, 2026). Benchmarks vary but you can expect roughly an order of magnitude smaller payloads than equivalent JSON, and meaningful latency wins for chatty internal services. The catch: browsers can't speak it natively, so for browser-facing APIs you need grpc-web plus a proxy. Use it for east-west service-to-service communication, especially at scale. Don't bother for public APIs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;tRPC&lt;/strong&gt; is TypeScript-only RPC where your TypeScript types &lt;em&gt;are&lt;/em&gt; the contract. No schema, no codegen, no OpenAPI. Tiny bundle (around 5KB). If you're shipping a TypeScript monorepo with a Next.js frontend and a Node backend, it eliminates a whole layer of contract management. Useless outside the TypeScript ecosystem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Server-Sent Events&lt;/strong&gt; (SSE) is one-way streaming from server to client over plain HTTP. The browser supports it natively through &lt;code&gt;EventSource&lt;/code&gt;. This is what OpenAI and Anthropic use to stream AI tokens. It's underrated. If you need streaming and you don't need bidirectional, use SSE before reaching for WebSockets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WebSockets&lt;/strong&gt; are full-duplex, separate protocol after an HTTP upgrade. For chat, multiplayer games, real-time collaboration. Overkill for most uses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AsyncAPI&lt;/strong&gt; is the specification for documenting event-driven APIs (Kafka, MQTT, AMQP, WebSockets, etc.) — the equivalent of OpenAPI but for messages instead of requests. Current version is 3.1.0, released January 31, 2026.&lt;/p&gt;

&lt;p&gt;Why is REST still the default in 2026 despite all of this? Because it's the lowest common denominator. Every language has an HTTP client. Every developer can &lt;code&gt;curl&lt;/code&gt; your API. Every API gateway, CDN, monitoring tool, and WAF understands HTTP+JSON for free. The cost of "more correct" alternatives is real and often not worth paying for typical CRUD workloads.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 2026 Angle: AI Agents Read Your API
&lt;/h2&gt;

&lt;p&gt;Here's the new thing in 2026 that wasn't a thing five years ago: LLM agents consume APIs. They read the OpenAPI spec, they figure out the tools available, they make the calls themselves. Google's Agent Development Kit has &lt;code&gt;OpenAPIToolset&lt;/code&gt;. The FastMCP library has &lt;code&gt;FastMCP.from_openapi()&lt;/code&gt;. Anthropic's Model Context Protocol standardizes the tool-discovery layer that bridges LLMs and APIs.&lt;/p&gt;

&lt;p&gt;OpenAPI 3.2.0 (dated September 19, 2025) added native streaming media types — SSE, JSON Lines, multipart feeds — exactly because AI workloads need them. The OpenAPI Initiative has explicitly oriented the spec toward AI consumption.&lt;/p&gt;

&lt;p&gt;What this means for API designers: &lt;strong&gt;your API documentation is now an LLM-readability concern.&lt;/strong&gt; Vague operation IDs, missing descriptions, chatty endpoints with poor schemas — these used to be human-only problems. Now they cause AI agents to hallucinate, misuse your API, or fail to use it at all. Good operation names (&lt;code&gt;createUser&lt;/code&gt;, not &lt;code&gt;endpoint_v2_user_create_new&lt;/code&gt;), thorough descriptions, well-typed request and response schemas, and clear error responses are no longer nice-to-have. They are the difference between an LLM agent that can use your API and one that can't.&lt;/p&gt;

&lt;p&gt;If you maintain a public API, generating high-quality OpenAPI 3.2 specs is probably the highest-leverage investment you can make in 2026. The audience for your docs now includes machines.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Mistakes I Have Watched Get Made In Production
&lt;/h2&gt;

&lt;p&gt;Permit me a personal list. Every one of these I have either shipped myself or watched a colleague ship:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;POST for everything.&lt;/strong&gt; I have seen &lt;code&gt;POST /api/getUser&lt;/code&gt; in production. At three different unicorns. Yes, in production. There is no excuse. Use GET for reads.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;HTTP 200 with &lt;code&gt;{ "success": false, "error": "..." }&lt;/code&gt;.&lt;/strong&gt; Throws away every benefit of HTTP status codes. Breaks monitoring, breaks caching, breaks retries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inconsistent casing in the same response.&lt;/strong&gt; &lt;code&gt;{ "userId": 1, "user_name": "alice" }&lt;/code&gt;. This happens. Usually because the field was added by a different team that hadn't read the style guide.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verbs in URLs.&lt;/strong&gt; &lt;code&gt;/getUser&lt;/code&gt;, &lt;code&gt;/createOrder&lt;/code&gt;, &lt;code&gt;/deleteAccount&lt;/code&gt;. Verbs go in the HTTP method. That is literally what HTTP methods are for. If your URL has &lt;code&gt;/get&lt;/code&gt; or &lt;code&gt;/create&lt;/code&gt; or &lt;code&gt;/delete&lt;/code&gt; in it, you're doing RPC and you should at least be honest about it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No pagination on collection endpoints.&lt;/strong&gt; Until one customer has 50,000 invoices, and then your API times out, and then you spend a sprint adding pagination retrofit. Add pagination from day one, even if you don't think you'll need it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ignoring idempotency for create operations.&lt;/strong&gt; Network blip, double-charged customer, angry email at 2am. Use Stripe's pattern. Implement idempotency keys.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Massive nested URLs.&lt;/strong&gt; &lt;code&gt;/users/:id/orders/:orderId/items/:itemId/notes/:noteId&lt;/code&gt;. You've now hardcoded your data model into your URL structure. When the data model changes, every URL is a breaking change. Flatten where possible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not using HTTP cache headers, even when you could.&lt;/strong&gt; Public, read-heavy endpoints can absolutely use proper &lt;code&gt;Cache-Control&lt;/code&gt; and &lt;code&gt;ETag&lt;/code&gt; responses. The CDN will love you. Your origin will love you. Your bill will love you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Versioning policy of "we'll figure it out later."&lt;/strong&gt; Then later arrives, you have ten thousand customers depending on v1, and you can't change anything without breaking the world. Adopt calendar versioning from day one, or commit to URL versioning with a clear deprecation policy. Either is fine. Both beat nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Two Reference APIs You Should Read
&lt;/h2&gt;

&lt;p&gt;If you want to learn what good REST API design looks like, there are two public APIs to study.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stripe&lt;/strong&gt; is the canonical "good REST API." Their own docs put it plainly: the Stripe API is organized around REST, with predictable resource-oriented URLs, form-encoded bodies, JSON responses, standard HTTP status codes, and standard HTTP authentication. The little details matter — typed prefixes for object IDs (&lt;code&gt;cus_&lt;/code&gt; for customers, &lt;code&gt;pi_&lt;/code&gt; for payment intents, &lt;code&gt;in_&lt;/code&gt; for invoices), the &lt;code&gt;Idempotency-Key&lt;/code&gt; header pattern, the &lt;code&gt;expand[]&lt;/code&gt; query parameter for embedding related resources (avoiding N+1 round trips), the dated version strings. Read the docs. Steal liberally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub&lt;/strong&gt; is the canonical "evolved over a decade and shows it." The API has been alive since around 2008. It's been through multiple versioning schemes, multiple authentication paradigms, multiple pagination styles. It's a museum of every fashion in REST API design since the late 2000s, all coexisting in one product, none of them quite consistent with each other. Read it as a cautionary tale about what happens when you don't decide on conventions early and stick to them.&lt;/p&gt;

&lt;p&gt;Stripe and GitHub between them will teach you 90% of what you need to know about REST API design in practice. Read them. Compare them. Notice what Stripe got right that GitHub didn't. Then go design your own API with that knowledge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tooling You Should Actually Be Using
&lt;/h2&gt;

&lt;p&gt;The 2026 tooling landscape:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For API documentation:&lt;/strong&gt; OpenAPI 3.2.0. Generate it from your code (FastAPI, tRPC's OpenAPI plugin, NestJS, Spring) or write it by hand and use it to generate code (openapi-generator, Stainless, Speakeasy, Fern, Kiota). Either direction works. Pick one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For API exploration:&lt;/strong&gt; Postman is still #1 but increasingly hated for cloud-sync requirements and pricing. Insomnia got acquired by Kong and lost mindshare. &lt;strong&gt;Bruno&lt;/strong&gt; is the breakout 2024 alternative — open-source, MIT-licensed, stores collections as plain-text files on disk for proper Git workflows, no cloud, no login, no telemetry. As of early 2026 it has around 41,000 GitHub stars and is what most developers I know have switched to. Hoppscotch is the lightweight browser-first option.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For API mocking:&lt;/strong&gt; MSW (Mock Service Worker) for frontend tests. Prism for mock servers from OpenAPI specs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For API linting:&lt;/strong&gt; Spectral, from Stoplight. Catches inconsistencies, enforces house style. Put it in CI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For the command line:&lt;/strong&gt; &lt;code&gt;curl&lt;/code&gt; for everything serious. &lt;code&gt;HTTPie&lt;/code&gt; if you want commands to read like English (&lt;code&gt;http POST api.example.com/users name=alice&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;That's it. Five tools cover 99% of API workflows. Don't overthink your tooling.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Closing Argument
&lt;/h2&gt;

&lt;p&gt;I've written this article in a slightly grumpy mood because I think the gap between Fielding's REST and what we call REST is one of the most interesting stories in modern software, and most articles on the topic skip it entirely. The honest version is that we — the industry — borrowed a term, simplified it, and built something useful but not what the inventor meant. Whether that's fine or a sin against software longevity depends on how much you take Fielding's "decades-scale design" seriously.&lt;/p&gt;

&lt;p&gt;Personally, I think it's fine. HATEOAS was a beautiful idea that didn't survive contact with the average engineering team's quarterly priorities. The Level 2 industry standard — HTTP+JSON with proper verbs and status codes — has shipped countless functional systems that work well enough for their lifespans. The cost of doing real REST was higher than the benefit for most teams. So we did the cheaper thing and called it REST anyway. The web didn't end.&lt;/p&gt;

&lt;p&gt;But the details still matter. Use the right HTTP verb. Return honest status codes. Don't return 200 with an error in the body. Use cursor-based pagination for large collections. Implement RFC 7807 problem details for errors. Pick a versioning strategy and commit to it. Add idempotency keys for non-idempotent writes. Generate good OpenAPI specs because the LLMs are reading them now too. These are the things that separate an API your customers love from one they have to write a wrapper library around.&lt;/p&gt;

&lt;p&gt;Roy Fielding probably still doesn't think any of this is REST. But if we get the daily details right, our APIs are going to outlive us anyway, which was the point.&lt;/p&gt;

&lt;p&gt;Now go fix the one in your repo that returns 200 with &lt;code&gt;{ "success": false }&lt;/code&gt;. You know which one.&lt;/p&gt;

</description>
      <category>rest</category>
      <category>restapi</category>
      <category>ai</category>
      <category>programming</category>
    </item>
    <item>
      <title>?</title>
      <dc:creator>DevUnionX</dc:creator>
      <pubDate>Sat, 09 May 2026 11:48:28 +0000</pubDate>
      <link>https://dev.to/devunionx/-4k5</link>
      <guid>https://dev.to/devunionx/-4k5</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/devunionx/5-things-ai-cant-do-even-in-cypress-dn1" class="crayons-story__hidden-navigation-link"&gt;5 Things AI Can't Do, Even in Cypress&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/devunionx" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3180316%2F804c7ae5-1a93-4c38-b9ec-023a59a621a8.jpg" alt="devunionx profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/devunionx" class="crayons-story__secondary fw-medium m:hidden"&gt;
              DevUnionX
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                DevUnionX
                
              
              &lt;div id="story-author-preview-content-3639909" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/devunionx" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3180316%2F804c7ae5-1a93-4c38-b9ec-023a59a621a8.jpg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;DevUnionX&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/devunionx/5-things-ai-cant-do-even-in-cypress-dn1" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;May 9&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/devunionx/5-things-ai-cant-do-even-in-cypress-dn1" id="article-link-3639909"&gt;
          5 Things AI Can't Do, Even in Cypress
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/test"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;test&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/react"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;react&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/webdev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;webdev&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/programming"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;programming&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/devunionx/5-things-ai-cant-do-even-in-cypress-dn1" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/exploding-head-daceb38d627e6ae9b730f36a1e390fca556a4289d5a41abb2c35068ad3e2c4b5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;6&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/devunionx/5-things-ai-cant-do-even-in-cypress-dn1#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            22 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
    </item>
    <item>
      <title>5 Things AI Can't Do, Even in Cypress</title>
      <dc:creator>DevUnionX</dc:creator>
      <pubDate>Sat, 09 May 2026 11:47:59 +0000</pubDate>
      <link>https://dev.to/devunionx/5-things-ai-cant-do-even-in-cypress-dn1</link>
      <guid>https://dev.to/devunionx/5-things-ai-cant-do-even-in-cypress-dn1</guid>
      <description>&lt;h2&gt;
  
  
  I Wrote 800 Cypress Tests. Then Playwright Came Out. Here's the Honest Story.
&lt;/h2&gt;

&lt;p&gt;The first Cypress test I ever wrote was a login flow for a SaaS dashboard sometime in late 2019. I had spent the previous week wrestling with Selenium and a flaky Chromedriver, and a colleague kept telling me to "just try Cypress, it's different." I installed it, ran &lt;code&gt;npx cypress open&lt;/code&gt;, and a window popped up showing my actual application running next to a list of commands, and I could click any command in the sidebar and watch the DOM rewind to that exact moment.&lt;/p&gt;

&lt;p&gt;I think I said "oh" out loud. The kind of "oh" that means a tool just made a problem you'd been brute-forcing for years go away.&lt;/p&gt;

&lt;p&gt;For the next four years, Cypress was the tool. Not "a tool we considered" the tool. We had eight hundred tests. We had a Cypress Cloud subscription. We had custom commands for everything. We had a wiki page called "How to debug a flaky Cypress test" that was longer than most onboarding documents. I gave talks about Cypress at meetups. I argued for Cypress in code review. I was a Cypress person.&lt;/p&gt;

&lt;p&gt;And then, one Tuesday in mid-2024, I sat down to write a test for a feature that opened a popup window, and I remembered for what felt like the hundredth time that Cypress can't open a second tab. And I looked at the Playwright docs that I had been deliberately avoiding for two years, because deep down I knew what I would find. And I found it.&lt;/p&gt;

&lt;p&gt;This article is the long version of what I found. It's also a love letter, because Cypress is genuinely a brilliant piece of software and the philosophy behind it changed how I think about testing. But it's an honest love letter, which means it has to acknowledge that the world has moved. As of April 2026, Playwright has roughly seven times Cypress's weekly npm downloads. The crossover happened in June 2024. There are real reasons that happened, and there are also real reasons Cypress is still installed on millions of machines and shipping a new release every two weeks.&lt;/p&gt;

&lt;p&gt;If you're picking a testing tool today, you need both halves of the story. Get coffee. We're going to walk through what Cypress actually is, what makes it special, what it can't do, and where it sits in 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Cypress Actually Is (Architecturally, Which Is The Whole Story)
&lt;/h2&gt;

&lt;p&gt;Most articles about Cypress start with "it's an end-to-end testing framework" and move on. That sentence is correct and useless. The thing you have to understand about Cypress the thing that explains every strength and every weakness, every pleasant surprise and every infuriating limitation is &lt;strong&gt;where the test code runs&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In Selenium, in Playwright, in basically every other browser-based testing tool, your test code runs in a Node.js process, outside the browser. The test process talks to the browser through a protocol WebDriver in Selenium's case, Chrome DevTools Protocol in Playwright's. When you write &lt;code&gt;await page.click('button')&lt;/code&gt;, that's a Node-side instruction that travels over a wire, gets executed inside the browser, and reports back.&lt;/p&gt;

&lt;p&gt;Cypress doesn't do this. Cypress runs your test code &lt;strong&gt;inside the browser&lt;/strong&gt;, in the same JavaScript event loop as the application under test. There's a Node.js process that handles file watching, network proxying, and CI orchestration. But the actual &lt;code&gt;cy.get('.button').click()&lt;/code&gt; you wrote runs in an iframe that's a sibling of your app, in the same tab, with full direct access to the DOM, the window object, and every JavaScript runtime detail.&lt;/p&gt;

&lt;p&gt;This decision is the single most important thing about Cypress. It's why Cypress feels magical when it works and frustrating when it doesn't.&lt;/p&gt;

&lt;p&gt;Because the test code lives in the browser, Cypress can do things other tools can't easily do. It can synchronously inspect any DOM node. It can stub &lt;code&gt;window.fetch&lt;/code&gt; at the source. It can take a complete snapshot of the DOM after every command and let you scrub through them like a video. It can show you the application running while the tests run, with a sidebar of commands you can click to see exactly what the page looked like at that moment. The "time-travel debugger" everyone raves about isn't a feature so much as a free side effect of where the code lives.&lt;/p&gt;

&lt;p&gt;But the same architectural choice forecloses certain things permanently. Browsers are designed so that code in one tab cannot control another tab. So Cypress, by definition, cannot test multi-tab flows there's no second tab for it to reach into. The same-origin policy, which is fundamental to web security, means Cypress can't navigate to a different origin and keep running, because that would be a sandbox violation. (They've worked around this with &lt;code&gt;cy.origin()&lt;/code&gt;, but the workaround has its own limits.) Real Safari support is hard, because Safari's WebKit doesn't expose the same hooks Chromium does for in-browser test injection. Parallelism is hard, because each browser instance needs its own coordinator and the natural parallelism of a Node process spawning workers doesn't apply.&lt;/p&gt;

&lt;p&gt;If you keep this architectural difference in mind, every Cypress strength and every Cypress limitation makes immediate sense. It's not arbitrary product design. It's the consequence of a single decision made in 2014 and lived with ever since.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Cypress Got Big
&lt;/h2&gt;

&lt;p&gt;Brian Mann started Cypress around 2014 because he was tired of writing brittle Selenium tests for his own team. The first commit landed on June 5, 2014. Public beta was October 2017. Commercial launch came in October 2018. By 2019, every JavaScript developer I knew had at least heard of it, and most had tried it.&lt;/p&gt;

&lt;p&gt;The reason it caught on was not architectural cleverness most users didn't and don't care how it works inside. The reason was that &lt;strong&gt;writing the first test was joyful&lt;/strong&gt;. You ran &lt;code&gt;cypress open&lt;/code&gt; and the runner showed you your app and the test side by side. You could see the test run. You could click any command in the log to time-travel. When something failed, the runner showed you exactly which element wasn't found, with a screenshot of the page at the moment of failure. There were no Selenium grids to set up, no Chromedriver versions to manage, no flakiness from "stale element reference" errors. Tests just worked, mostly, and when they didn't, you could see why.&lt;/p&gt;

&lt;p&gt;Compared to Selenium which had been the only serious option for ten years this was a revelation. Compared to the other test frameworks that existed at the time (Nightwatch, WebdriverIO, TestCafe), Cypress just felt nicer. The docs were good. The error messages were good. The DX was, frankly, an order of magnitude better than what came before.&lt;/p&gt;

&lt;p&gt;By 2021, Cypress had been adopted by Slack, Disney, the NBA, Netflix, Shopify, LEGO, NASA, GitHub, Vercel, and Netlify (their own marketing materials, which is to say: don't quote this as a current state of affairs, since several of those teams have publicly or quietly migrated since). The company had raised $54M across a 2019 Series A and a 2020 Series B. They launched Cypress Dashboard (later renamed Cypress Cloud) as the paid product. Things were, by every metric, going great.&lt;/p&gt;

&lt;p&gt;Then Microsoft launched Playwright in 2020.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Anatomy of a Cypress Test
&lt;/h2&gt;

&lt;p&gt;Before we get to the comparison, let's talk about what writing Cypress tests actually feels like, because this is the part that people either fall in love with or get frustrated by within twenty minutes.&lt;/p&gt;

&lt;p&gt;A typical Cypress test looks like this:&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="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;login flow&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;beforeEach&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;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;visit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/login&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;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;logs in with valid credentials&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[data-cy=email]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jane@example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[data-cy=password]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;correct-password&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[data-cy=submit]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;should&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;include&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/dashboard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Welcome back, Jane&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;should&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;be.visible&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;There are several things going on here that look like JavaScript but aren't.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;cy.get&lt;/code&gt; is not a function call that returns an element. It's a command that gets queued. When you write &lt;code&gt;cy.get('[data-cy=email]').type('jane@example.com')&lt;/code&gt;, you are not actually getting the email field and then typing into it. You are appending two commands to a queue. After your &lt;code&gt;it&lt;/code&gt; block returns, Cypress walks the queue and executes the commands one at a time, with each command's result feeding into the next.&lt;/p&gt;

&lt;p&gt;This is the part that breaks people's brains, so let me say it again with feeling. &lt;strong&gt;Cypress commands are not promises. Cypress commands cannot be awaited.&lt;/strong&gt; The official docs say so explicitly: "While the API may look similar to Promises, with its &lt;code&gt;then()&lt;/code&gt; syntax, Cypress commands and queries are not promises they are serial commands passed into a central queue, to be executed asynchronously at a later date."&lt;/p&gt;

&lt;p&gt;This means the following code, which you will absolutely write at some point, does not work:&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="c1"&gt;// DOES NOT WORK&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&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;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.title&lt;/span&gt;&lt;span class="dl"&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;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// undefined&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;await&lt;/code&gt; succeeds sort of but &lt;code&gt;text&lt;/code&gt; is not the element. It's whatever Cypress decides to return, which is not what you want. The correct version is:&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;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.title&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;$el&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;$el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="c1"&gt;// do something with text inside this callback&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You have to do everything inside the chain. You can't extract a value, do some plain JavaScript with it, and then resume Cypress commands as if they were normal async/await code. The chain is the whole world.&lt;/p&gt;

&lt;p&gt;The first time this hits you, it feels like a bug. The second time, it feels like an inconvenience. The hundredth time, after you've worked around it with custom commands and aliases and &lt;code&gt;cy.then&lt;/code&gt; callbacks for two years, it feels like the cost of admission. Some developers love it, because the queue-based model is what enables the magic of automatic waiting and time-travel debugging. Some developers hate it, because it forecloses entire categories of normal JavaScript patterns.&lt;/p&gt;

&lt;p&gt;GitHub issue #1417, "Await-ing Cypress Chains," is one of the most-engaged issues in the project's history. The answer, which has been the answer since 2017, is still no. Cypress's whole architecture would have to change.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Best Thing Cypress Got Right: Implicit Assertions and Auto-Waiting
&lt;/h2&gt;

&lt;p&gt;Here's the thing Cypress nailed and nobody else had really cracked at the time. Look at this:&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;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.notification&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;should&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;contain&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Saved&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;This command does something subtle. It tries to find &lt;code&gt;.notification&lt;/code&gt;. If it doesn't exist yet, it retries. If it exists but doesn't contain "Saved" yet, it retries. If both conditions become true within the timeout (default four seconds), the command passes. If neither is true after four seconds, it fails.&lt;/p&gt;

&lt;p&gt;You don't write &lt;code&gt;await page.waitForSelector&lt;/code&gt; followed by a separate assertion. The assertion is the wait. The wait is the assertion. They're the same thing.&lt;/p&gt;

&lt;p&gt;This was the answer to the most common form of test flakiness. In Selenium tests, you'd write something like:&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;await&lt;/span&gt; &lt;span class="nx"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;until&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;elementLocated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;By&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;css&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.notification&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="mi"&gt;5000&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;text&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;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;By&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;css&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.notification&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;getText&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Saved&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;Three operations, three opportunities for a race condition. You wait for the element to be located, then you find it (it might have re-rendered between those steps), then you get its text (it might have changed). Cypress collapses all of this into one retry-able assertion. The element either exists and contains the text within four seconds, or it doesn't. If your test is going to be flaky here, it'll be flaky on this one line, not on three different lines for three different reasons.&lt;/p&gt;

&lt;p&gt;This pattern alone is responsible for a lot of Cypress's reputation for "non-flaky" tests. It's not that the tool magically eliminated race conditions. It's that the API made the right thing easy to write.&lt;/p&gt;

&lt;p&gt;Playwright eventually adopted similar patterns &lt;code&gt;expect(locator).toContainText(...)&lt;/code&gt; retries the way Cypress's &lt;code&gt;should&lt;/code&gt; does but Cypress had it first, and for a few years that mattered.&lt;/p&gt;

&lt;h2&gt;
  
  
  cy.intercept: Network Stubbing That Doesn't Suck
&lt;/h2&gt;

&lt;p&gt;Cypress 6.0, released in December 2020, introduced &lt;code&gt;cy.intercept&lt;/code&gt;. Before it, network stubbing was done with &lt;code&gt;cy.route&lt;/code&gt; and &lt;code&gt;cy.server&lt;/code&gt;, which only worked for XHR meaning if your app used &lt;code&gt;fetch&lt;/code&gt; (which by 2020 most apps did), you were out of luck without a workaround.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;cy.intercept&lt;/code&gt; works at the proxy layer. It catches every network request the page makes, regardless of whether it's &lt;code&gt;XMLHttpRequest&lt;/code&gt;, &lt;code&gt;fetch&lt;/code&gt;, an iframe load, a WebSocket handshake, or a static asset. You can spy on requests, stub them with static responses, or attach handlers that modify requests and responses on the fly:&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="c1"&gt;// Spy mode wait for the request, don't change it&lt;/span&gt;
&lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;intercept&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/users&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;getUsers&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;visit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/users&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@getUsers&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Static stub&lt;/span&gt;
&lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;intercept&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/users&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="na"&gt;fixture&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;users.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// Dynamic handler&lt;/span&gt;
&lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;intercept&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/login&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;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;correct&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;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;abc123&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="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Invalid credentials&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="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;cy.wait('@alias')&lt;/code&gt; pattern is the real magic. Instead of &lt;code&gt;cy.wait(2000)&lt;/code&gt;, you wait until a specific network request happens. Tests get faster (you only wait as long as you need to) and less flaky (you're not guessing how long the network will take). The Cypress best practices page is dogmatic about this, and it's right. Every time I see &lt;code&gt;cy.wait(5000)&lt;/code&gt; in a codebase, I know I'm about to find a flaky test.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;cy.intercept&lt;/code&gt; is, hands down, my favorite Cypress feature. It's the part of the API that has aged the best, and it's still genuinely competitive with Playwright's &lt;code&gt;page.route&lt;/code&gt; (which works similarly but with slightly different ergonomics).&lt;/p&gt;

&lt;h2&gt;
  
  
  The Famous Errors
&lt;/h2&gt;

&lt;p&gt;You can't write about Cypress without writing about its errors, because every Cypress developer alive has seen these specific messages and felt the specific kind of dread they produce.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"This element is detached from the DOM."&lt;/strong&gt; Here's the actual text:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;CypressError: Timed out retrying after 4050ms: cy.click() failed because this element is detached from the DOM. Cypress requires elements be attached in the DOM to interact with them. The previous command that ran was: &amp;gt; cy.should() This DOM element likely became detached somewhere between the previous and current command. Common situations why this happens: - Your JS framework re-rendered asynchronously - Your app code reacted to an event firing and removed the element&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;What this means: Cypress found the element, was about to click it, and in the millisecond between finding and clicking, your React component re-rendered and replaced the DOM node with a new one. The reference Cypress has is now pointing to a node that no longer exists in the document.&lt;/p&gt;

&lt;p&gt;The fix is usually to re-query break the chain so Cypress refetches the element right before the click. Cypress 12 in December 2022 made this much better by rewriting the query commands to retry-and-re-query automatically in many cases. It still happens, especially with libraries like react-select or Material UI's autocompletes that do aggressive async re-rendering. GitHub issues #5743, #6215, #7306, #17043, #25863 are all this same problem with collectively thousands of upvotes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Cypress detected a cross origin error."&lt;/strong&gt; Exact text:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;CypressError: Cypress detected a cross origin error happened on page load: Blocked a frame with origin "&lt;a href="http://example.com" rel="noopener noreferrer"&gt;http://example.com&lt;/a&gt;" from accessing a cross-origin frame.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Before Cypress 9.6 in April 2022, this was a near-fatal limitation. If your test needed to navigate to a different origin say, an OAuth provider you basically couldn't. The workaround was &lt;code&gt;chromeWebSecurity: false&lt;/code&gt;, which disabled most of the browser's security model for the test run, and even that didn't always work.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;cy.origin('https://other.example.com', () =&amp;gt; { ... })&lt;/code&gt; is the modern fix. It runs the callback in the context of the other origin. It works, but with caveats. You can't nest origins. The callback runs in a fresh JavaScript context, so closures over outer-scope variables don't work you have to explicitly pass them in via the &lt;code&gt;args&lt;/code&gt; option. And the syntax is, frankly, awkward enough that I've seen teams just write OAuth flows as API requests in &lt;code&gt;cy.request()&lt;/code&gt; instead, bypassing the UI entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Cannot click on multiple elements."&lt;/strong&gt; Cypress, by design, refuses to operate on multiple matching elements unless you explicitly ask for them. This is good philosophy your test should know exactly which element it's targeting but it produces this error any time your selector matches more than one thing, and figuring out which extra element matched can take longer than you'd think.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The chain is broken errors&lt;/strong&gt; when you accidentally try to use a Cypress command outside the chain context, or you &lt;code&gt;await&lt;/code&gt; something you shouldn't have, the error messages are detailed but not always actionable. New users hit these constantly.&lt;/p&gt;

&lt;h2&gt;
  
  
  cy.session: The Quiet Revolution
&lt;/h2&gt;

&lt;p&gt;If you've been writing Cypress tests for years and still log in via the UI in &lt;code&gt;beforeEach&lt;/code&gt;, please stop. &lt;code&gt;cy.session()&lt;/code&gt; exists. It's been GA since Cypress 12 in December 2022. With &lt;code&gt;cacheAcrossSpecs: true&lt;/code&gt;, you log in once per test run instead of once per test:&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;login&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&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;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&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;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;visit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/login&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[data-cy=email]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[data-cy=password]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Cypress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PASSWORD&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
      &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[data-cy=submit]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;should&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;include&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/dashboard&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;cacheAcrossSpecs&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="nf"&gt;beforeEach&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="nf"&gt;login&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jane@example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;visit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/some-page&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first time &lt;code&gt;login('jane@example.com')&lt;/code&gt; runs, it executes the callback. Cypress captures all the cookies, localStorage, and sessionStorage at the end. Every subsequent call with the same key restores those cookies and storage instead of running the callback again. On a 100-test suite where logging in takes two seconds, this saves more than three minutes per run.&lt;/p&gt;

&lt;p&gt;Filip Hric's blog post "Use cy.session instead of login page object" is the canonical write-up if you want to dig deeper. The pattern took a while to spread because it was experimental for so long, but in 2026 there's no excuse for not using it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Component Testing in Cypress
&lt;/h2&gt;

&lt;p&gt;Cypress 10, released June 1, 2022, made Component Testing a first-class mode. You can mount React, Vue, Angular, or Svelte components directly in the runner and test them in isolation:&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;Stepper&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./Stepper&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;Stepper /&amp;gt;&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;increments and decrements&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Stepper&lt;/span&gt; &lt;span class="nx"&gt;initialValue&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;)
&lt;/span&gt;    &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[data-cy=increment]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;should&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;be.visible&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[data-cy=decrement]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;should&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;be.visible&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 pitch is real: you're testing in a real browser, with real CSS, real layout, real DOM. JSDOM (which React Testing Library uses by default) lies about a lot of layout-related things. Visibility checks are unreliable. Hover states don't work. CSS that depends on the viewport doesn't render correctly. Cypress component tests don't have those problems.&lt;/p&gt;

&lt;p&gt;So why hasn't it taken over? Two reasons.&lt;/p&gt;

&lt;p&gt;First, RTL plus Vitest is faster. A lot faster. Cypress component tests need to boot a browser, load a dev server, and render the component, all per spec file. Vitest tests run in milliseconds and parallelize across CPU cores trivially. For most components buttons, forms, data displays JSDOM is fine, and the speed difference matters more than the fidelity difference.&lt;/p&gt;

&lt;p&gt;Second, the React testing ecosystem standardized on RTL in 2020, and that gravity is hard to escape. Most teams have RTL tests already. Most tutorials use RTL. The "Testing Library" mental model (test what the user sees, not implementation details) is what every senior React developer absorbed.&lt;/p&gt;

&lt;p&gt;Cypress component testing is genuinely useful for visual-heavy components, complex hover/focus interactions, drag-and-drop, anything where the real DOM matters. For most teams it ends up being a complement to RTL, not a replacement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cypress Cloud and the Cost Question
&lt;/h2&gt;

&lt;p&gt;The free Cypress runner is MIT-licensed and complete. You can run thousands of tests on your laptop or in CI without paying anyone. Where Cypress.io makes its money is &lt;strong&gt;Cypress Cloud&lt;/strong&gt;, which records test runs to a dashboard, provides screenshots and videos, and crucially handles parallelization.&lt;/p&gt;

&lt;p&gt;Here's the part that drives migrations. &lt;strong&gt;Parallelization is paywalled.&lt;/strong&gt; If you want to split your test suite across multiple CI machines to make it run faster, you need a Cloud subscription. The free tier gives you 500 test results per month, which a real CI pipeline can chew through in a day or two.&lt;/p&gt;

&lt;p&gt;Pricing as of early 2026 looks roughly like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Free / Starter: 500 results/month, 3 users, 30-day retention&lt;/li&gt;
&lt;li&gt;Team: around $67/month&lt;/li&gt;
&lt;li&gt;Business: around $267/month&lt;/li&gt;
&lt;li&gt;Enterprise: custom, typically $15K to $40K+ annually for mid-market&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a one-developer side project, free is fine. For a five-person team, Team works. For a real product with a proper CI pipeline, you're either paying low four figures a year for Business or you're rolling your own parallelization with &lt;code&gt;cypress-split&lt;/code&gt; or Sorry Cypress or Currents.dev.&lt;/p&gt;

&lt;p&gt;This pricing model free OSS tool, paid coordination layer is the same model GitLab and HashiCorp use, and it's defensible. But for testing tools, where Playwright offers parallelization for free, it stings. The BigBinary blog post "Why we switched from Cypress to Playwright" cites paywalled parallelization as one of their two main migration triggers, alongside the inability to bypass third-party bot detection in real OAuth flows.&lt;/p&gt;

&lt;p&gt;Cypress Cloud has shipped genuinely impressive new features in the last two years Test Replay (think Playwright's Trace Viewer but for CI runs), UI Coverage (visualizes what percent of your UI is exercised by tests), Cypress Accessibility (axe-core scans on every recorded run), and recently Cloud MCP for AI assistants. These are real and they solve real problems. But they're also enterprise features at enterprise prices, and the message they send is clear: Cypress.io's commercial focus has moved upmarket.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Playwright Comparison We've Been Avoiding
&lt;/h2&gt;

&lt;p&gt;Let's just do this.&lt;/p&gt;

&lt;p&gt;In June 2024, Playwright passed Cypress in weekly npm downloads for the first time. Since then the gap has only widened. As of late April 2026, Playwright sits around 48 million weekly downloads to Cypress's 7 million. State of JS 2024 showed Playwright leading on retention (the percentage of users who say they'd use it again) at around 94%, with Cypress around 74%. Playwright's GitHub stars are roughly 86,000 to Cypress's 49,600.&lt;/p&gt;

&lt;p&gt;These numbers are not close.&lt;/p&gt;

&lt;p&gt;The reasons, in approximate order of how often I hear them cited by teams that migrated:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Free parallelization.&lt;/strong&gt; Playwright parallelizes across CPU cores by default. No paid cloud needed. For teams that were paying mid-four-figures a year for Cypress Cloud just to get tests running fast in CI, this alone justifies the migration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multi-browser, including real WebKit.&lt;/strong&gt; Playwright runs against Chromium, Firefox, and WebKit (which is Safari's engine). Cypress can run against Chrome, Edge, Firefox, and Electron, but not WebKit. If your users use Safari and on iOS, your users always use Safari Playwright actually tests their browser. Cypress doesn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multi-tab, multi-origin, multi-context.&lt;/strong&gt; Cypress can't open a second tab. Playwright can, trivially. Cypress's &lt;code&gt;cy.origin&lt;/code&gt; works but has limitations. Playwright treats different origins as a non-event.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Real async/await.&lt;/strong&gt; Playwright tests are normal async JavaScript. You can &lt;code&gt;await&lt;/code&gt; anything. You can mix Playwright calls with regular function calls and Promises without thinking about it. The mental model is just JavaScript. Coming from Cypress, this feels like a weight lifting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Speed.&lt;/strong&gt; The BigBinary writeup cites their auth flow going from two minutes in Cypress to under twenty seconds in Playwright. One of their isolated tests went from 16 seconds to 1.8 an 88% speedup. Playwright's out-of-process design makes parallelism easy, and it's just generally faster at routine operations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trace Viewer.&lt;/strong&gt; Playwright's equivalent of Cypress's time-travel debugger is Trace Viewer, and it's good. You get DOM snapshots, network logs, console logs, action timelines, all for every test run, all without paying anyone.&lt;/p&gt;

&lt;p&gt;These are real advantages and they're not subjective. If you're greenfielding a project in 2026 and you don't have a strong reason to pick Cypress, you should probably pick Playwright. I say this as someone with eight hundred Cypress tests in production and tattoos that say "data-cy" on them figuratively.&lt;/p&gt;

&lt;p&gt;So why am I writing this article instead of an article called "Don't Use Cypress"? Because Cypress still has things going for it.&lt;/p&gt;

&lt;p&gt;The runner experience the live, interactive GUI where you watch your tests run alongside your application is still better than Playwright's UI mode in some ways, especially for debugging and authoring new tests. The time-travel debugger is still uniquely good. The community knowledge base the Stack Overflow answers, the recipe blogs, the years of accumulated patterns  is enormous, even compared to Playwright. Component testing in a real browser is a genuine niche that Playwright doesn't really compete in. And for teams that already have a thousand Cypress tests in production, the migration cost is non-trivial. We did the math at one point: at our pace, migrating our suite would have taken about four engineering months. That's a lot of months.&lt;/p&gt;

&lt;p&gt;If you're starting fresh: Playwright. If you're considering migrating: do the math. If you're staying on Cypress: that's a perfectly defensible choice in 2026, you just need to know what you're trading off.&lt;/p&gt;

&lt;h2&gt;
  
  
  Best Practices That Actually Matter
&lt;/h2&gt;

&lt;p&gt;If you're going to write Cypress tests, the official best practices page is genuinely worth reading top to bottom. The greatest hits, with my commentary:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use &lt;code&gt;data-cy&lt;/code&gt; (or &lt;code&gt;data-test&lt;/code&gt;) attributes for selectors.&lt;/strong&gt; Not classes. Not IDs. Classes change for styling reasons. IDs sometimes change for accessibility reasons. &lt;code&gt;data-cy&lt;/code&gt; exists for one reason to be a stable selector for tests and that's its whole job. The Cypress Selector Playground autodetects them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't write conditional tests.&lt;/strong&gt; If you find yourself writing &lt;code&gt;if (element exists) { do thing } else { do other thing }&lt;/code&gt;, your test isn't deterministic. Either the thing is going to happen or it isn't. Pick one and assert on it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't share state between tests.&lt;/strong&gt; Each test should set up everything it needs. Use &lt;code&gt;cy.session&lt;/code&gt; for auth, &lt;code&gt;cy.task&lt;/code&gt; to seed your database, &lt;code&gt;cy.intercept&lt;/code&gt; to control the network. Don't rely on test A having logged in before test B runs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't use &lt;code&gt;cy.wait(milliseconds)&lt;/code&gt;.&lt;/strong&gt; Ever. This is the single biggest cause of slow, flaky Cypress tests in the wild. Wait on aliases (&lt;code&gt;cy.wait('@apiCall')&lt;/code&gt;) or wait on assertions (&lt;code&gt;cy.get('.thing').should('be.visible')&lt;/code&gt;). Cypress's whole retry-ability machinery is designed to make &lt;code&gt;cy.wait(5000)&lt;/code&gt; unnecessary. If you're using it, you're fighting the framework.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Always start a new chain after an action.&lt;/strong&gt; Don't try to chain queries through a &lt;code&gt;.click()&lt;/code&gt;. Click ends a chain. Start fresh:&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="c1"&gt;// Sketchy&lt;/span&gt;
&lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.btn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.result&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Better&lt;/span&gt;
&lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.btn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.result&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;&lt;strong&gt;Don't test multiple flows in one test.&lt;/strong&gt; A test should do one thing. If you find yourself writing &lt;code&gt;it('handles signup, login, and password reset')&lt;/code&gt;, split it. Cypress is fast enough that the overhead per test is minimal compared to the debugging cost when one big test fails for unclear reasons.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't use traditional Page Object Models.&lt;/strong&gt; This one is contentious. Cypress's official guidance is to skip the POM pattern and use custom commands plus &lt;code&gt;data-cy&lt;/code&gt; attributes instead. The community is split there are a thousand Cypress POM tutorials online but I've come around to Cypress's view. POMs add a layer of abstraction that doesn't pay off in a queue-based command model. Custom commands give you the same encapsulation with less ceremony.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use &lt;code&gt;Cypress.Commands.add&lt;/code&gt; for repeated patterns.&lt;/strong&gt; A &lt;code&gt;cy.login(email)&lt;/code&gt; command, once written, makes every subsequent test cleaner. Same for any UI flow that repeats opening a dialog, filling a form, navigating to a tab.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Still Hard
&lt;/h2&gt;

&lt;p&gt;Honesty corner. There are things Cypress is just bad at, even in 2026:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;iframes.&lt;/strong&gt; They're better than they used to be, but still awkward. The &lt;code&gt;cypress-iframe&lt;/code&gt; plugin helps. Tests that involve embedded payment forms (Stripe Elements, etc.) are usually either painful or written as API tests instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;File downloads.&lt;/strong&gt; Cypress has no built-in way to verify a file was downloaded. You either intercept the download URL and check the response, or you use a plugin like &lt;code&gt;cypress-downloadfile&lt;/code&gt;, or you skip it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mobile emulation.&lt;/strong&gt; You can set a viewport size, but real mobile emulation touch events, mobile-specific network conditions, device frames is limited. Playwright is significantly better here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Visual regression testing.&lt;/strong&gt; Cypress doesn't have built-in visual diffing. You bolt on Percy, Applitools, or Cypress Cloud's own UI Coverage feature. Each costs money. Playwright has built-in screenshot comparison.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WebKit/Safari.&lt;/strong&gt; Just no. Not happening. The architecture forecloses it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Performance testing.&lt;/strong&gt; Cypress is for functional testing. If you want Lighthouse-style metrics or detailed performance traces, you're in Playwright territory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The async chain still trips up new developers.&lt;/strong&gt; I've onboarded probably a dozen engineers onto Cypress over the years, and every single one hit the "why doesn't &lt;code&gt;await cy.get()&lt;/code&gt; work" wall in their first week. It's a well-trodden path with good explanations available, but it's still a path.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to Pick Cypress in 2026
&lt;/h2&gt;

&lt;p&gt;After all that, here's my honest recommendation framework.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pick Cypress if:&lt;/strong&gt; you primarily test in Chromium, you value a live interactive runner above all else, your team is small enough that the free tier or Team tier is enough, you don't need multi-tab or multi-origin in serious ways, your existing team already knows Cypress, you have an existing Cypress test suite that's working, or you genuinely prefer the queue/chain mental model after trying both.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pick Playwright if:&lt;/strong&gt; you need cross-browser including WebKit, you have a complex CI parallelization need, you need multi-tab or multi-context flows, you're starting fresh in 2026 with no existing tests and no team preference, you want async/await to just work, or your application includes serious mobile flows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pick neither if:&lt;/strong&gt; you're testing a static site or a simple form, in which case Cypress and Playwright are both overkill and you should just write a few integration tests with Vitest plus jsdom and call it a day.&lt;/p&gt;

&lt;p&gt;For component testing specifically, default to React Testing Library plus Vitest, and reach for Cypress component testing only when you have specific real-browser needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Realistic Cypress Test File in 2026
&lt;/h2&gt;

&lt;p&gt;Here's what I think a healthy Cypress E2E test file looks like today:&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="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Checkout Flow&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;beforeEach&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;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;checkout-user&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/test/login&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="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;test@example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Cypress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;TEST_PASSWORD&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;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;token&lt;/span&gt;&lt;span class="dl"&gt;'&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;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="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;cacheAcrossSpecs&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="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;intercept&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/products*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;getProducts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;intercept&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/orders&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;createOrder&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;visit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/shop&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@getProducts&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;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;completes checkout with valid card&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[data-cy=product-card]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;within&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;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[data-cy=add-to-cart]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[data-cy=cart-button]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[data-cy=checkout-button]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[data-cy=card-number]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;4242424242424242&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[data-cy=card-expiry]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1234&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[data-cy=card-cvc]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;123&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[data-cy=pay-button]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@createOrder&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;its&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;response.statusCode&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;should&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;eq&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;should&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;include&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/order-confirmation&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/thank you/i&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;should&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;be.visible&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;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;shows error on declined card&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;intercept&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/orders&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="na"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;402&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Card declined&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;as&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;declinedOrder&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[data-cy=product-card]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;within&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;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[data-cy=add-to-cart]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[data-cy=cart-button]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[data-cy=checkout-button]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[data-cy=card-number]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;4000000000000002&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[data-cy=card-expiry]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1234&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[data-cy=card-cvc]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;123&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[data-cy=pay-button]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@declinedOrder&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/card declined/i&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;should&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;be.visible&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;should&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;not.include&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/order-confirmation&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;Notice the patterns. Authentication via &lt;code&gt;cy.session&lt;/code&gt; with cross-spec caching. Network calls aliased and waited on by alias, never by milliseconds. Selectors all use &lt;code&gt;data-cy&lt;/code&gt;. Each test does one thing. Setup happens via the API where possible (the login bypasses the UI entirely with &lt;code&gt;cy.request&lt;/code&gt;), reserving the UI exercise for the actual feature being tested. Stubbed responses for failure scenarios using &lt;code&gt;cy.intercept&lt;/code&gt;'s static stub mode. Assertions are &lt;code&gt;should&lt;/code&gt;-based, leveraging implicit retry-ability.&lt;/p&gt;

&lt;p&gt;If your Cypress tests look something like this, you're in good shape. If they look like a hundred lines of &lt;code&gt;cy.wait(2000)&lt;/code&gt; and UI-based logins in &lt;code&gt;beforeEach&lt;/code&gt;, you have an afternoon's worth of cleanup that will make your suite dramatically faster and less flaky.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Closing Argument
&lt;/h2&gt;

&lt;p&gt;Cypress was the first testing tool that made me enjoy writing browser tests. That's not a small thing. For ten years before Cypress, browser testing was a chore that everyone did badly because the tools were bad. Cypress changed the baseline expectation for what these tools should feel like. Every modern alternative, including Playwright, has been pulled forward by Cypress's example.&lt;/p&gt;

&lt;p&gt;That doesn't mean it's the right tool for new projects today. The architectural choices that made Cypress feel magical in 2017 running in the browser, queue-based commands, time-travel by default are also the choices that have foreclosed multi-browser and multi-tab and easy parallelism, and the world has moved toward those things mattering more.&lt;/p&gt;

&lt;p&gt;If I'm starting a new project tomorrow, I'm starting with Playwright. If I'm advising a team with an existing Cypress suite that's working, I'm telling them to keep it and use the time saved to ship features. If I'm hiring, I'm looking for people who understand testing principles, not specific tools, because both will be relevant for years to come.&lt;/p&gt;

&lt;p&gt;The good news is that the principles are the same regardless. Test what the user does, not what the code does internally. Wait on conditions, not on time. Set up state programmatically, exercise the UI for the thing under test, assert on what's visible. Don't share state. Don't get clever. Whether you write that with &lt;code&gt;cy.get&lt;/code&gt; or &lt;code&gt;page.locator&lt;/code&gt;, the test that comes out the other side will be the same kind of test. The tool is a syntax. The thinking is the work.&lt;/p&gt;

&lt;p&gt;Now if you'll excuse me, I have eight hundred tests to keep alive.&lt;/p&gt;

</description>
      <category>test</category>
      <category>react</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>5 Things AI Can't Do, Even in React Testing Library</title>
      <dc:creator>DevUnionX</dc:creator>
      <pubDate>Tue, 28 Apr 2026 02:02:29 +0000</pubDate>
      <link>https://dev.to/devunionx/5-things-ai-cant-do-even-in-react-testing-library-34o1</link>
      <guid>https://dev.to/devunionx/5-things-ai-cant-do-even-in-react-testing-library-34o1</guid>
      <description>&lt;h2&gt;
  
  
  I Spent Three Years Writing the Wrong React Tests. Here's What I Wish Someone Had Told Me.
&lt;/h2&gt;

&lt;p&gt;If you wonder my game:&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://play.google.com/store/apps/details?id=com.electricitytycoon.app" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fplay-lh.googleusercontent.com%2FZ4J_ayp4GTnVs1XnvyIBAgkvjLK1jUcUu8MViVnx38_7n-JVf6mMYN3_ySqts4fSx26fb1aZVkeS9Ne7DbA9VA" height="auto" class="m-0"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://play.google.com/store/apps/details?id=com.electricitytycoon.app" rel="noopener noreferrer" class="c-link"&gt;
            Electricity Tycoon - Apps on Google Play
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Build your electricity empire from a hand crank to industrial power plants.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.gstatic.com%2Fandroid%2Fmarket_images%2Fweb%2Ffavicon_v3.ico"&gt;
          play.google.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;The first React test I ever wrote checked that a component's state had a property called &lt;code&gt;count&lt;/code&gt; and that it equaled zero. I was using Enzyme. I was very proud. The test passed. Six weeks later we refactored that component to use a reducer and renamed &lt;code&gt;count&lt;/code&gt; to &lt;code&gt;value&lt;/code&gt;, and seventeen tests in the same file went red even though the application worked exactly the same as before. I spent a Friday afternoon updating tests that tested nothing useful.&lt;/p&gt;

&lt;p&gt;If you've been writing React tests for any length of time, you've probably had this experience. The tests pass. The tests fail. The tests don't seem to care whether your application actually works for an actual human being. You start to wonder if testing is just an elaborate ceremony we perform to feel professional.&lt;/p&gt;

&lt;p&gt;It isn't. But the tools we used to use made it feel that way, and the tools we use now — primarily React Testing Library — have a philosophy that makes everything click into place once you internalize it. This article is the long version of "internalize it." Get coffee. We're going to talk about queries, async, the &lt;code&gt;act()&lt;/code&gt; warning that has eaten more developer hours than any other single warning in frontend history, why your snapshots are lying to you, and what the actual state of React testing looks like in April 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Sentence That Changed React Testing
&lt;/h2&gt;

&lt;p&gt;There is one sentence on the testing-library.com docs that you should tattoo on the inside of your eyelids:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The more your tests resemble the way your software is used, the more confidence they can give you.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's it. That's the whole framework. Everything that follows in this article — every query priority, every async pattern, every reason to delete your snapshot tests — is a consequence of that one sentence.&lt;/p&gt;

&lt;p&gt;Kent C. Dodds wrote it in a tweet years ago, then it became the project tagline, then it became the README of every Testing Library repo, and now it's basically the unofficial constitution of the React testing world. The reason it matters is that it tells you what to optimize for. You're not optimizing for code coverage. You're not optimizing for "every function has a test." You're optimizing for &lt;strong&gt;confidence that your application works for the people who use it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Most of the bad testing habits I had with Enzyme came from optimizing for the wrong thing. I was optimizing for "every component has a corresponding test file with the same name." I was optimizing for "every line of code is covered." I was optimizing for the dashboard at the top of our CI tool. None of those things told me whether the user could actually click the button and have something happen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Enzyme Died (And Why That Was a Good Thing)
&lt;/h2&gt;

&lt;p&gt;Enzyme is dead. I should say this with some kindness because Enzyme was important and a lot of us learned testing on it, but it's been clinically dead since December 21, 2019, when version 3.11.0 shipped and then nothing happened for the next six and a half years. There is no official React 17 adapter, no React 18 adapter, no React 19 adapter. Community-maintained adapters exist — &lt;code&gt;@wojtekmaj/enzyme-adapter-react-17&lt;/code&gt; and &lt;code&gt;@cfaester/enzyme-adapter-react-18&lt;/code&gt; — but the maintainer of one of them literally writes "you probably shouldn't use it" in the README. That's the state of things.&lt;/p&gt;

&lt;p&gt;The reason Enzyme died is interesting. It wasn't a security issue. It wasn't that React broke it on purpose. It was that the React team moved toward concurrent rendering and hooks, and Enzyme's whole API was built around the idea that you could reach into a component and inspect its instance — its state, its props, its internal methods. With hooks, there's no instance to inspect. There's just a function that runs. Enzyme's mental model didn't have a place for that.&lt;/p&gt;

&lt;p&gt;But the deeper reason Enzyme died is that the way it encouraged you to write tests was structurally wrong. You'd render a component, then assert on its state. You'd call &lt;code&gt;wrapper.instance().handleClick()&lt;/code&gt; directly. You'd use &lt;code&gt;shallow()&lt;/code&gt; rendering, which renders only one component without its children, and then you'd assert that a child component received certain props. None of these things are things a user does. A user doesn't know your component has state. A user doesn't call your handler functions directly. A user doesn't know what's a parent and what's a child.&lt;/p&gt;

&lt;p&gt;Kent C. Dodds wrote a post called "Testing Implementation Details" that still gets quoted constantly. The line that landed for me was about renaming. Imagine your component has a state variable called &lt;code&gt;openIndex&lt;/code&gt;. You can rename it to &lt;code&gt;openIndexes&lt;/code&gt;, or to &lt;code&gt;tacosAreTasty&lt;/code&gt;, and the application works exactly the same. The user can't tell. The interface to the outside world hasn't changed. But every test that asserted on &lt;code&gt;openIndex&lt;/code&gt; just broke. That's a false negative. The test failed but nothing is broken.&lt;/p&gt;

&lt;p&gt;The opposite happens too. You can break the application — say, the click handler stops actually doing anything — and your test still passes because it was checking that &lt;code&gt;state.count&lt;/code&gt; got incremented, not that anything appeared on the screen. That's a false positive. The test passed but everything is broken.&lt;/p&gt;

&lt;p&gt;When most of your test failures fall into one of those two categories, you stop trusting your tests. And once you stop trusting your tests, you might as well not have them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Query Priority Hierarchy (And Why It Exists)
&lt;/h2&gt;

&lt;p&gt;React Testing Library gives you a bunch of ways to find elements in the DOM. They're not equivalent. They're ranked, and the ranking is opinionated, and the opinion comes from that one sentence about resembling how users use your software.&lt;/p&gt;

&lt;p&gt;Here's the order, with my unsolicited commentary on each.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;getByRole&lt;/code&gt;&lt;/strong&gt; is the one you should reach for first. Almost always. If you're writing a test and you find yourself reaching for something else, ask whether you can use &lt;code&gt;getByRole&lt;/code&gt; instead. It works for buttons, links, headings, form inputs (textbox, checkbox, radio, combobox), regions, dialogs, lists — basically every meaningful piece of a web interface. The reason it's at the top is that ARIA roles are how assistive technology like screen readers understands your page. If your test can find a button by its accessible role and name, then a screen reader user can find that button too. The query is also surprisingly tolerant. &lt;code&gt;screen.getByRole('button', {name: /submit/i})&lt;/code&gt; finds a button whose accessible name contains "submit", case-insensitive. You don't need exact text. You don't need an ID. You're describing the button the way a person would describe it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;getByLabelText&lt;/code&gt;&lt;/strong&gt; is for form fields. When a sighted user fills out a form, they look at the label and then click the field next to it. When a screen reader user fills out a form, the label is announced when the field is focused. Either way, the label is the user's way in. So &lt;code&gt;screen.getByLabelText(/email/i)&lt;/code&gt; is how your test should find the email field, not by some &lt;code&gt;data-cy="email-input"&lt;/code&gt; attribute. The bonus is that if your test breaks because the label disappeared, that's actually a real bug — your form just became inaccessible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;getByPlaceholderText&lt;/code&gt;&lt;/strong&gt; is a fallback for when a field has no label. The docs are clear about this: a placeholder is not a substitute for a label. But sometimes you're testing legacy code and there's just no label. Fine. Use it. Then file a ticket to add a label.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;getByText&lt;/code&gt;&lt;/strong&gt; is for non-interactive content. Headings, paragraphs, status messages, error text. If the user can read it on the page, your test can find it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;getByDisplayValue&lt;/code&gt;&lt;/strong&gt; finds form elements by their current value. Useful when you're testing that a form is pre-populated with edit data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;getByAltText&lt;/code&gt;&lt;/strong&gt; for images. &lt;strong&gt;&lt;code&gt;getByTitle&lt;/code&gt;&lt;/strong&gt; is rarely useful and the docs warn that title attributes aren't consistently announced by screen readers anyway.&lt;/p&gt;

&lt;p&gt;And finally, at the bottom: &lt;strong&gt;&lt;code&gt;getByTestId&lt;/code&gt;&lt;/strong&gt;. This is the escape hatch. The docs literally say "the user cannot see (or hear) these, so this is only recommended for cases where you can't match by role or text or it doesn't make sense." If your codebase has &lt;code&gt;data-testid&lt;/code&gt; sprinkled on every other element, that's a smell. It usually means somebody learned testing-library by Googling "how do I find an element in RTL" and the first Stack Overflow answer said &lt;code&gt;getByTestId&lt;/code&gt;. There's a reason it's last on the list.&lt;/p&gt;

&lt;p&gt;The mental shift takes a while. The first time I sat down to convert a test to use &lt;code&gt;getByRole&lt;/code&gt; instead of &lt;code&gt;container.querySelector('.submit-btn')&lt;/code&gt;, I had to actually look at my component and ask "wait, is this even a button? Or did I make a div with an onClick?" Half the time the answer was "I made a div with an onClick" and the test was forcing me to fix the accessibility of the component before I could even test it. That's the whole point.&lt;/p&gt;

&lt;h2&gt;
  
  
  getBy, queryBy, findBy — The Cheat Sheet Nobody Reads
&lt;/h2&gt;

&lt;p&gt;There are three flavors of every query, and developers confuse them constantly. Here's what they do.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;getBy*&lt;/code&gt;&lt;/strong&gt; throws an error if the element isn't found, returns it if it is. Use this for "I'm asserting this element exists." If you do &lt;code&gt;screen.getByRole('button', {name: /submit/i})&lt;/code&gt; and there's no submit button, your test fails with a useful error message that includes the entire DOM and a list of all the roles that &lt;em&gt;do&lt;/em&gt; exist. Helpful.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;queryBy*&lt;/code&gt;&lt;/strong&gt; returns null if the element isn't found, returns it if it is. Use this for "I'm asserting this element does NOT exist." Because if you used &lt;code&gt;getByText('Error')&lt;/code&gt; to assert that no error was shown, the &lt;code&gt;getBy&lt;/code&gt; would throw before your assertion ever ran. So you do &lt;code&gt;expect(screen.queryByText('Error')).not.toBeInTheDocument()&lt;/code&gt; instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;findBy*&lt;/code&gt;&lt;/strong&gt; returns a Promise that resolves when the element appears, or rejects after a timeout (default 1000ms). Use this for "I'm asserting this element will appear, but not synchronously." Anything triggered by an async effect, a network request, a state update after a user event — &lt;code&gt;findBy*&lt;/code&gt;. The docs put it plainly: &lt;code&gt;findBy*&lt;/code&gt; is literally &lt;code&gt;getBy*&lt;/code&gt; plus &lt;code&gt;waitFor&lt;/code&gt;. Just shorter.&lt;/p&gt;

&lt;p&gt;The number one mistake I see in code review is people using &lt;code&gt;getByText&lt;/code&gt; to check for something that hasn't appeared yet, then wondering why the test fails. The element isn't there yet. Wait for it. Use &lt;code&gt;findByText&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The number two mistake is using &lt;code&gt;getByText&lt;/code&gt; to assert something is gone. Use &lt;code&gt;queryByText&lt;/code&gt;. The error you get from &lt;code&gt;getByText&lt;/code&gt; failing is "Unable to find an element with text," which is technically true but not what your test is trying to say.&lt;/p&gt;

&lt;p&gt;There's also &lt;code&gt;getAllBy*&lt;/code&gt;, &lt;code&gt;queryAllBy*&lt;/code&gt;, and &lt;code&gt;findAllBy*&lt;/code&gt; for when you expect multiple matches. The behavior is what you'd guess.&lt;/p&gt;

&lt;h2&gt;
  
  
  fireEvent vs user-event: The Day I Learned to Add &lt;code&gt;await&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;For the first year I used React Testing Library, I used &lt;code&gt;fireEvent&lt;/code&gt; for everything. &lt;code&gt;fireEvent.click(button)&lt;/code&gt;. &lt;code&gt;fireEvent.change(input, {target: {value: 'hello'}})&lt;/code&gt;. It worked. Tests passed. Life was good.&lt;/p&gt;

&lt;p&gt;Then I read the user-event docs and realized I'd been simulating the wrong thing.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;fireEvent&lt;/code&gt; dispatches a single DOM event. Period. &lt;code&gt;fireEvent.click(button)&lt;/code&gt; fires a click event. That's it. No focus event, no mousedown, no mouseup. Just click. &lt;code&gt;fireEvent.change(input, {target: {value: 'hello'}})&lt;/code&gt; fires a single change event with the value already set. The user never actually typed anything. The input was never focused. No keystrokes happened. From the DOM's perspective, the value just teleported in.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;user-event&lt;/code&gt; simulates what actually happens when a real human interacts with the page. &lt;code&gt;user.click(button)&lt;/code&gt; triggers a &lt;code&gt;mousedown&lt;/code&gt;, then a &lt;code&gt;mouseup&lt;/code&gt;, then a &lt;code&gt;click&lt;/code&gt;, and along the way it focuses the button if it's focusable. &lt;code&gt;user.type(input, 'hello')&lt;/code&gt; focuses the input, then for each character fires a &lt;code&gt;keydown&lt;/code&gt;, a &lt;code&gt;keypress&lt;/code&gt;, an &lt;code&gt;input&lt;/code&gt; event, and a &lt;code&gt;keyup&lt;/code&gt;. It manipulates the cursor position. It does what a real keyboard does.&lt;/p&gt;

&lt;p&gt;This matters more than you'd think. There are bugs that only appear when events fire in a specific order. There are form libraries (looking at you, react-hook-form) that depend on focus and blur events to run validation. There are accessibility behaviors that hinge on whether an element actually got focused. If you use &lt;code&gt;fireEvent&lt;/code&gt;, none of this gets exercised. Your test runs in a fantasy world where state just changes.&lt;/p&gt;

&lt;p&gt;In version 14 of &lt;code&gt;user-event&lt;/code&gt; (released March 2022, currently at 14.6.1 as of January 2025), every interaction became asynchronous. This was a big migration pain at the time and is still tripping people up in 2026 because half the tutorials online are from before the change. The new pattern looks like this:&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;userEvent&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@testing-library/user-event&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;submits the form&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;userEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;MyForm&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByLabelText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/email/i&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jane@example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button&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="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/submit/i&lt;/span&gt;&lt;span class="p"&gt;}))&lt;/span&gt;

  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/thanks/i&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeInTheDocument&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;Notice three things: &lt;code&gt;userEvent.setup()&lt;/code&gt; is called once at the top, every interaction is &lt;code&gt;await&lt;/code&gt;ed, and we use &lt;code&gt;findByText&lt;/code&gt; for content that appears after async work.&lt;/p&gt;

&lt;p&gt;The number one symptom of forgetting an &lt;code&gt;await&lt;/code&gt; somewhere is the warning we're about to talk about.&lt;/p&gt;

&lt;h2&gt;
  
  
  The act() Warning: A Eulogy In Three Acts
&lt;/h2&gt;

&lt;p&gt;If you've written React tests and never seen the &lt;code&gt;act()&lt;/code&gt; warning, you have not actually written React tests. Here it is, in its full glory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Warning: An update to ComponentName inside a test was not wrapped in act(...).

When testing, code that causes React state updates should be wrapped into act(...):

act(() =&amp;gt; {
  /* fire events that update state */
});
/* assert on the output */

This ensures that you're testing the behavior the user would see in the browser.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I have seen this warning in production codebases. I have seen it in tutorials. I have seen it in pull request descriptions where the author says "ignore the warnings, the tests pass." I have personally spent multiple full evenings hunting down a single act warning in a single test file. It is the most universally hated message in the React ecosystem and it is also, somewhat unfairly, almost always your fault.&lt;/p&gt;

&lt;p&gt;Here is what &lt;code&gt;act()&lt;/code&gt; actually is. React batches state updates and side effects in production. When you're testing, you want to make assertions on the DOM after all those batched updates have flushed. &lt;code&gt;act()&lt;/code&gt; is the mechanism that says "do all the React work, including effects, before this function returns." If you cause a state update without it being inside an &lt;code&gt;act()&lt;/code&gt; boundary, React doesn't know whether you're going to do more updates next, so it warns you that the test environment isn't behaving like the browser.&lt;/p&gt;

&lt;p&gt;The good news is that React Testing Library wraps almost everything in &lt;code&gt;act()&lt;/code&gt; for you. &lt;code&gt;render()&lt;/code&gt; is wrapped. &lt;code&gt;fireEvent&lt;/code&gt; is wrapped. &lt;code&gt;waitFor&lt;/code&gt; is wrapped. &lt;code&gt;findBy*&lt;/code&gt; is wrapped. &lt;code&gt;user-event&lt;/code&gt; v14 is wrapped. You almost never need to call &lt;code&gt;act()&lt;/code&gt; yourself.&lt;/p&gt;

&lt;p&gt;So why do you keep seeing the warning?&lt;/p&gt;

&lt;p&gt;Because something is updating state after your test thinks it's done. The most common cause is a &lt;code&gt;useEffect&lt;/code&gt; that runs after initial render and triggers a state update. Your test renders the component, makes its assertions, and exits — and then the effect fires, and React tries to update state, and there's no test left to be inside an &lt;code&gt;act()&lt;/code&gt; boundary.&lt;/p&gt;

&lt;p&gt;The fix is almost always to wait for the state to settle before your test ends. If you have a component that fetches data on mount, your test should look like:&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="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;shows the user list&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;UserList&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/jane doe/i&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeInTheDocument&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;Notice the &lt;code&gt;await&lt;/code&gt; on &lt;code&gt;findByText&lt;/code&gt;. That's the magic. The test now waits until the data has loaded and the component has settled before it finishes. No more act warning.&lt;/p&gt;

&lt;p&gt;If your effect updates state but doesn't render anything new (say, it sets some internal flag), use &lt;code&gt;waitFor&lt;/code&gt;:&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;await&lt;/span&gt; &lt;span class="nf"&gt;waitFor&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mockSomething&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveBeenCalled&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're using fake timers, that's a whole separate can of worms. &lt;code&gt;jest.useFakeTimers()&lt;/code&gt; runs code outside React's normal callstack, so the auto-&lt;code&gt;act&lt;/code&gt; doesn't catch it. You need to manually wrap the timer advancement:&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;await&lt;/span&gt; &lt;span class="nf"&gt;act&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;jest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;runAllTimers&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 you're using a form library like react-hook-form with async validation, you'll see the warning when validation runs after submit. The fix is to await something that depends on the validation completing — like an error message appearing, or a submit handler being called.&lt;/p&gt;

&lt;p&gt;The thing not to do is wrap things that are already wrapped. I see this all the time:&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="c1"&gt;// Don't do this — render is already wrapped in act&lt;/span&gt;
&lt;span class="nf"&gt;act&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="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;MyComponent&lt;/span&gt; &lt;span class="o"&gt;/&amp;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;You're double-wrapping. It doesn't break anything but it produces a different warning telling you about overlapping &lt;code&gt;act()&lt;/code&gt; calls. Stop.&lt;/p&gt;

&lt;p&gt;One more thing. In React 19 (which RTL 16.1.0 added support for, on December 5, 2024 — the same day React 19 went GA), &lt;code&gt;act&lt;/code&gt; is officially expected to be async. The synchronous version is deprecated. You should now import &lt;code&gt;act&lt;/code&gt; from &lt;code&gt;@testing-library/react&lt;/code&gt;, not from &lt;code&gt;react&lt;/code&gt;. RTL has been re-exporting it since version 15.0.6 (May 2024). If you have old test files that import &lt;code&gt;act&lt;/code&gt; from React, fix them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Async Testing: findBy, waitFor, and the Order of Operations
&lt;/h2&gt;

&lt;p&gt;Async testing in RTL has three primitives. You only really need to know two of them well, and the third is for edge cases.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;findBy*&lt;/code&gt;&lt;/strong&gt; is for waiting for an element to appear. Use this 80% of the time you need to wait for something. The default timeout is 1000ms, which is usually fine. If you need longer, you can override it: &lt;code&gt;screen.findByText(/loaded/i, {}, {timeout: 5000})&lt;/code&gt;. Avoid bumping the timeout in normal cases — it usually means your component is doing something genuinely slow that you should fix.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;waitFor&lt;/code&gt;&lt;/strong&gt; is for waiting for an arbitrary assertion to pass. Use this when what you're waiting for isn't an element appearing — like waiting for a mock function to have been called, or waiting for a fetch to have happened.&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="c1"&gt;// ✅ Good&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;waitFor&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mockOnSubmit&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveBeenCalledWith&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a@b.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}))&lt;/span&gt;

&lt;span class="c1"&gt;// ❌ Bad — putting side effects inside waitFor&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;waitFor&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;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mockOnSubmit&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveBeenCalled&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 reason the second one is bad is that &lt;code&gt;waitFor&lt;/code&gt; runs its callback repeatedly until either the assertion passes or the timeout fires. If you have a click event inside there, you'll fire that click event many times. That's not what you want.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;waitForElementToBeRemoved&lt;/code&gt;&lt;/strong&gt; is for waiting for something to disappear. Loading spinners, mostly. The pattern is:&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;await&lt;/span&gt; &lt;span class="nf"&gt;waitForElementToBeRemoved&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;queryByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/loading/i&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice we use &lt;code&gt;queryByText&lt;/code&gt; here, not &lt;code&gt;getByText&lt;/code&gt;. That's because at the moment the element is finally removed, &lt;code&gt;getByText&lt;/code&gt; would throw (the element isn't there anymore), which would defeat the whole purpose.&lt;/p&gt;

&lt;p&gt;The most common async testing mistake I've seen recently is using &lt;code&gt;waitFor&lt;/code&gt; to look for an element when &lt;code&gt;findBy&lt;/code&gt; would do:&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="c1"&gt;// ❌ Verbose&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;waitFor&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ Direct&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;button&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;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button&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;These are functionally equivalent. &lt;code&gt;findBy*&lt;/code&gt; uses &lt;code&gt;waitFor&lt;/code&gt; internally. The second version is shorter, the error messages are better, and it makes intent clearer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mock Service Worker, or, Stop Mocking fetch
&lt;/h2&gt;

&lt;p&gt;If you're testing components that fetch data, you have a few options. Most of them are bad.&lt;/p&gt;

&lt;p&gt;You can mock &lt;code&gt;window.fetch&lt;/code&gt; with &lt;code&gt;jest.fn()&lt;/code&gt;. This works but it's brittle — every test has to set up the mock, you couple your tests to the exact shape of the request, and as soon as you switch from &lt;code&gt;fetch&lt;/code&gt; to &lt;code&gt;axios&lt;/code&gt; to &lt;code&gt;ky&lt;/code&gt; to whatever's trendy this week, all your tests break.&lt;/p&gt;

&lt;p&gt;You can mock the network library directly. Same problem.&lt;/p&gt;

&lt;p&gt;You can use Mock Service Worker (MSW). This is what the testing-library docs themselves recommend. MSW intercepts requests at the network level — it doesn't care whether you used &lt;code&gt;fetch&lt;/code&gt;, &lt;code&gt;axios&lt;/code&gt;, &lt;code&gt;XMLHttpRequest&lt;/code&gt;, or carrier pigeons. You declare what your endpoints return, MSW handles the rest, and your component code is completely unchanged.&lt;/p&gt;

&lt;p&gt;The MSW v2 API looks like this:&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;HttpResponse&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;msw&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;setupServer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;msw/node&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setupServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/users&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="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nx"&gt;HttpResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;([{&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Jane Doe&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="nf"&gt;beforeAll&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="nf"&gt;afterEach&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resetHandlers&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="nf"&gt;afterAll&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;shows the user list&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;UserList&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/jane doe/i&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeInTheDocument&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 you have a test where you want to simulate an error, you override the handler for that one test:&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="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;shows error on server failure&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/users&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;HttpResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;oops&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="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;}))&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;UserList&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/something went wrong/i&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeInTheDocument&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern has changed how I write integration tests. I now write tests that render entire features with actual components and actual routing, and the only thing mocked is the network. The tests look almost like end-to-end tests but run in milliseconds because there's no browser.&lt;/p&gt;

&lt;p&gt;Note that MSW v2 changed the API — if you're looking at older tutorials with &lt;code&gt;rest.get&lt;/code&gt; and &lt;code&gt;res(ctx.json(...))&lt;/code&gt;, that's the v1 syntax and it no longer works. The new shape is &lt;code&gt;http.get&lt;/code&gt; and &lt;code&gt;HttpResponse.json(...)&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing React Query (Or TanStack Query, As The Cool Kids Now Call It)
&lt;/h2&gt;

&lt;p&gt;If your app uses React Query, you'll discover that your tests time out for no obvious reason. Then you'll discover that React Query retries failed queries three times by default with exponential backoff. That's great in production. In tests, it means a single failed query takes upwards of seven seconds before your test even sees the error.&lt;/p&gt;

&lt;p&gt;The fix is documented in TkDodo's blog (he's a React Query maintainer) and it's the canonical pattern:&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;function&lt;/span&gt; &lt;span class="nf"&gt;createWrapper&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;queryClient&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;QueryClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;defaultOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;queries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;retry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&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;return &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;children&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;QueryClientProvider&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;queryClient&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;children&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/QueryClientProvider&lt;/span&gt;&lt;span class="err"&gt;&amp;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;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;shows error on failure&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/data&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;HttpResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;}))&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;MyComponent&lt;/span&gt; &lt;span class="o"&gt;/&amp;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;wrapper&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;createWrapper&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/error/i&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeInTheDocument&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;Two things to notice. First, you create a fresh &lt;code&gt;QueryClient&lt;/code&gt; per test. Don't share one. Tests will pollute each other through the cache. Second, &lt;code&gt;retry: false&lt;/code&gt;. Always. In tests, you want immediate failure.&lt;/p&gt;

&lt;p&gt;If you forget to wrap your component in a &lt;code&gt;QueryClientProvider&lt;/code&gt; at all, you'll get the very recognizable &lt;code&gt;No QueryClient set, use QueryClientProvider to set one&lt;/code&gt; error. That one I've seen in roughly seventy percent of "help me debug my test" Slack threads.&lt;/p&gt;

&lt;h2&gt;
  
  
  Custom Hooks With renderHook
&lt;/h2&gt;

&lt;p&gt;You can test custom hooks directly with &lt;code&gt;renderHook&lt;/code&gt;. This used to live in a separate package called &lt;code&gt;@testing-library/react-hooks&lt;/code&gt;, which was deprecated in 2022 when &lt;code&gt;renderHook&lt;/code&gt; got moved into the main &lt;code&gt;@testing-library/react&lt;/code&gt; package starting in version 13.1.&lt;/p&gt;

&lt;p&gt;The basic pattern:&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;renderHook&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;act&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@testing-library/react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useCounter&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./useCounter&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;increments the counter&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;renderHook&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;useCounter&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

  &lt;span class="nf"&gt;act&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;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;increment&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your hook needs context, use the &lt;code&gt;wrapper&lt;/code&gt; option:&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;renderHook&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;useMyHook&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;wrapper&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;children&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;MyProvider&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;children&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/MyProvider&lt;/span&gt;&lt;span class="err"&gt;&amp;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;A small honesty note: the new &lt;code&gt;renderHook&lt;/code&gt; in &lt;code&gt;@testing-library/react&lt;/code&gt; is less full-featured than the old &lt;code&gt;@testing-library/react-hooks&lt;/code&gt; package. It doesn't have first-class SSR support. There are some &lt;code&gt;waitForNextUpdate&lt;/code&gt; patterns from the old package that have to be rewritten with &lt;code&gt;waitFor&lt;/code&gt;. If you migrated and felt like something was missing, you weren't imagining it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Routing: MemoryRouter Is Your Friend
&lt;/h2&gt;

&lt;p&gt;Testing components that use React Router requires giving them a router. You don't want the real &lt;code&gt;BrowserRouter&lt;/code&gt; because that touches &lt;code&gt;window.history&lt;/code&gt;, which is shared across tests and causes pollution. You want &lt;code&gt;MemoryRouter&lt;/code&gt;, which keeps its history in memory:&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;MemoryRouter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Routes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Route&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-router&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;MemoryRouter&lt;/span&gt; &lt;span class="nx"&gt;initialEntries&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/users/42&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Routes&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Route&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/users/:id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;UserPage&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Routes&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/MemoryRouter&lt;/span&gt;&lt;span class="err"&gt;&amp;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 &lt;code&gt;initialEntries&lt;/code&gt; prop sets where the router thinks you are. It only takes paths starting with &lt;code&gt;/&lt;/code&gt;, not full URLs (this catches people).&lt;/p&gt;

&lt;p&gt;If you're testing a component that triggers navigation, you can either render multiple routes and assert that you ended up on the right page (which is how a user would experience it), or you can use the &lt;code&gt;useLocation&lt;/code&gt; hook in a test helper component and assert on the location object. The first option is more behavior-focused and is what I usually do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Vitest Is Eating Jest's Lunch
&lt;/h2&gt;

&lt;p&gt;A note on the surrounding ecosystem because it's been shifting fast. As of 2026, Vitest has effectively passed Jest in adoption for new projects. The State of JS 2024 survey put Vitest at the top for retention and positivity rankings, and the weekly download trend lines crossed sometime in late 2025. Angular 21 (released late 2025) made Vitest the default test runner. Nuxt, SvelteKit, and Astro all recommend it. Jest is not dead — it's still huge, especially in legacy codebases — but the momentum is gone.&lt;/p&gt;

&lt;p&gt;For React Testing Library, this matters approximately zero. RTL is test-runner-agnostic. The same &lt;code&gt;render&lt;/code&gt;, &lt;code&gt;screen&lt;/code&gt;, and &lt;code&gt;userEvent&lt;/code&gt; work identically whether you're using Jest or Vitest. The difference is in setup and the matchers.&lt;/p&gt;

&lt;p&gt;For Jest, you import jest-dom matchers like this:&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@testing-library/jest-dom&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Vitest, you need a slightly different import to register the matchers with Vitest's &lt;code&gt;expect&lt;/code&gt;:&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@testing-library/jest-dom/vitest&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's about it. The actual test code is identical.&lt;/p&gt;

&lt;p&gt;A word of caution: Vitest is not always faster than Jest. There are real-world reports of teams migrating from Jest to Vitest and getting slower test suites, especially with large React codebases. The default Vitest configuration uses Vite's transform pipeline, which can be slower for some setups than Jest's babel-based pipeline. The trick most teams that get fast Vitest results use is &lt;code&gt;happy-dom&lt;/code&gt; instead of &lt;code&gt;jsdom&lt;/code&gt; for the DOM environment. That alone often cuts test times in half. Your mileage will absolutely vary, so benchmark before you commit to a migration.&lt;/p&gt;

&lt;p&gt;If you want one less thing to think about, keep using Jest. If you're starting a new project and using Vite for your build, Vitest is the natural choice. Either is fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Anti-Patterns That Are Killing Your Tests
&lt;/h2&gt;

&lt;p&gt;Let me list, in roughly the order I see them in code review, the things you should stop doing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stop using &lt;code&gt;container.querySelector&lt;/code&gt;.&lt;/strong&gt; I know it's tempting. You know CSS selectors. You wrote one in twelve seconds. But every time you do &lt;code&gt;container.querySelector('.btn-primary')&lt;/code&gt;, you're testing a class name that has nothing to do with what the user sees. Change the class name during a styling refactor, the test breaks. It also gives the worst error messages of any query type. Use &lt;code&gt;getByRole&lt;/code&gt; or one of its siblings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stop snapshot testing entire components.&lt;/strong&gt; I had to learn this one the hard way. We had a snapshot test for a dashboard component that was 640 lines long. Every time anyone changed anything in that dashboard, the snapshot would update, and the reviewer would scan the diff for ten seconds, see that all the changes "looked fine," and approve the snapshot update. We never caught a single bug with that snapshot. We just had a 640-line file in our repo that everyone learned to ignore. The Justin Searls quote that Kent C. Dodds quotes is the eulogy: "Most developers, upon seeing a snapshot test fail, will sooner just nuke the snapshot and record a fresh passing one instead of agonizing over what broke it." That's exactly what we did.&lt;/p&gt;

&lt;p&gt;Snapshots have a place. Tiny, focused snapshots of pure functions or deeply structured data are fine. Whole-component snapshots are almost always a smell.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stop testing component state.&lt;/strong&gt; If you find yourself writing &lt;code&gt;expect(wrapper.instance().state.count).toBe(1)&lt;/code&gt; you're back in Enzyme-land mentally. Test what's on the screen. If clicking the button increments a counter, assert that the displayed text changed from "0" to "1". The user can't see your state. Your test shouldn't either.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stop testing implementation methods directly.&lt;/strong&gt; No &lt;code&gt;expect(component.handleSubmit).toHaveBeenCalled()&lt;/code&gt;. Test that submitting the form calls the &lt;code&gt;onSubmit&lt;/code&gt; prop, or that an API call happens, or that the success message appears. Test the visible outcome.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stop over-mocking.&lt;/strong&gt; I saw a test recently that mocked four child components, the routing library, the date library, and a translation hook, all to "isolate" the component under test. By the time all the mocks were set up, the test was longer than the component, the component being tested wasn't really being tested anymore (since most of its actual collaborators had been replaced with stubs), and any refactor that moved logic between components broke the test. The Tahamjp dev.to article from 2025 has my favorite line on this: "Mocks are like duct tape — handy until you start covering the whole car with it."&lt;/p&gt;

&lt;p&gt;The right answer most of the time is: don't mock. Render the whole feature with MSW handling network. Let the components actually collaborate. Test what the user sees.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stop wrapping things in &lt;code&gt;act()&lt;/code&gt; that are already wrapped.&lt;/strong&gt; I covered this above but it's worth repeating. &lt;code&gt;render&lt;/code&gt;, &lt;code&gt;fireEvent&lt;/code&gt;, &lt;code&gt;waitFor&lt;/code&gt;, &lt;code&gt;findBy*&lt;/code&gt;, and v14 &lt;code&gt;userEvent&lt;/code&gt; are all wrapped internally. Don't double-wrap.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stop using &lt;code&gt;fireEvent&lt;/code&gt; when &lt;code&gt;user-event&lt;/code&gt; would do.&lt;/strong&gt; Just use &lt;code&gt;user-event&lt;/code&gt;. The async pattern feels weird at first; you'll get used to it in a week.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stop ignoring ESLint plugins.&lt;/strong&gt; There are two: &lt;code&gt;eslint-plugin-testing-library&lt;/code&gt; and &lt;code&gt;eslint-plugin-jest-dom&lt;/code&gt;. They catch most of the mistakes in this section automatically. Install them, set them to error, and let the linter teach your team.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Still Hard in 2026
&lt;/h2&gt;

&lt;p&gt;Honesty time. There are things React Testing Library doesn't do well, and pretending otherwise would be unfair.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Server Components.&lt;/strong&gt; There is no canonical way to unit-test an async React Server Component. RTL doesn't support them, the React team hasn't published a recommended pattern, and the Next.js documentation explicitly tells you to use end-to-end tests with Playwright instead. If your app is heavily server-rendered, expect a gap in your testing story. This is the single biggest unsolved problem in React testing right now.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Portals.&lt;/strong&gt; Components that render into a portal (modals, tooltips, popovers built on Radix or Headless UI) sometimes mount outside your test container, and &lt;code&gt;screen.getByRole&lt;/code&gt; doesn't find them at first glance. The fix is to use &lt;code&gt;screen&lt;/code&gt; queries (which look at &lt;code&gt;document.body&lt;/code&gt;, not just the rendered container), or to pass &lt;code&gt;baseElement: document.body&lt;/code&gt; to your render. Once you know the trick it's fine, but the first time it happens you'll spend twenty minutes confused.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Animations and motion libraries.&lt;/strong&gt; framer-motion in particular has a reputation for being slow in jsdom and producing weird test failures. There are open issues going back years. The workarounds usually involve mocking the motion components.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Suspense regression in React 19.&lt;/strong&gt; There's an open issue (#1375) where suspended components in tests sometimes keep rendering their fallbacks instead of their children. Affected at one point about 300 tests in the Vercel monorepo. Worth knowing about if you use Suspense heavily.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The npm dependency hell of late 2024.&lt;/strong&gt; When React 19 went GA, RTL 16.0.x had a peer dependency on &lt;code&gt;@testing-library/dom@^10&lt;/code&gt;, but a lot of toolchains (especially Create React App, may it rest in peace) were locked to &lt;a href="mailto:dom@8"&gt;dom@8&lt;/a&gt;. The result was the dreaded ERESOLVE errors and a lot of frantic Stack Overflow questions. RTL 16.1.0 on December 5, 2024 fixed this the same day React 19 shipped, but a lot of people are still on 16.0.1 and don't know why their installs are angry.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Realistic Test File, From Top To Bottom
&lt;/h2&gt;

&lt;p&gt;Here's what I think a good RTL test file looks like in 2026. This is for a hypothetical login form.&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;beforeAll&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;afterEach&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;afterAll&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;vi&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vitest&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;render&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;screen&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@testing-library/react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;userEvent&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@testing-library/user-event&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;HttpResponse&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;msw&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;setupServer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;msw/node&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;LoginForm&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./LoginForm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setupServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/login&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;request&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&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;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jane@example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;correct&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;return&lt;/span&gt; &lt;span class="nx"&gt;HttpResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;abc123&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;return&lt;/span&gt; &lt;span class="nx"&gt;HttpResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Invalid credentials&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="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;401&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="nf"&gt;beforeAll&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="nf"&gt;afterEach&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resetHandlers&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="nf"&gt;afterAll&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;LoginForm&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;logs in with valid credentials&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;userEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setup&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;onLogin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;vi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;LoginForm&lt;/span&gt; &lt;span class="nx"&gt;onLogin&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;onLogin&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;)
&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByLabelText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/email/i&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jane@example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByLabelText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/password/i&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;correct&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button&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="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/log in/i&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;waitFor&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;onLogin&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveBeenCalledWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;abc123&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;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;shows an error with invalid credentials&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;userEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;LoginForm&lt;/span&gt; &lt;span class="nx"&gt;onLogin&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;vi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt; &lt;span class="sr"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;)
&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByLabelText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/email/i&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jane@example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByLabelText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/password/i&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;wrong&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button&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="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/log in/i&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;

    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;alert&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toHaveTextContent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/invalid credentials/i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;disables the submit button while submitting&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;userEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;LoginForm&lt;/span&gt; &lt;span class="nx"&gt;onLogin&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;vi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt; &lt;span class="sr"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;)
&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByLabelText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/email/i&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jane@example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByLabelText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/password/i&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;correct&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;button&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button&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="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/log in/i&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBeDisabled&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;Notice what's not there. No mocks of child components. No assertions on state. No &lt;code&gt;data-testid&lt;/code&gt; attributes. No snapshots. No checking that handlers were called with specific internal arguments. Every assertion is something a real user could verify by looking at or interacting with the page. The login flow is tested end to end with real network mocking.&lt;/p&gt;

&lt;p&gt;This is the shape of a healthy RTL test file. If your test files don't look like this, that's something to think about.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Closing Argument
&lt;/h2&gt;

&lt;p&gt;When I started writing React tests with Enzyme, I thought testing was about coverage. Cover every line, cover every branch, sleep at night. When I switched to React Testing Library, I thought testing was about queries — learn the priority list, find the elements the right way, sleep at night.&lt;/p&gt;

&lt;p&gt;After a few years, I think testing is mostly about having a good answer to one question: &lt;strong&gt;if my application is broken, will my test suite tell me?&lt;/strong&gt; Not "will some test fail." Not "will coverage drop." Will my tests, the actual ones I wrote with intent, tell me that the user's experience is broken. If yes, the tests are doing their job. If no, they're decoration.&lt;/p&gt;

&lt;p&gt;React Testing Library doesn't make this question easier to answer by accident. It makes it easier on purpose, by forcing you to write tests in the language of the user instead of the language of the implementation. That's the whole thing. That's the only reason any of this matters. Every query priority, every async pattern, every anti-pattern in this article — they all come back to that.&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;getByRole&lt;/code&gt;. Await your &lt;code&gt;user.click&lt;/code&gt;. Wait for things with &lt;code&gt;findBy*&lt;/code&gt;. Mock the network with MSW. Don't snapshot anything you wouldn't want to read. Don't reach into state. Don't test children you've already mocked. Trust your tests to fail loudly when something real is broken, and to stay quiet when you're just renaming a variable.&lt;/p&gt;

&lt;p&gt;If you do that, you'll spend less of your life arguing with the act warning, and more of your life shipping. Which, last time I checked, is what we're all here for.&lt;/p&gt;

&lt;p&gt;Now stop reading and go delete some snapshots.&lt;/p&gt;

</description>
      <category>react</category>
      <category>testing</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>!!!Hello</title>
      <dc:creator>DevUnionX</dc:creator>
      <pubDate>Sat, 25 Apr 2026 17:05:24 +0000</pubDate>
      <link>https://dev.to/devunionx/hello-h4c</link>
      <guid>https://dev.to/devunionx/hello-h4c</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/devunionx/5-things-ai-cant-do-even-in-npm-yarn-pnpm-4ef5" class="crayons-story__hidden-navigation-link"&gt;5 Things AI Can't Do, Even in npm / yarn / pnpm&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/devunionx" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3180316%2F804c7ae5-1a93-4c38-b9ec-023a59a621a8.jpg" alt="devunionx profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/devunionx" class="crayons-story__secondary fw-medium m:hidden"&gt;
              DevUnionX
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                DevUnionX
                
              
              &lt;div id="story-author-preview-content-3550601" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/devunionx" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3180316%2F804c7ae5-1a93-4c38-b9ec-023a59a621a8.jpg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;DevUnionX&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/devunionx/5-things-ai-cant-do-even-in-npm-yarn-pnpm-4ef5" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Apr 25&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/devunionx/5-things-ai-cant-do-even-in-npm-yarn-pnpm-4ef5" id="article-link-3550601"&gt;
          5 Things AI Can't Do, Even in npm / yarn / pnpm
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/npm"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;npm&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/devops"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;devops&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ai"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ai&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/programming"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;programming&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/devunionx/5-things-ai-cant-do-even-in-npm-yarn-pnpm-4ef5" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/exploding-head-daceb38d627e6ae9b730f36a1e390fca556a4289d5a41abb2c35068ad3e2c4b5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;6&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/devunionx/5-things-ai-cant-do-even-in-npm-yarn-pnpm-4ef5#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            21 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
    </item>
  </channel>
</rss>
