<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: joe-re</title>
    <description>The latest articles on Forem by joe-re (@joe-re).</description>
    <link>https://forem.com/joe-re</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F52265%2Fb76bcf58-d762-4e09-9865-31f0367f4d51.jpg</url>
      <title>Forem: joe-re</title>
      <link>https://forem.com/joe-re</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/joe-re"/>
    <language>en</language>
    <item>
      <title>Why Does GraphQL Return 200 Even on Errors? A Clear Guide to GraphQL HTTP Status Codes</title>
      <dc:creator>joe-re</dc:creator>
      <pubDate>Wed, 11 Mar 2026 16:16:04 +0000</pubDate>
      <link>https://forem.com/joe-re/why-does-graphql-return-200-even-on-errors-a-clear-guide-to-graphql-http-status-codes-194m</link>
      <guid>https://forem.com/joe-re/why-does-graphql-return-200-even-on-errors-a-clear-guide-to-graphql-http-status-codes-194m</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;🇯🇵 This article is a self-translated version of my original post written in Japanese, published on Zenn: &lt;a href="https://zenn.dev/peoplex_blog/articles/fbae471fc99dfd" rel="noopener noreferrer"&gt;GraphQLはなぜエラーでも200を返すのか？&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;One of the most frequently debated topics in GraphQL error handling is: &lt;em&gt;what HTTP status code should the server return?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In REST, the convention is clear: &lt;code&gt;400&lt;/code&gt; for bad input, &lt;code&gt;403&lt;/code&gt; for authorization errors, &lt;code&gt;500&lt;/code&gt; for server errors. The HTTP status code carries the semantic meaning of the response.&lt;/p&gt;

&lt;p&gt;GraphQL, however, can return &lt;code&gt;200&lt;/code&gt; even when the response contains errors.&lt;br&gt;
That said, this does &lt;strong&gt;not&lt;/strong&gt; mean "always return 200 regardless of what went wrong." This tends to confuse developers coming from a REST background.&lt;/p&gt;

&lt;p&gt;Historically, &lt;code&gt;application/json&lt;/code&gt; was the widely used content type for GraphQL responses, and for compatibility reasons, returning &lt;code&gt;200&lt;/code&gt; for everything was the safe choice. With the introduction of &lt;code&gt;application/graphql-response+json&lt;/code&gt;, however, things have changed — the cases where &lt;code&gt;4xx&lt;/code&gt; or &lt;code&gt;5xx&lt;/code&gt; should be returned are now much more clearly defined.&lt;/p&gt;

&lt;p&gt;Since status code design comes up frequently in internal discussions on my team, I'd like to share my own understanding of the current GraphQL over HTTP spec and its historical evolution.&lt;/p&gt;


&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;My conclusion: return &lt;code&gt;4xx&lt;/code&gt;/&lt;code&gt;5xx&lt;/code&gt; in roughly two situations, and &lt;code&gt;2xx&lt;/code&gt; for everything else.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;When an error occurs before GraphQL execution begins&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;When a well-formed GraphQL response cannot be constructed&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Errors before execution fall into two main categories:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Document syntax errors or failure to identify the operation&lt;/strong&gt; — the GraphQL request itself is malformed → &lt;code&gt;400&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rejection at the HTTP layer before reaching the endpoint&lt;/strong&gt; — e.g., unauthenticated request → &lt;code&gt;401&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Failure to construct a GraphQL response typically means a server-side problem (e.g., server crash) where no response body can be generated at all → &lt;code&gt;5xx&lt;/code&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The GraphQL over HTTP spec also allows servers to add custom validation rules (e.g., depth limits, complexity limits). Whether application-specific input rules fall into this category is not explicitly defined in the spec. Given that the listed examples are things like depth/complexity limits, it seems reasonable to interpret this as covering &lt;strong&gt;pre-execution, GraphQL-layer validations&lt;/strong&gt; rather than business logic errors.&lt;/p&gt;
&lt;/blockquote&gt;


&lt;h2&gt;
  
  
  Two Types of GraphQL Errors
&lt;/h2&gt;

&lt;p&gt;GraphQL has a concept called &lt;strong&gt;Partial Response&lt;/strong&gt;.&lt;br&gt;
→ &lt;a href="https://spec.graphql.org/draft/#sec-Response" rel="noopener noreferrer"&gt;GraphQL Spec: Response&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The idea is that even if an error occurs during execution, only the failed part is dropped — the successfully resolved parts are still returned.&lt;/p&gt;

&lt;p&gt;The spec distinguishes between two result types: &lt;strong&gt;Execution Result&lt;/strong&gt; and &lt;strong&gt;Request Error Result&lt;/strong&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Execution Result and Partial Response
&lt;/h3&gt;

&lt;p&gt;→ &lt;a href="https://spec.graphql.org/draft/#sec-Execution-Result" rel="noopener noreferrer"&gt;GraphQL Spec: Execution Result&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In an Execution Result, the &lt;code&gt;data&lt;/code&gt; field is always present. Even when errors occur, they appear alongside the data in an &lt;code&gt;errors&lt;/code&gt; field.&lt;/p&gt;

&lt;p&gt;In other words, &lt;strong&gt;a response with both &lt;code&gt;data&lt;/code&gt; and &lt;code&gt;errors&lt;/code&gt; is perfectly normal in GraphQL&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here's a simple example. Consider the following query where &lt;code&gt;favoriteArticles&lt;/code&gt; is nullable:&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;users&lt;/span&gt;&lt;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;name&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;favoriteArticles&lt;/span&gt;&lt;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;references&lt;/span&gt;&lt;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;# error occurs here&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="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;If fetching &lt;code&gt;references&lt;/code&gt; throws an error, GraphQL propagates &lt;code&gt;null&lt;/code&gt; up to the nearest nullable field (&lt;code&gt;favoriteArticles&lt;/code&gt;), and the response looks like this:&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;"users"&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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="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;"joe-re"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"favoriteArticles"&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;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;"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="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;"Failed to fetch references"&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="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"users"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"favoriteArticles"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"references"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is key: in GraphQL, &lt;code&gt;errors&lt;/code&gt; in the response body represents &lt;strong&gt;a part of the GraphQL execution result&lt;/strong&gt;, not an HTTP-level failure.&lt;br&gt;
→ &lt;a href="https://graphql.org/learn/response/#field-errors" rel="noopener noreferrer"&gt;GraphQL Docs: Field Errors&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Request Error Result
&lt;/h3&gt;

&lt;p&gt;→ &lt;a href="https://spec.graphql.org/draft/#sec-Request-Error-Result" rel="noopener noreferrer"&gt;GraphQL Spec: Request Error Result&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A GraphQL request returns a request error result when one or more request errors are raised, causing the request to fail before execution. This request will result in no response data.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This covers &lt;strong&gt;errors that occur before execution even begins&lt;/strong&gt;. Critically, &lt;strong&gt;the &lt;code&gt;data&lt;/code&gt; field is not returned at all&lt;/strong&gt; in this case.&lt;/p&gt;

&lt;p&gt;The spec lists causes such as:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A request error may be raised before execution due to missing information, syntax errors, validation failure, coercion failure, or any other reason the implementation may determine should prevent the request from proceeding.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;These are cases where the server decides &lt;strong&gt;the request cannot proceed&lt;/strong&gt; — before GraphQL execution starts.&lt;/p&gt;




&lt;h2&gt;
  
  
  HTTP Status Codes in GraphQL over HTTP
&lt;/h2&gt;

&lt;p&gt;The above distinction (Execution Result vs. Request Error Result) comes from the core GraphQL spec. How these map to HTTP is defined by the &lt;strong&gt;GraphQL over HTTP&lt;/strong&gt; spec.&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://graphql.github.io/graphql-over-http/draft/#sec-Status-Codes" rel="noopener noreferrer"&gt;GraphQL over HTTP: Status Codes&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In case of errors that completely prevent the generation of a well-formed GraphQL response, the server SHOULD respond with the appropriate status code depending on the concrete error condition, and MUST NOT respond with a &lt;code&gt;2xx&lt;/code&gt; status code when using the &lt;code&gt;application/graphql-response+json&lt;/code&gt; media type.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The key principle here is: &lt;strong&gt;whether a well-formed GraphQL response can be generated&lt;/strong&gt; determines the HTTP status code.&lt;/p&gt;

&lt;p&gt;For example, if the response body can't be assembled at all (e.g., the JSON is broken or the server crashed), a well-formed response is impossible — so &lt;code&gt;2xx&lt;/code&gt; is inappropriate.&lt;/p&gt;

&lt;p&gt;The spec provides concrete examples for &lt;code&gt;application/graphql-response+json&lt;/code&gt;:&lt;br&gt;
→ &lt;a href="https://graphql.github.io/graphql-over-http/draft/#sec-application-graphql-response-json.Examples" rel="noopener noreferrer"&gt;6.4.2.1 Examples&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: The rest of this article assumes &lt;code&gt;application/graphql-response+json&lt;/code&gt;. The differences with &lt;code&gt;application/json&lt;/code&gt; are covered later.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  When to Return 4xx
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;4xx&lt;/code&gt; applies when &lt;strong&gt;the request fails before GraphQL execution begins&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Document syntax error&lt;/li&gt;
&lt;li&gt;Document validation failure&lt;/li&gt;
&lt;li&gt;Operation cannot be determined&lt;/li&gt;
&lt;li&gt;Variable coercion failure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are cases where the server can determine upfront that &lt;strong&gt;the request cannot proceed&lt;/strong&gt;. For &lt;code&gt;application/graphql-response+json&lt;/code&gt;, the spec recommends &lt;strong&gt;&lt;code&gt;400 Bad Request&lt;/code&gt;&lt;/strong&gt; for these.&lt;/p&gt;

&lt;p&gt;Separately, requests that fail at the HTTP layer — such as &lt;strong&gt;unauthenticated requests&lt;/strong&gt; that never reach the GraphQL endpoint — should return &lt;code&gt;401&lt;/code&gt; or similar.&lt;/p&gt;

&lt;h3&gt;
  
  
  When to Return 2xx
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;2xx&lt;/code&gt; applies when &lt;strong&gt;GraphQL execution proceeded but encountered errors along the way&lt;/strong&gt; — i.e., when Partial Response applies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An exception thrown inside a resolver&lt;/li&gt;
&lt;li&gt;A partial field resolution failure&lt;/li&gt;
&lt;li&gt;A business logic condition that caused processing to fail&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In these cases, the failed fields become &lt;code&gt;null&lt;/code&gt; in the response. GraphQL's null propagation rules apply: if a non-null field is &lt;code&gt;null&lt;/code&gt;, the error propagates up to the nearest nullable ancestor.&lt;br&gt;
→ &lt;a href="https://spec.graphql.org/draft/#sec-Handling-Execution-Errors" rel="noopener noreferrer"&gt;GraphQL Spec: Handling Execution Errors&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  When to Return 5xx
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;5xx&lt;/code&gt; applies when &lt;strong&gt;a well-formed GraphQL response cannot be returned at all&lt;/strong&gt;, regardless of pre-execution checks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The server has crashed&lt;/li&gt;
&lt;li&gt;Middleware fails before reaching the GraphQL handler&lt;/li&gt;
&lt;li&gt;The response body cannot be generated&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Since returning any well-formed GraphQL response is impossible, these are treated as standard HTTP server errors.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Did Things Look Like Before &lt;code&gt;application/graphql-response+json&lt;/code&gt;?
&lt;/h2&gt;

&lt;p&gt;A bit of historical context helps here.&lt;/p&gt;

&lt;p&gt;GraphQL originally used &lt;code&gt;application/json&lt;/code&gt; as the response content type. During the development of the GraphQL over HTTP spec, a GraphQL-specific media type called &lt;code&gt;application/graphql+json&lt;/code&gt; was introduced. However, this name caused confusion with the media type for GraphQL documents (&lt;code&gt;.graphql&lt;/code&gt; files), so in 2022 it was renamed to &lt;code&gt;application/graphql-response+json&lt;/code&gt; to clearly indicate it's for responses.&lt;br&gt;
→ &lt;a href="https://github.com/graphql/graphql-over-http/pull/215" rel="noopener noreferrer"&gt;GitHub PR #215&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For legacy compatibility, the official docs recommend including &lt;code&gt;application/json&lt;/code&gt; in the &lt;code&gt;Accept&lt;/code&gt; header when targeting GraphQL servers that predated 2025:&lt;br&gt;
→ &lt;a href="https://graphql.org/learn/serving-over-http/" rel="noopener noreferrer"&gt;GraphQL: Serving over HTTP&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The &lt;code&gt;application/graphql-response+json&lt;/code&gt; is described in the draft GraphQL over HTTP specification. To ensure compatibility, if a client sends a request to a legacy GraphQL server before 1st January 2025, the &lt;code&gt;Accept&lt;/code&gt; header should also include the &lt;code&gt;application/json&lt;/code&gt; media type as follows: &lt;code&gt;application/graphql-response+json, application/json&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Timeline summary:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;application/json&lt;/code&gt; was the de facto standard for GraphQL responses&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;application/graphql-response+json&lt;/code&gt; was proposed in 2022&lt;/li&gt;
&lt;li&gt;It is now the preferred response type in the GraphQL over HTTP spec&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Why &lt;code&gt;application/json&lt;/code&gt; Alone Was Problematic
&lt;/h2&gt;

&lt;p&gt;GraphQL works fine with &lt;code&gt;application/json&lt;/code&gt;. But using HTTP status codes meaningfully alongside it had a problem:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When a non-2xx JSON response is received, it's hard to tell if it came from the GraphQL server itself.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;400&lt;/code&gt; or &lt;code&gt;500&lt;/code&gt; JSON response might be from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The GraphQL server&lt;/li&gt;
&lt;li&gt;An intermediary like an API Gateway, reverse proxy, or WAF&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From the client's perspective, there's no reliable way to distinguish these. This is why, in the &lt;code&gt;application/json&lt;/code&gt; world, &lt;strong&gt;always returning &lt;code&gt;200&lt;/code&gt;&lt;/strong&gt; was the safer operational choice — and why that pattern persists in many codebases today.&lt;/p&gt;




&lt;h2&gt;
  
  
  How &lt;code&gt;application/graphql-response+json&lt;/code&gt; Solved This
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;application/graphql-response+json&lt;/code&gt; was introduced precisely to address this ambiguity. It is a media type exclusively for GraphQL responses.&lt;/p&gt;

&lt;p&gt;The GraphQL over HTTP spec positions it as the &lt;strong&gt;preferred response type&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Its key benefit: &lt;strong&gt;even when the HTTP status is not &lt;code&gt;200&lt;/code&gt;, the response body can be reliably interpreted as a GraphQL response&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This enables a much cleaner separation of concerns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;HTTP status code&lt;/strong&gt; → signals transport-level success or failure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Response body&lt;/strong&gt; → carries GraphQL-level error details&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For clients, this means you can more confidently trust the &lt;code&gt;errors&lt;/code&gt; field in the response — without worrying about whether the JSON came from GraphQL or from an intermediary.&lt;/p&gt;




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

&lt;p&gt;In this article, I've shared my understanding of GraphQL status code design, grounded in the GraphQL over HTTP spec.&lt;/p&gt;

&lt;p&gt;Since &lt;code&gt;application/graphql-response+json&lt;/code&gt; was introduced, the use of &lt;code&gt;4xx&lt;/code&gt;/&lt;code&gt;5xx&lt;/code&gt; in GraphQL has become much clearer and better defined.&lt;/p&gt;

&lt;p&gt;The key mental model when thinking about GraphQL status codes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Did the error occur before GraphQL execution started?&lt;/strong&gt; → &lt;code&gt;4xx&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Did execution proceed, but result in an error?&lt;/strong&gt; → &lt;code&gt;2xx&lt;/code&gt; with &lt;code&gt;errors&lt;/code&gt; in the body&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Could no GraphQL response be generated at all?&lt;/strong&gt; → &lt;code&gt;5xx&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With &lt;code&gt;application/graphql-response+json&lt;/code&gt; as your baseline, this distinction becomes very clean.&lt;/p&gt;

&lt;p&gt;I hope this helps someone out there navigating the same questions.&lt;/p&gt;

</description>
      <category>graphql</category>
      <category>webdev</category>
      <category>api</category>
      <category>http</category>
    </item>
    <item>
      <title>I Built a Desktop App to Supercharge My TMUX + Claude Code Workflow</title>
      <dc:creator>joe-re</dc:creator>
      <pubDate>Mon, 12 Jan 2026 16:19:29 +0000</pubDate>
      <link>https://forem.com/joe-re/i-built-a-desktop-app-to-supercharge-my-tmux-claude-code-workflow-521m</link>
      <guid>https://forem.com/joe-re/i-built-a-desktop-app-to-supercharge-my-tmux-claude-code-workflow-521m</guid>
      <description>&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;Until recently, I was primarily using Cursor for AI-assisted coding. The editor-centric AI integration worked really well with my development style—it made the feedback loop smooth when AI-generated code didn't match my intentions, whether I needed to manually fix it or provide additional instructions.&lt;/p&gt;

&lt;p&gt;But everything changed when Opus 4.5 was released in late November last year.&lt;/p&gt;

&lt;p&gt;Opus 4.5 delivers outputs that match my expectations far better than any previous model. Claude Code's CUI-first design also feels natural to my workflow. Now, Claude Code has become the center of my development process. I've locked in Opus 4.5 as my daily driver.&lt;/p&gt;

&lt;p&gt;I typically run multiple Claude Code sessions simultaneously—across different projects or multiple branches using git worktree. Managing notifications and checking outputs across these sessions is critical.&lt;/p&gt;

&lt;p&gt;I was using OS notifications to check in whenever changes happened, but I kept missing them. I wanted something better.&lt;/p&gt;

&lt;p&gt;So I built an app to streamline my workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/joe-re/eyes-on-claude-code" rel="noopener noreferrer"&gt;eyes-on-claude-code&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It's a cross-platform desktop application built with &lt;strong&gt;Tauri&lt;/strong&gt;. I've only tested it on macOS (my daily environment), but the codebase is designed to support Linux as well.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Environment &amp;amp; Workflow
&lt;/h2&gt;

&lt;p&gt;This app is primarily designed to optimize my own workflow, so the features reflect my environment and habits.&lt;/p&gt;

&lt;p&gt;I develop using &lt;strong&gt;Ghostty + tmux&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;My typical workflow looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Draft ideas and design in Markdown&lt;/li&gt;
&lt;li&gt;Give instructions to Claude Code (using Plan mode for larger tasks)&lt;/li&gt;
&lt;li&gt;Review the diff of generated code, then provide additional instructions or continue&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Features
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Multi-Session Monitoring Dashboard
&lt;/h3&gt;

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

&lt;p&gt;The dashboard monitors Claude Code sessions by receiving events through hooks configured in the global &lt;code&gt;settings.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Since I keep this running during development, I designed it with a minimal, non-intrusive UI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Always-on-top mode&lt;/strong&gt; (optional) ensures the window doesn't get buried under other apps—so you never miss a notification.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Transparency settings&lt;/strong&gt; let you configure opacity separately for active and inactive states. When inactive, you can make it nearly invisible so it doesn't get in the way.&lt;/p&gt;

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

&lt;p&gt;&lt;em&gt;It's there in the top-right corner, barely visible.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Status Display &amp;amp; Sound Notifications
&lt;/h3&gt;

&lt;p&gt;Sessions are displayed with one of four states:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;State&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;th&gt;Display&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Active&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Claude is working&lt;/td&gt;
&lt;td&gt;🟢&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;WaitingPermission&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Waiting for permission approval&lt;/td&gt;
&lt;td&gt;🔐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;WaitingInput&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Waiting for user input (idle)&lt;/td&gt;
&lt;td&gt;⏳&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Completed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Response complete&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Sound effects play on state changes (can be toggled off):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Waiting (Permission/Input)&lt;/strong&gt;: Alert tone (two low beeps)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Completed&lt;/strong&gt;: Completion chime (ascending two-note sound)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'm planning to add volume control and custom commands in the future—like using &lt;code&gt;say&lt;/code&gt; to speak or playing music on completion 🎵&lt;/p&gt;

&lt;h3&gt;
  
  
  Git-Based Diff Viewer
&lt;/h3&gt;

&lt;p&gt;I usually review AI-generated changes using &lt;a href="https://github.com/yoshiko-pg/difit" rel="noopener noreferrer"&gt;difit&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I wanted to integrate that same flow into this app, so you can launch difit directly on changed files.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Huge thanks to the difit team for building such a great tool!&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  tmux Integration
&lt;/h3&gt;

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

&lt;p&gt;When developing, I use tmux panes and tabs to manage multiple windows. My typical setup is Claude Code on the left half, and server/commands on the right.&lt;/p&gt;

&lt;p&gt;When working across multiple projects or branches via git worktree, it's frustrating to hunt for which tmux tab has Claude Code running.&lt;/p&gt;

&lt;p&gt;So I added a &lt;strong&gt;tmux mirror view&lt;/strong&gt; that lets you quickly check results and give simple instructions without switching tabs.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;p&gt;The app uses Claude Code hooks to determine session status based on which hooks fire.&lt;/p&gt;

&lt;h3&gt;
  
  
  Event Flow
&lt;/h3&gt;

&lt;p&gt;I didn't want to introduce complexity with an intermediate server for inter-process communication. So I went with a simple approach: hooks write to log files, and the app watches those files.&lt;/p&gt;

&lt;p&gt;Hooks write logs to a temporary directory (&lt;code&gt;.local/eocc/logs&lt;/code&gt;), which the app monitors.&lt;/p&gt;

&lt;p&gt;Since Claude Code runs in a terminal, hooks can access terminal environment paths. This lets me grab tmux and npx paths from within hooks and pass them to the app.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mapping Hooks to Status
&lt;/h3&gt;

&lt;p&gt;Claude Code provides these hook events:&lt;br&gt;
&lt;a href="https://code.claude.com/docs/hooks-guide" rel="noopener noreferrer"&gt;https://code.claude.com/docs/hooks-guide&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's how I map them to session states:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;th&gt;Usage&lt;/th&gt;
&lt;th&gt;Session State&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;session_start&lt;/code&gt; (startup/resume)&lt;/td&gt;
&lt;td&gt;Start a session&lt;/td&gt;
&lt;td&gt;Active&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;session_end&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;End a session&lt;/td&gt;
&lt;td&gt;Remove session&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;notification&lt;/code&gt; (permission_prompt)&lt;/td&gt;
&lt;td&gt;Waiting for approval&lt;/td&gt;
&lt;td&gt;WaitingPermission&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;notification&lt;/code&gt; (idle_prompt)&lt;/td&gt;
&lt;td&gt;Waiting for input&lt;/td&gt;
&lt;td&gt;WaitingInput&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;stop&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Response completed&lt;/td&gt;
&lt;td&gt;Completed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;post_tool_use&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;After tool execution&lt;/td&gt;
&lt;td&gt;Active&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;user_prompt_submit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Prompt submitted&lt;/td&gt;
&lt;td&gt;Active&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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

&lt;p&gt;I've only been using Claude Code as my primary tool for about two months, and I expect my workflow will keep evolving.&lt;/p&gt;

&lt;p&gt;Thanks to AI, I can quickly build and adapt tools like this—which is exactly what makes this era so exciting.&lt;/p&gt;

&lt;p&gt;If your workflow is similar to mine, give it a try! I'd love to hear your feedback.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/joe-re/eyes-on-claude-code" rel="noopener noreferrer"&gt;https://github.com/joe-re/eyes-on-claude-code&lt;/a&gt;&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>tauri</category>
      <category>productivity</category>
      <category>tmux</category>
    </item>
  </channel>
</rss>
