{"slug": "alert-5-things-even-ai-can-t-do-graphql", "title": "(Alert!)5 Things Even AI Can't Do, GraphQL", "summary": "GraphQL, created at Facebook in 2012 and now governed by the GraphQL Foundation, is a query language and runtime that allows clients to request exactly the data they need from a single endpoint, solving over-fetching and under-fetching issues common in REST APIs. It is transport- and storage-agnostic, enabling resolvers to pull data from various sources like PostgreSQL, REST microservices, or gRPC. The guide covers schema definitions, resolvers, the N+1 problem, federation, and security for production use.", "body_md": "NEWS: MY GAME JUST LAUNCHED\n\nIf 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 *different* shape of the same data, and six months later you are maintaining `/v3/users/:id/full`\n\nnext to `/v2/users/:id/summary`\n\nand nobody remembers which one the Android app actually calls.\n\n**GraphQL was built to kill that exact pain.** 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.\n\nThis 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.\n\nGraphQL 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 **GraphQL Foundation** 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.\n\nThat last point trips people up, so let it sink in: **GraphQL is transport-agnostic and storage-agnostic.** 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.\n\nThe mental model is simple. You describe your data as a **graph of types**. Clients write **queries** that traverse that graph. The server **resolves** each requested field by running a function. The response mirrors the shape of the query exactly.\n\nHere is the canonical hello-world. A query:\n\n```\nquery {\n  user(id: \"4\") {\n    name\n    email\n    posts(last: 3) {\n      title\n    }\n  }\n}\n```\n\nAnd the response:\n\n```\n{\n  \"data\": {\n    \"user\": {\n      \"name\": \"Mary Watson\",\n      \"email\": \"mary@example.com\",\n      \"posts\": [\n        { \"title\": \"On the Nature of APIs\" },\n        { \"title\": \"Why I Stopped Versioning\" },\n        { \"title\": \"Cursors, Not Offsets\" }\n      ]\n    }\n  }\n}\n```\n\nNotice 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 *and* their posts in a single round trip, even though those are almost certainly two different tables or services on the backend.\n\nTo 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.\n\n**Over-fetching.** A REST endpoint returns a fixed payload. `GET /users/4`\n\nmight 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.\n\n**Under-fetching and the N+1 round trip.** 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 `fetch`\n\ncalls. GraphQL collapses that into one request because the client describes the entire tree it wants up front.\n\n**Endpoint proliferation and versioning.** REST tends to grow an endpoint per view. A new screen needs a slightly different shape, so you add a route, or a `?include=`\n\nparameter, or a `?fields=`\n\nfilter, 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. **Most GraphQL APIs never version at all**; they evolve additively and deprecate fields with metadata rather than spinning up `/v2`\n\n.\n\nThe 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.\n\nEverything in GraphQL starts with the **schema**. The schema is a contract written in the **Schema Definition Language (SDL)** 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.\n\nLet's build out a small blog API to make the type system concrete.\n\n```\ntype User {\n  id: ID!\n  name: String!\n  email: String\n  role: Role!\n  posts: [Post!]!\n}\n\ntype Post {\n  id: ID!\n  title: String!\n  body: String!\n  published: Boolean!\n  author: User!\n  comments: [Comment!]!\n}\n\ntype Comment {\n  id: ID!\n  text: String!\n  author: User!\n}\n\nenum Role {\n  ADMIN\n  EDITOR\n  READER\n}\n```\n\nA few things to unpack here, because the syntax is dense with meaning.\n\n**Scalars** are the leaf values. GraphQL ships with five built-in scalars: `Int`\n\n, `Float`\n\n, `String`\n\n, `Boolean`\n\n, and `ID`\n\n. `ID`\n\nis a string under the hood but signals \"this is a unique identifier\" to tooling. You can and should define **custom scalars** like `DateTime`\n\n, `Email`\n\n, or `URL`\n\nto add validation and semantic meaning.\n\n**The exclamation mark means non-null.** `String!`\n\nis a string that will never be null. `[Post!]!`\n\nis a non-null list of non-null posts — the list itself is always present (possibly empty), and no element inside it is ever null. `[Post]`\n\nwould 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.\n\n**Object types** like `User`\n\nand `Post`\n\nare the nodes of your graph, and the fields that point to other object types are the edges. `User.posts`\n\nconnects a user to its posts; `Post.author`\n\nconnects back. That bidirectional linking is exactly why it is called a *graph*.\n\nBeyond objects and scalars, the type system has a few more tools you will reach for constantly.\n\n**Enums** restrict a field to a fixed set of values, as `Role`\n\nshows above. **Input types** describe the structured arguments you pass into mutations — they look like object types but use the `input`\n\nkeyword and cannot have fields that resolve to object types. **Interfaces** define a set of fields that multiple types must implement. **Unions** say a field can return one of several types that need not share any fields.\n\nHere is an interface and a union in practice:\n\n```\ninterface Node {\n  id: ID!\n}\n\ntype Image implements Node {\n  id: ID!\n  url: String!\n  altText: String\n}\n\ntype Video implements Node {\n  id: ID!\n  url: String!\n  durationSeconds: Int!\n}\n\nunion SearchResult = User | Post | Image\n\ninput CreatePostInput {\n  title: String!\n  body: String!\n  published: Boolean = false\n}\n```\n\nThe `Node`\n\ninterface is the foundation of the **Relay** specification for global object identification — any type implementing `Node`\n\ncan be refetched by its global `id`\n\n. Unions are perfect for search results or activity feeds where heterogeneous types share a list. Note the default value `published: Boolean = false`\n\nin the input — defaults are first-class in SDL.\n\nFinally, the three special **root operation types** are the entry points into the entire graph:\n\n```\ntype Query {\n  user(id: ID!): User\n  posts(published: Boolean): [Post!]!\n  search(term: String!): [SearchResult!]!\n}\n\ntype Mutation {\n  createPost(input: CreatePostInput!): Post!\n  deletePost(id: ID!): Boolean!\n}\n\ntype Subscription {\n  postPublished: Post!\n}\n```\n\n`Query`\n\nis for reads, `Mutation`\n\nis for writes, and `Subscription`\n\nis 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.\n\nA query selects fields starting from the `Query`\n\nroot and walking down the graph. The shape you write is the shape you get back.\n\n```\nquery GetDashboard {\n  posts(published: true) {\n    id\n    title\n    author {\n      name\n    }\n    comments {\n      text\n    }\n  }\n}\n```\n\n**Arguments** like `published: true`\n\ncan appear on any field, not just the root. You could ask for `comments(last: 5)`\n\ndeep 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.\n\nFor anything beyond a hardcoded example you want **variables** rather than string interpolation. Variables keep your query static (which matters for caching and persisted queries) and let the client pass values separately:\n\n```\nquery GetUser($id: ID!, $postCount: Int = 10) {\n  user(id: $id) {\n    name\n    posts(last: $postCount) {\n      title\n    }\n  }\n}\n{ \"id\": \"4\", \"postCount\": 3 }\n```\n\nVariables are declared in the operation signature with their types and optional defaults, then referenced with `$`\n\n. Never build queries with string concatenation — it is the GraphQL equivalent of SQL injection waiting to happen, and it defeats caching.\n\n**Aliases** let you request the same field twice with different arguments, which would otherwise collide in the response object:\n\n```\nquery {\n  recent: posts(last: 5) { title }\n  popular: posts(orderBy: VIEWS, last: 5) { title }\n}\n```\n\n**Fragments** are reusable selection sets. They keep queries DRY and, more importantly, are the mechanism that powers component-colocated data requirements in modern clients:\n\n```\nfragment PostCard on Post {\n  id\n  title\n  author { name }\n}\n\nquery {\n  posts(published: true) {\n    ...PostCard\n  }\n}\n```\n\nAt GraphQLConf 2025, fragments were a recurring theme, with the community converging on the idea that **fragments are primarily for describing a UI component's data dependencies, not just for reuse**. A `<PostCard />`\n\nReact 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.\n\n**Directives** modify execution. The two built into the spec are `@include`\n\nand `@skip`\n\n, which conditionally add or remove fields:\n\n```\nquery GetUser($id: ID!, $withPosts: Boolean!) {\n  user(id: $id) {\n    name\n    posts @include(if: $withPosts) {\n      title\n    }\n  }\n}\n```\n\nThere are also incremental-delivery directives like `@defer`\n\nand `@stream`\n\nfor 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 `@async`\n\ndirective precisely because `@defer`\n\ncarries hidden overhead, so the streaming story is still actively evolving.\n\nMutations look like queries but live under the `Mutation`\n\nroot and signal intent to write. Critically, **top-level mutation fields execute serially**, 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.\n\n```\nmutation PublishPost($input: CreatePostInput!) {\n  createPost(input: $input) {\n    id\n    title\n    published\n  }\n}\n```\n\nA mutation returns data just like a query — and you should lean into that. **Return the modified object (and anything else that changed) so the client can update its cache without a refetch.** A common best practice is to return a dedicated payload type that wraps the result alongside metadata and typed errors:\n\n```\ntype CreatePostPayload {\n  post: Post\n  errors: [UserError!]!\n}\n\ntype UserError {\n  field: String\n  message: String!\n}\n```\n\nThis pattern — modeling *expected* errors (validation failures, business-rule violations) as part of the schema rather than throwing them into the top-level `errors`\n\narray — 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).\n\nSubscriptions 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.\n\n```\nsubscription OnPostPublished {\n  postPublished {\n    id\n    title\n    author { name }\n  }\n}\n```\n\nUnder the hood subscriptions usually run over WebSockets (via the `graphql-ws`\n\nprotocol) 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 `@defer`\n\napproach is the pragmatic starting point.\n\nThe schema describes *what* is possible. **Resolvers** are the functions that actually produce the data for each field. This is where GraphQL meets your real backend.\n\nA resolver is a function that receives four arguments, conventionally `(parent, args, context, info)`\n\n:\n\n`root`\n\nor `source`\n\n).Here is a minimal server using **Apollo Server**, the most popular Node.js implementation, wired to the blog schema:\n\n``` js\nimport { ApolloServer } from \"@apollo/server\";\nimport { startStandaloneServer } from \"@apollo/server/standalone\";\n\nconst typeDefs = `#graphql\n  type User {\n    id: ID!\n    name: String!\n    posts: [Post!]!\n  }\n  type Post {\n    id: ID!\n    title: String!\n    author: User!\n  }\n  type Query {\n    user(id: ID!): User\n    posts: [Post!]!\n  }\n`;\n\nconst resolvers = {\n  Query: {\n    user: (parent, args, context) => context.db.getUser(args.id),\n    posts: (parent, args, context) => context.db.getPosts(),\n  },\n  User: {\n    posts: (parent, args, context) => context.db.getPostsByAuthor(parent.id),\n  },\n  Post: {\n    author: (parent, args, context) => context.db.getUser(parent.authorId),\n  },\n};\n\nconst server = new ApolloServer({ typeDefs, resolvers });\n\nconst { url } = await startStandaloneServer(server, {\n  context: async ({ req }) => ({\n    db: database,\n    user: await authenticate(req.headers.authorization),\n  }),\n  listen: { port: 4000 },\n});\n\nconsole.log(`Server ready at ${url}`);\n```\n\nThe key insight: **resolvers are nested and lazy.** When a query asks for `user.posts.author`\n\n, GraphQL first runs `Query.user`\n\n, passes that result as `parent`\n\ninto `User.posts`\n\n, then for each post passes *it* as `parent`\n\ninto `Post.author`\n\n. Fields you do not request never run their resolvers. You did not write a single `JOIN`\n\n— the execution engine walked the graph for you.\n\nIf a field's value can simply be read off the parent object (like `User.name`\n\nfrom a row that already has a `name`\n\ncolumn), you do not even need to write a resolver. GraphQL provides a **default resolver** that returns `parent[fieldName]`\n\n. You only write explicit resolvers for fields that require computation, a database hit, or a call to another service.\n\nUnderstanding what the server does with an incoming operation demystifies a lot of behavior and performance characteristics. Every request goes through three phases.\n\n**Parsing.** The raw query string is tokenized and turned into an Abstract Syntax Tree (AST). Syntax errors are caught here before anything else runs.\n\n**Validation.** 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 — *before* 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.\n\n**Execution.** 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.\n\nThis pipeline is also where you hook in cross-cutting concerns. **Validation rules** 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.\n\nNow the most important performance pitfall in GraphQL, the one that bites every team eventually.\n\nRecall the nested resolver model. Consider this query:\n\n```\nquery {\n  posts {       # 1 query: fetch 50 posts\n    title\n    author {    # runs once PER post: 50 more queries!\n      name\n    }\n  }\n}\n```\n\nThe `posts`\n\nresolver runs once and returns fifty posts. Then GraphQL runs the `Post.author`\n\nresolver *once for each of those fifty posts*. If each invocation does `SELECT * FROM users WHERE id = ?`\n\n, you have just fired **51 database queries** to render one list. This is the **N+1 problem**, and naive GraphQL servers are exceptionally prone to it because the nested resolver model makes it so easy to write.\n\nThe standard solution is **batching and caching per request**, implemented by the `DataLoader`\n\nlibrary (originally from Facebook). A DataLoader collects all the individual `.load(id)`\n\ncalls that happen within a single tick of the event loop, then dispatches them as one batched request:\n\n``` python\nimport DataLoader from \"dataloader\";\n\nfunction createUserLoader(db) {\n  return new DataLoader(async (userIds) => {\n    // userIds = ['1', '7', '7', '12', ...] collected across all 50 author resolvers\n    const users = await db.getUsersByIds(userIds);\n    const byId = new Map(users.map((u) => [u.id, u]));\n    // must return results in the SAME ORDER as the input keys\n    return userIds.map((id) => byId.get(id));\n  });\n}\n\n// create a fresh loader per request, in context\nconst server = new ApolloServer({ typeDefs, resolvers });\nstartStandaloneServer(server, {\n  context: async () => ({\n    userLoader: createUserLoader(database),\n  }),\n});\n\n// resolver now uses the loader\nconst resolvers = {\n  Post: {\n    author: (post, args, context) => context.userLoader.load(post.authorId),\n  },\n};\n```\n\nNow those fifty `author`\n\nresolvers each call `userLoader.load(authorId)`\n\n, DataLoader batches them into a *single* `WHERE id IN (...)`\n\nquery, and the per-request cache means duplicate IDs are deduplicated for free. Fifty-one queries become two.\n\n**Two rules to remember:** 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.\n\nNeither wins universally. Here is how they actually stack up.\n\n**GraphQL's advantages.** 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.\n\n**GraphQL's costs.** HTTP caching is harder — since everything is usually a `POST`\n\nto one URL, you lose the free CDN and browser caching that REST's distinct `GET`\n\nURLs 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 *must* add query-cost protections that REST gets somewhat for free.\n\n**When REST is the better call.** 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.\n\n**When GraphQL shines.** 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.\n\nMany 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.\n\nOffset pagination (`?page=2&limit=20`\n\n) 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 **cursor-based pagination** via the **Relay Connections** specification.\n\n```\ntype PostConnection {\n  edges: [PostEdge!]!\n  pageInfo: PageInfo!\n}\n\ntype PostEdge {\n  cursor: String!\n  node: Post!\n}\n\ntype PageInfo {\n  hasNextPage: Boolean!\n  hasPreviousPage: Boolean!\n  startCursor: String\n  endCursor: String\n}\n\ntype Query {\n  posts(first: Int, after: String, last: Int, before: String): PostConnection!\n}\n```\n\nA cursor is an opaque pointer to a position in the list (often a base64-encoded ID or timestamp). The client asks for `first: 10, after: \"cursor123\"`\n\nand the server returns the next ten plus a new `endCursor`\n\n. Because cursors point at stable positions rather than numeric offsets, inserts and deletes elsewhere in the list do not corrupt pagination. The `edges`\n\n/`node`\n\nindirection looks verbose, but it gives you a clean place to hang edge-specific metadata (like `addedAt`\n\non a membership). Notably, the September 2025 conference introduced **relative cursors**, 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.\n\nGraphQL'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.\n\n**Depth limiting.** Reject queries nested beyond a sane threshold to stop pathological recursion like `posts { author { posts { author { posts ... }}}}`\n\n.\n\n**Query complexity / cost analysis.** Assign a cost to fields and reject queries whose total exceeds a budget. Apollo Federation standardized a `@cost(weight: Int!)`\n\ndirective and a `@listSize`\n\ndirective to inform this analysis, so the gateway can score a query before executing it.\n\n**Rate limiting.** Per-client limits, ideally weighted by query cost rather than raw request count, since one GraphQL request can do the work of fifty.\n\n**Disable introspection in production** (or restrict it) so attackers cannot trivially map your entire schema, and turn off field suggestions that hint at valid field names.\n\n**Persisted (trusted) documents.** 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: **everyone serious is now using persisted/trusted documents**, 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.\n\n**Authentication and authorization** belong in resolvers and `context`\n\n, not in the schema's existence. Authenticate the request once (populate `context.user`\n\n), then authorize per field or per resolver. Field-level auth — checking that the current user may see *this specific* field on *this specific* object — is more granular than REST's typical endpoint-level checks, which is both a feature and more code to get right.\n\nA single monolithic schema works until many teams need to own different parts of it. **Federation** lets you split one unified graph across multiple independently deployed services (**subgraphs**), composed by a **gateway** or **router** into a single graph the client sees.\n\nEach subgraph owns its types and can extend types owned by others using directives like `@key`\n\n:\n\n```\n# Users subgraph\ntype User @key(fields: \"id\") {\n  id: ID!\n  name: String!\n}\n\n# Reviews subgraph — extends User without owning it\ntype User @key(fields: \"id\") {\n  id: ID!\n  reviews: [Review!]!\n}\n\ntype Review {\n  id: ID!\n  body: String!\n  author: User!\n}\n```\n\nThe 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 `User`\n\ntype with both `name`\n\nand `reviews`\n\n. Apollo Federation popularized this, and it has matured fast: recent Federation versions added directives like `@cost`\n\n, `@listSize`\n\n, and `@cacheTag`\n\nfor response caching, and there is now a vendor-neutral **Composite Schema Specification** 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.\n\nFederation 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.\n\nYou can consume GraphQL with nothing but `fetch`\n\n— it is just a `POST`\n\nwith a JSON body:\n\n``` js\nconst res = await fetch(\"https://api.example.com/graphql\", {\n  method: \"POST\",\n  headers: { \"Content-Type\": \"application/json\" },\n  body: JSON.stringify({\n    query: `query($id: ID!) { user(id: $id) { name } }`,\n    variables: { id: \"4\" },\n  }),\n});\nconst { data, errors } = await res.json();\n```\n\nFor real apps you want a client that handles caching, normalization, and request deduplication. **Apollo Client** 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. **urql** is lighter and more composable. **Relay** is the most opinionated and most powerful at scale, built around fragments and the `Node`\n\ninterface, and is what Meta runs. Newer entrants like **Houdini** (Svelte-first) and **Isograph** push the component-data-colocation idea even further.\n\n``` js\nimport { useQuery, gql } from \"@apollo/client\";\n\nconst GET_USER = gql`\n  query GetUser($id: ID!) {\n    user(id: $id) {\n      name\n      posts { title }\n    }\n  }\n`;\n\nfunction Profile({ id }) {\n  const { loading, error, data } = useQuery(GET_USER, { variables: { id } });\n  if (loading) return <Spinner />;\n  if (error) return <Error message={error.message} />;\n  return <h1>{data.user.name}</h1>;\n}\n```\n\nThe single biggest client-side productivity win is **code generation**. 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.\n\nA quick tour of what you will actually touch.\n\n**GraphiQL** and the embedded explorers in **Apollo Studio / GraphOS** give you an interactive, autocompleting query IDE backed by introspection — point it at any endpoint and start exploring. **Schema registries** track your schema over time and run composition and breaking-change checks in CI. The new **Schema Coordinates** 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.\n\nServer libraries exist for essentially every language: Apollo Server, GraphQL Yoga, and Mercurius in JavaScript; **graphql-java** and **DGS** in Java; **Strawberry** and **Graphene** in Python; **gqlgen** in Go; **async-graphql** and **Juniper** in Rust; **Hot Chocolate** in .NET. Whatever your backend, there is a mature, idiomatic option.\n\nOne genuinely new development worth flagging: the September 2025 spec added **descriptions on executable documents** (queries, mutations, fragments — not just schema types). This sounds minor but it directly enabled GraphQL operations to be exposed as **MCP tools for AI agents** 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.\n\nA condensed field guide from teams who have shipped this in anger.\n\n**Design the schema for the client, not the database.** Your schema is a product surface, not an ORM dump. Model the domain the way consumers think about it. Do not expose a `users_posts_join`\n\ntable as a type.\n\n**Always solve N+1 from day one** with DataLoader or your library's equivalent. It is not premature optimization; it is the default failure mode.\n\n**Model expected errors as typed schema results**, reserve the top-level `errors`\n\narray for genuine exceptions, and never leak stack traces or internal messages to clients.\n\n**Use cursor-based pagination** for any list that can grow, and adopt the Relay Connections shape so clients and tooling have a consistent contract.\n\n**Lock down public graphs** with depth limits, cost analysis, persisted documents, and disabled introspection in production. Do this before launch, not after the incident.\n\n**Evolve additively and deprecate with @deprecated** rather than versioning. Watch field usage in your registry before removing anything.\n\n**Adopt fragment colocation and codegen.** Let components declare their own data needs and let generated types keep frontend and backend honest. This is where the developer-experience payoff compounds.\n\n**Do not reach for federation or subscriptions prematurely.** 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.\n\nGraphQL 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 (`@oneOf`\n\n), schema coordinates, and operation descriptions. The ecosystem around it — Apollo, Relay, urql, federation routers in Rust, codegen, schema registries — is deep and production-hardened.\n\nReach for GraphQL when you have **diverse clients with varied data needs**, when you are **aggregating multiple services** behind one interface, when **round-trip cost and over-fetching genuinely hurt**, or when you want **end-to-end type safety** 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.\n\nIf you do adopt it, internalize three things and you will avoid most of the pain: **the schema is the contract, resolvers are where performance lives, and a public graph must be defended.** 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.\n\nNow 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.", "url": "https://wpnews.pro/news/alert-5-things-even-ai-can-t-do-graphql", "canonical_source": "https://dev.to/devunionx/alert5-things-even-ai-cant-do-graphql-1340", "published_at": "2026-06-17 21:19:59+00:00", "updated_at": "2026-06-17 21:51:44.820689+00:00", "lang": "en", "topics": ["developer-tools", "large-language-models"], "entities": ["GraphQL", "Facebook", "GraphQL Foundation", "Linux Foundation", "PostgreSQL", "REST", "gRPC", "Redis"], "alternates": {"html": "https://wpnews.pro/news/alert-5-things-even-ai-can-t-do-graphql", "markdown": "https://wpnews.pro/news/alert-5-things-even-ai-can-t-do-graphql.md", "text": "https://wpnews.pro/news/alert-5-things-even-ai-can-t-do-graphql.txt", "jsonld": "https://wpnews.pro/news/alert-5-things-even-ai-can-t-do-graphql.jsonld"}}