<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Graphlinkdev on Medium]]></title>
        <description><![CDATA[Stories by Graphlinkdev on Medium]]></description>
        <link>https://medium.com/@graphlinkdev?source=rss-43b9254a2251------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*8a-UyE4vstZmnZzvUz7tsg.png</url>
            <title>Stories by Graphlinkdev on Medium</title>
            <link>https://medium.com/@graphlinkdev?source=rss-43b9254a2251------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Fri, 13 Nov 2026 21:38:35 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@graphlinkdev/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[Start With the REST API You Have: A Typed Client Today, GraphQL Later]]></title>
            <link>https://medium.com/@graphlinkdev/start-with-the-rest-api-you-have-a-typed-client-today-graphql-later-b5c0477e6be3?source=rss-43b9254a2251------2</link>
            <guid isPermaLink="false">https://medium.com/p/b5c0477e6be3</guid>
            <category><![CDATA[rest-api]]></category>
            <category><![CDATA[graphql]]></category>
            <category><![CDATA[graphlink]]></category>
            <dc:creator><![CDATA[Graphlinkdev]]></dc:creator>
            <pubDate>Tue, 10 Nov 2026 00:00:39 GMT</pubDate>
            <atom:updated>2026-11-10T01:04:33.167Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*Dz91YohlJyBCmaGM.png" /></figure><p>Most teams that want GraphQL already have a REST API. They cannot switch in a weekend, and they should not have to. So the realistic plan is: keep calling REST, and move to GraphQL as the server catches up. GraphLink 6.0 is built for that plan.</p><p><strong>TL;DR</strong></p><ul><li>Describe a REST endpoint in a schema file, run glink, and get a typed client in Dart, Java, TypeScript, Kotlin, Swift, Python or Go.</li><li>Cache REST calls on the client with @glCache, using the same store and tags as GraphQL.</li><li>Migrate a field to GraphQL by removing one directive. The call site does not change.</li></ul><h3>The problem</h3><p>A hand-written REST client is a pile of small chores. A model class for every response. A serializer for every model. A function per endpoint that builds the path, the query string and the body, sends them, checks the status, and decodes the result. Then the same chores in the mobile app, the web app and the backend service, in three languages.</p><p>When the API later moves to GraphQL, all of that gets rewritten, because REST calls and GraphQL calls look nothing alike.</p><h3>One schema file</h3><p>GraphLink describes a REST endpoint with a directive on a field of Query or Mutation:</p><p>@glRestPrefix shares a path across a block, and each argument gets a role: here the id fills the {id} placeholder and @glRestBody is the JSON body. Other roles cover query strings, headers, form fields and multipart parts.</p><p>A schema with only REST operations needs no GraphQL URL and no GraphQL adapter. You do not need a GraphQL server to try this.</p><h3>Using the generated code</h3><p>Run glink, and the client has one method per field:</p><p>The same schema generates the equivalent in TypeScript, Kotlin, Java, Swift, Python and Go. A non-2xx status throws a RestError with the status, headers and body, so a server error is never silently turned into null.</p><h3>A cache that is not an HTTP cache</h3><p>Now the part that REST clients rarely have. Add the cache policy to the schema:</p><p>Within two minutes, a second product(&quot;p1&quot;) is answered from the client&#39;s store without a request. A successful createProduct evicts everything tagged products. The policy comes from your schema, not from Cache-Control headers, so it works even when the API sends none.</p><p>It is the same store and the same tag space that GraphQL operations use. A GraphQL mutation can invalidate a tag held by a REST query, and the reverse. That matters for the next step.</p><p>This is a per-operation cache with tags and an offline fallback ( staleIfOffline, for transport failures only). It is not a normalized entity cache.</p><h3>What happens under the hood</h3><p>The generated client never touches the network by itself. It builds a request (method, path, query, headers, body) and calls one function you provide, or a generated adapter for package:http, Dio, fetch, axios, OkHttp, URLSession, httpx or net/http. The response is decoded into the generated types. There is no runtime dependency on GraphLink.</p><h3>Migrating, one field at a time</h3><p>The day the GraphQL server can resolve product, remove the directive from that one field:</p><p>The type, the method and the call are the same. Only the transport changed. Every other field keeps working over REST until its turn, and the shared cache keeps @glCache and @glCacheInvalidate working while you move.</p><p>If today’s REST API omits fields that the schema declares non-null, mark the operation @glRestNullable. The client then decodes into a copy of the type ( ProductRest) where every field is nullable, so calls do not fail while the API catches up.</p><h3>Limitations</h3><ul><li>REST operations exist in the Dart, Java, TypeScript, Kotlin, Swift, Python and Go clients.</li><li>Subscriptions are always GraphQL.</li><li>Only GET queries are cached.</li><li>If you used @glRestNullable, the result type goes back from ProductRest to Product when the field moves to GraphQL, with no automatic conversion.</li><li>The cache is per operation and arguments, not normalized by entity.</li></ul><h3>Try it</h3><ul><li>Why REST: <a href="https://graphlink.dev/why-rest.html">https://graphlink.dev/why-rest.html</a></li><li>REST operations reference: <a href="https://graphlink.dev/rest-client.html">https://graphlink.dev/rest-client.html</a></li><li>Install: <a href="https://graphlink.dev/getting-started.html">https://graphlink.dev/getting-started.html</a></li></ul><p><em>Originally published at </em><a href="https://graphlink.dev/blog/rest-first-graphql-later.html"><em>https://graphlink.dev</em></a><em> on November 10, 2026.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=b5c0477e6be3" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[GraphLink 6.0: REST, Python, Go and a Stricter Generator]]></title>
            <link>https://medium.com/@graphlinkdev/graphlink-6-0-rest-python-go-and-a-stricter-generator-b3be379127d5?source=rss-43b9254a2251------2</link>
            <guid isPermaLink="false">https://medium.com/p/b3be379127d5</guid>
            <category><![CDATA[graphql]]></category>
            <category><![CDATA[python]]></category>
            <category><![CDATA[golang]]></category>
            <category><![CDATA[rest-api]]></category>
            <category><![CDATA[graphlink]]></category>
            <dc:creator><![CDATA[Graphlinkdev]]></dc:creator>
            <pubDate>Tue, 10 Nov 2026 00:00:01 GMT</pubDate>
            <atom:updated>2026-11-13T12:40:26.540Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*2Zx0YsgKAZN6GUtK.png" /></figure><p>GraphLink 6.0 adds a Python and a Go client, serves REST operations through the same generated client in all seven languages, converts custom scalars with codecs, and fails loudly where it used to guess. Some of that is breaking, so the migration notes are at the end.</p><h3>Seven clients</h3><p>Dart, Java, Kotlin, TypeScript, Swift, Python and Go all generate from the same schema. The two new ones follow the idioms of their language:</p><ul><li><strong>Python</strong>: pydantic v2 models, an asyncio client with keyword-only arguments, AsyncIterator subscriptions, and httpx and websockets adapters.</li><li><strong>Go</strong>: plain structs with json tags in one package, a context-aware client whose methods return (value, error), channel subscriptions, and net/http and coder/websocket adapters. Go 1.22 or later.</li></ul><p>Both have caching with tag invalidation, file uploads, @glCaptureErrors and persisted queries.</p><h3>REST operations</h3><p>Annotate a field of Query or Mutation with @glRest and the generated client calls it over HTTP/JSON instead of GraphQL.</p><p>The call site is the same as for a GraphQL field. To move an API from REST to GraphQL one field at a time, remove the directive. Arguments get explicit roles (@glRestPath, @glRestParam, @glRestHeader, @glRestBody, @glRestField, @glRestPart), responses can be JSON, text, Void or a streamed Download, and @glCache works on REST operations too. @glRestNullable decodes into a copy of the response type where every field is nullable, for APIs that omit fields the schema declares non-null. See <a href="https://graphlink.dev/rest-client.html">REST operations</a>.</p><h3>Scalar codecs</h3><p>A typeMappings entry can now be an object, so a custom scalar is converted when it is decoded and encoded:</p><p>{} stands for the value. The codec applies to fields, list elements, input fields and operation arguments, and jsonType makes the generated decoder check the JSON type before your fromJson runs.</p><h3>A stricter generator</h3><ul><li><strong>Directives are validated.</strong> Every directive must be built in or declared with directive @name ... on .... Unknown or misspelled arguments, wrong value kinds and wrong locations fail generation with the line and column. glink --print-directives prints the declarations of GraphLink&#39;s own directives for your IDE.</li><li><strong>Bad responses fail clearly.</strong> A response with no data and no errors, or a non-2xx status without a GraphQL body, now fails with the client&#39;s error type instead of returning null or crashing.</li><li><strong>Uploads stream</strong> in every client instead of being built in memory, with exact progress and chunked transfer when the length is unknown.</li></ul><h3>Migrating from 5.x</h3><ol><li><strong>Declare your custom directives</strong> (federation’s @key, your own @auth) and fix the typos the new errors point to.</li><li><strong>Regenerate</strong> and review the diff. Projected interface and union names, and the _all_fields fragments of types in a cycle, can change.</li><li><strong>Swift:</strong> fromJson of every type that holds an interface or union is now throws; call it with try.</li><li><strong>Java and Kotlin:</strong> DefaultGraphLinkClientAdapter takes a decoder. The GraphLinkClient factory methods are unchanged.</li><li><strong>Python and Go:</strong> a missing non-null field is now an error that names the type and the field.</li></ol><p>The full list is in the <a href="https://github.com/Oualitsen/graphlink/blob/main/CHANGELOG.md">changelog</a>.</p><h3>Try it</h3><p><em>Originally published at </em><a href="https://graphlink.dev/blog/graphlink-6-0-release.html"><em>https://graphlink.dev</em></a><em> on November 10, 2026.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=b3be379127d5" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Custom GraphQL Queries in Dart: Select Only the Fields You Need]]></title>
            <link>https://medium.com/@graphlinkdev/custom-graphql-queries-in-dart-select-only-the-fields-you-need-8a9cf12887f5?source=rss-43b9254a2251------2</link>
            <guid isPermaLink="false">https://medium.com/p/8a9cf12887f5</guid>
            <category><![CDATA[graphql]]></category>
            <category><![CDATA[flutter]]></category>
            <category><![CDATA[graphlink]]></category>
            <category><![CDATA[dartlang]]></category>
            <category><![CDATA[dart]]></category>
            <dc:creator><![CDATA[Graphlinkdev]]></dc:creator>
            <pubDate>Mon, 02 Nov 2026 00:00:50 GMT</pubDate>
            <atom:updated>2026-11-02T02:43:35.743Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*toMcvrRyDd6GQsNi.png" /></figure><p>GraphQL promises that you can ask for exactly what you want and no more. In the last video the generated client fetched every book together with its author. That is fine until you only need the books.</p><p>This post covers the next step: write your own query, let GraphLink generate the method for it, and see what the generated Dart looks like. The video shows all of it, and the sections below follow the same order.</p><p><a href="https://youtu.be/gHGtCBYaj7c">Watch on YouTube</a></p><h3>The query</h3><p>The schema is still the bookstore from the quickstart: authors and books, where a Book has an author. We want the books without their authors. Create a second file next to the schema, queries.graphql:</p><p>The name is long on purpose. It comes from Java habits: it is verbose, but when you read it you know what you are going to get.</p><p>If you run glink now, nothing new appears in the client. The generator only knows about the files listed in the config, so add the query file next to the schema:</p><p>I forgot this step in the video and had to debug it, so it is worth remembering. Run the command again:</p><h3>The generated method</h3><p>The client now has a new method, next to the ones generated from the schema, with the same name as your query:</p><p>In the video this prints the first book’s title, <em>The Dispossessed</em>. Everything is typed: there is no map and no cast.</p><h3>No dollar signs</h3><p>Look at the generated names. There are no dollar signs in the functions or the types. Some GraphQL code generators add them, and I never understood why. GraphLink doesn’t, because if your app already talks to a GraphQL server you shouldn’t have to give up names like Author and Book for generated code. The names should be as natural as possible.</p><h3>A class for the projection</h3><p>Open the response and you will find a new class named after the type and the fields you picked: Book_IdTitleYear. It has id, title and year as final fields, a constructor with named required parameters, and its own toJson and fromJson.</p><p>Why a new class and not the existing Book? Because Book has an author, and the schema says it is never null. If the query didn&#39;t select it and GraphLink reused Book, the only honest choice would be to make every field nullable, and you would be checking for null on values you know are there. With a dedicated class, a field that isn&#39;t null in the schema is still not null in your code. That is also why this works for languages with null safety: Dart, Kotlin and TypeScript (TypeScript handles it a bit differently).</p><h3>Try it</h3><p>The rest of the Dart series covers fragments, batching, caching and subscriptions.</p><p><em>Originally published at </em><a href="https://graphlink.dev/blog/graphlink-dart-custom-queries.html"><em>https://graphlink.dev</em></a><em> on November 2, 2026.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=8a9cf12887f5" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Generate a Typed GraphQL Client for Dart in One Command]]></title>
            <link>https://medium.com/@graphlinkdev/generate-a-typed-graphql-client-for-dart-in-one-command-f53f561018e8?source=rss-43b9254a2251------2</link>
            <guid isPermaLink="false">https://medium.com/p/f53f561018e8</guid>
            <category><![CDATA[flutter]]></category>
            <category><![CDATA[graphql]]></category>
            <category><![CDATA[dartlang]]></category>
            <category><![CDATA[graphlink]]></category>
            <category><![CDATA[graphql-codegen]]></category>
            <dc:creator><![CDATA[Graphlinkdev]]></dc:creator>
            <pubDate>Thu, 29 Oct 2026 00:00:12 GMT</pubDate>
            <atom:updated>2026-10-31T08:18:44.861Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*GCGXsBnBOelsxTIQ.png" /></figure><p>A GraphQL schema is fully typed. Then you write the Dart client by hand, and the types stop at the network boundary: Map&lt;String, dynamic&gt;, string keys, casts, and a query string you hope matches the server.</p><p>GraphLink skips that part. It reads your schema and writes the client for you, with models, inputs, queries and mutations, all typed and null-safe. This post is the quickstart: one schema, one command, and a first query against a real server. The video shows all of it, and the sections below follow the same order.</p><p><a href="https://youtu.be/78LYiuWLKIU">Watch on YouTube</a></p><h3>The schema</h3><p>The example is a small bookstore: authors and books, two queries and one mutation. It is deliberately simple.</p><h3>One config file, one command</h3><p>GraphLink needs a small config file that points at the schema and says where the output goes:</p><p>packageName has to match the name in your pubspec.yaml. Then you run the CLI:</p><p>In the video it reports 30 files generated in about 10 milliseconds. Installation instructions for glink are in the docs at <a href="https://graphlink.dev">graphlink.dev</a>.</p><h3>What you get</h3><p>Under lib/generated there are a few folders ( client, enums, inputs, interfaces and types) and one file, graphlink.dart, which is the only thing you import.</p><p>Take the input type from the schema. The generated Dart class has typed fields, a constructor with named parameters, and toJson and fromJson. That means you don&#39;t need json_serializable, and you can remove it from your dependencies.</p><p>It is null-safe because the schema is. If you make a field optional in the schema, for example name on Author, run glink again and the Dart field and the constructor parameter become optional too. The compiler then points at every place in your code that needs to change.</p><p>Look at pubspec.yaml and you will find only http. GraphLink is not a dependency. It is a code generator, so the output is plain Dart, and if you delete GraphLink tomorrow your code still compiles.</p><h3>The adapter and the first query</h3><p>The generated client needs an adapter. An adapter is a function that takes the payload and returns a Future&lt;String&gt; with the server&#39;s response. That is the only way GraphLink talks to your server, so it doesn&#39;t care how the data travels. For HTTP, GraphLink generates one for you:</p><p>The query for book(id: ID!) has a required, named id parameter, and book.title and book.author.name are typed values, not map lookups. In the video the server is a small Spring Boot app, and fetching book 1 and then book 2 returns the right titles and IDs.</p><h3>No magic</h3><p>The last part of the video opens the generated query. The client declares the query and a map of fragments, one “all fields” fragment per type, and sends only the fragments the query needs. For Book that means its own fields plus the fragment for Author. All of it is ordinary Dart that you can read.</p><h3>Try it</h3><p>The rest of the Dart series covers custom queries, fragments, batching, caching and subscriptions.</p><p><em>Originally published at </em><a href="https://graphlink.dev/blog/graphlink-dart-quickstart.html"><em>https://graphlink.dev</em></a><em> on October 29, 2026.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=f53f561018e8" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[The Pains of GraphQL Clients, and How to Remove Them]]></title>
            <link>https://medium.com/@graphlinkdev/the-pains-of-graphql-clients-and-how-to-remove-them-d812c7a31246?source=rss-43b9254a2251------2</link>
            <guid isPermaLink="false">https://medium.com/p/d812c7a31246</guid>
            <category><![CDATA[flutter]]></category>
            <category><![CDATA[kotlin]]></category>
            <category><![CDATA[graphql]]></category>
            <category><![CDATA[graphlinq]]></category>
            <category><![CDATA[java]]></category>
            <dc:creator><![CDATA[Graphlinkdev]]></dc:creator>
            <pubDate>Sun, 25 Oct 2026 00:00:08 GMT</pubDate>
            <atom:updated>2026-10-25T15:57:29.001Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*t8LdFq2Si3g2_wIX.png" /></figure><p>GraphQL is a good deal on paper. You get a typed schema, one endpoint, and clients that ask for exactly the data they need. Then you write the client, and the typed schema stops at the network boundary.</p><p>This post walks through the six pains I kept running into when building GraphQL clients, and what happens to each of them when the client code is generated from the schema instead of written by hand. Each section links to a short video that shows it in action.</p><h3>The idea: your schema is the source of truth</h3><p>GraphLink is a compiler. You write your GraphQL schema once. GraphLink reads it and generates a typed, null-safe client in your language: Dart, Java, Kotlin, TypeScript or Swift.</p><p>Two things it is worth being clear about up front:</p><ul><li><strong>It is a code generator, not a runtime library.</strong> The output is plain code in your language. Delete GraphLink tomorrow and your code still compiles.</li><li><strong>The schema is the only thing you maintain.</strong> Change the schema, run the generator again, and the compiler tells you what broke.</li></ul><p>Watch the introduction: <a href="https://youtu.be/W3kYySbsz0k">https://youtu.be/W3kYySbsz0k</a></p><p>Now the pains.</p><h3>Pain 1: Raw maps and no compiler help</h3><p>With a plain HTTP client, a GraphQL response is JSON. You decode it into a Map&lt;String, dynamic&gt; and dig out the values yourself:</p><p>Dart’s dynamic makes this look easy, which is part of the problem. Rename a field on the server and this still compiles. You find out when a user opens the screen. In Java or Kotlin you would also have to cast at every step, which is more code and the same risk.</p><p>With a client generated from the schema, the same call looks like this:</p><p>books is a list of Book. title is a String, year is an int, and book.author.name autocompletes. No casts, no maps, no string keys. Nullability from the schema carries through, so a field declared non-null is non-null in your code.</p><p>Watch: <a href="https://youtu.be/G2QVInjpA-A">https://youtu.be/G2QVInjpA-A</a></p><h3>Pain 2: Subscriptions</h3><p>Subscriptions are the part of GraphQL that most clients make you build yourself. By hand it goes like this:</p><ol><li>Open a WebSocket to the server.</li><li>Send a connection_init payload, with a bearer token if you need authentication.</li><li>Send the subscription itself.</li><li>Decode every raw message and branch on its type: connection_ack, ping (and reply with pong), next, error, complete.</li><li>Handle dropped connections and server restarts, with backoff and a reconnect that re-sends the init and subscribe messages.</li></ol><p>That is a small protocol implementation, and it has to be right every time.</p><p>GraphLink generates this boilerplate for each subscription in your schema. In Dart you call a method on the generated subscriptions object and get a Stream back, then listen to it like any other stream. TypeScript works the same way. Java supports both reactive and blocking styles, and in the blocking one you pass a listener interface.</p><p>Watch: <a href="https://youtu.be/PuU7B25OJC8">https://youtu.be/PuU7B25OJC8</a></p><h3>Pain 3: Caching</h3><p>Caching is meant to stop the client from asking the server for something it was just told. Doing it by hand has two traps:</p><ul><li><strong>Partial responses overwrite complete ones.</strong> You fetch a book with its title and ISBN and cache it. Elsewhere in the app, another query fetches the same book with only the ISBN, and it replaces the fuller entry.</li><li><strong>The logic is yours to test.</strong> Cache keys, expiry, and invalidation after mutations all need tests, and mocks for those tests.</li></ul><p>GraphLink moves caching into the schema, which stays the source of truth. You mark a query with @glCache, giving it a time to live and a set of tags. To clear entries before they expire, you mark a mutation with @glCacheInvalidate and the tags it should invalidate:</p><p>When addBook succeeds, the generated code evicts everything tagged books. The heavy lifting is generated. What you write is a couple of lines in the schema.</p><p>Watch: <a href="https://youtu.be/2Ojjk259hcI">https://youtu.be/2Ojjk259hcI</a></p><h3>Pain 4: Batching meets caching</h3><p>Batching is a real GraphQL feature: ask for a book and an author in one query, in one HTTP request. The problem appears when you combine it with a cache. Most caches store the whole response as a single entry. Later, when you ask for only the book, the cache misses, even though the book is sitting right there inside the batched response.</p><p>GraphLink does not cache the whole response as one entry. It splits the response into separate entries. Here is the sequence from the video:</p><ol><li>The app loads a book by ID. That is a miss, so the request goes to the server.</li><li>The app sends a batched request for the book and an author. The book comes from the cache, and only the author goes to the server.</li><li>A mutation adds a book, which evicts the book cache entries. The next batched request hits for the author and fetches only the book.</li></ol><p>Each request sends only what the cache does not already have.</p><p>Watch: <a href="https://youtu.be/fVzvVfBl0IU">https://youtu.be/fVzvVfBl0IU</a></p><h3>Pain 5: File uploads</h3><p>A word of caution first: uploading files through GraphQL is not recommended in general. GraphQL is text-based, and a separate REST service for uploads is often the better design. But plenty of servers do accept uploads through GraphQL, so a client should handle it.</p><p>By hand, it means following the GraphQL multipart request convention. The variable that holds the file has to be null in the query payload. You add an operations part and a map part, both JSON, that describe which form field belongs to which variable. You read the file from the device, attach it, send the request, and then decode the response as raw data again.</p><p>In GraphLink you declare a custom scalar and mark it with @glUpload:</p><p>Any mutation that uses that scalar gets the multipart transport, and the generated client method takes a generated GLUpload type. In Java, for example, you create one from a File, a byte array, or an InputStream. The multipart request is generated for you.</p><p>Watch: <a href="https://youtu.be/u5Jbknx7R9g">https://youtu.be/u5Jbknx7R9g</a></p><h3>Pain 6: Error handling</h3><p>In GraphQL, an HTTP 200 does not mean success. A response can carry a normal status code and still contain a non-empty errors list, such as not authorized. A hand-written client has to check the status code, check the errors list, and do it consistently on every call. That is boilerplate that spreads through the code base.</p><p>GraphLink gives you two modes:</p><ul><li><strong>Default:</strong> if the response contains errors, the call throws a GraphLink exception that carries the list of errors. Either the call worked or it threw.</li><li><strong>Capture mode:</strong> you mark a query or mutation with @glCaptureErrors, and the call returns the result with data and errors side by side instead of throwing. You check response.hasErrors yourself.</li></ul><p>Capture mode earns its place with batched queries and partial failures. Suppose you ask for the list of books and the list of authors in one query, and you are allowed to see the books but not the authors. The server returns data with the books and a null for the authors, plus an errors entry explaining the authors failure. With capture mode, you keep the books and still see the error, instead of losing everything to an exception.</p><p>Watch: <a href="https://youtu.be/ySqkQ6ssSgk">https://youtu.be/ySqkQ6ssSgk</a></p><h3>Why I built it</h3><p>I wanted to use GraphQL with a Flutter app. On the server side I liked how Spring Boot’s GraphQL support handled data: I did not need to write a lot of DTOs. On the client side I found an existing generator and was happy at first. Two things got in the way. It returned streams where I expected futures, and it produced names with dollar signs that I did not like.</p><p>The bigger issue was how it talked to the backend. At the time it sent the whole schema’s worth of definitions, including fragments that the query did not use. A Java GraphQL server rejects a query with unused fragments, so it failed against my backend. I am not naming the library. The authors did good work, and it may work well with other backends. It did not work with mine, and a Java server will refuse anything it considers unnecessary.</p><p>That is where one of GraphLink’s design rules comes from: send the minimum the backend needs. GraphLink decides which query is sent to the server.</p><p>Two other design points follow from that:</p><ul><li><strong>Framework-agnostic transport.</strong> You provide the function that sends the request. In Dart that is a function that takes a string and returns a Future&lt;String&gt;. That makes it easy to mock in tests, and you do not need to test the generated code itself.</li><li><strong>Server scaffolding.</strong> GraphLink can also generate server code (Spring Boot and Express/Apollo), where you implement a few interfaces and the rest is done.</li></ul><p>Watch the recap: <a href="https://youtu.be/W9MwmBmtIeA">https://youtu.be/W9MwmBmtIeA</a></p><h3>Honest limits</h3><ul><li>You still need a GraphQL server. GraphLink generates the client, and optional server scaffolding, not your business logic.</li><li>Generated code is overwritten every time you run the generator. Keep your own code outside the generated folder.</li></ul><h3>Recap</h3><p>One schema, typed and null-safe clients. Subscriptions, caching, batching, uploads and error handling are generated, not hand-written. Because the output is plain code, you can leave whenever you want.</p><h3>Try it</h3><ul><li>Full video playlist: <a href="https://youtube.com/playlist?list=PLQCBiENjF_uU">https://youtube.com/playlist?list=PLQCBiENjF_uU</a></li><li>YouTube channel: <a href="https://www.youtube.com/@Graphlinkdev">https://www.youtube.com/@Graphlinkdev</a></li><li>Docs and quickstart: <a href="https://graphlink.dev">https://graphlink.dev</a></li><li>GraphLink on GitHub: <a href="https://github.com/Oualitsen/graphlink">https://github.com/Oualitsen/graphlink</a> (a star helps a lot)</li><li>Code from the videos: <a href="https://github.com/Oualitsen/graphlink-demos">https://github.com/Oualitsen/graphlink-demos</a></li><li>Community on Reddit: <a href="https://www.reddit.com/r/graphlinkdotdev/">https://www.reddit.com/r/graphlinkdotdev/</a></li><li>Follow on X: <a href="https://x.com/graphlinkdev">https://x.com/graphlinkdev</a></li></ul><p>If you hit a problem, open an issue on GitHub. Tell me which pain bites you the most, and I will cover it in the next video.</p><p><em>Originally published at </em><a href="https://graphlink.dev/blog/graphql-client-pains.html"><em>https://graphlink.dev</em></a><em> on October 25, 2026.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=d812c7a31246" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>