Apollo Connectors v0.4 is Generally Available

Ben Newman
Apollo Connectors let subgraph schemas turn REST or HTTP APIs into typed GraphQL with no resolver code and no subgraph server to run.
As teams bring more of their REST APIs onto the graph, those integrations need to reflect the data and patterns their applications use: different object types with different fields from a single endpoint, values derived from other fields, and transformations between API formats. Keeping this logic in a declarative schema makes it easier to build and maintain integrations as APIs evolve.
Today, Apollo Connectors v0.4 is generally available and no longer experimental. This release expands what teams can express directly in the schema, with support for abstract types (interfaces and unions), a larger set of inline -> transforms, connectors that return data without an HTTP request, and a unified grammar that brings GraphQL and JSON syntax together.
That work also provides a foundation for AI agents. The same REST APIs that client applications consume using GraphQL can also be exposed as tools through Apollo MCP Server, giving teams a shared API layer for both.
Let’s start with a feature that may seem small, but it provides a pretext for explaining a number of other features and problems that have been solved in connect/v0.4.
A connector with no request
Before this version, any time you used the @connect directive to annotate a GraphQL field, it demanded an http: {...} argument, assuming there must be an HTTP request involved.
What can you do with a @connect field if there’s no HTTP request? Here’s a working GraphQL field with no server, no resolver, and no API call behind it:
1type Query {2 featuredProduct: Product3 @connect(gragra4 selection: """5 {6 "id": "sku-4417",7 "name": "Aeron Chair",8 "price": { "amount": 1395, "currencyCode": "USD" }9 }10 """11 )12}We are calling this a virtual connector. The mapping produces the result data without consulting the network, using inputs (like $args and $this) that it has locally available. Query featuredProduct and you get the object.
The value of selection: can be a plain JSON object, with quoted keys and commas, just like the examples in an API’s documentation. That means you can paste an example response into your schema and get a queryable field to test with before you have credentials for the real API. This is by design: making sure JSON means the same thing in a selection as it does anywhere else was one of the biggest pieces of work in this release.
You’re not limited to JSON syntax, of course. You can take advantage of relaxations inspired by GraphQL and even JavaScript itself: keys don’t need quotes, and commas are optional as long as you drop all of them within a given object rather than some:
1{2 id: "sku-4417"3 name: "Aeron Chair"4 price: { amount: 1395 currencyCode: "USD" }5}If you’d rather try that than take our word for it, the mapping playground runs the same parser in your browser: paste a JSON value on one side and see what the mapping produces on the other.
Virtual connectors also support derived fields: a name assembled from two others, a vendor status code mapped onto your own enum, a URL built from an ID, a constant, or a lookup table. Those previously needed either a subgraph or an HTTP call to some REST service. Here are three of them on an entity, derived from data an ordinary connector already fetched:
1type User {2 id: ID!3 firstName: String4 lastName: String5 statusCode: String67 fullName: String8 @connect(selection: """9 [$this.firstName, $this.lastName]->joinNotNull(" ")10 """)1112 status: Status13 @connect(selection: """14 $this.statusCode->match(["A", "ACTIVE"], ["S", "SUSPENDED"], [@, "UNKNOWN"])15 """)1617 profileUrl: String18 @connect(selection: """19 ["https://example.com/users/", $this.id]->joinNotNull("")20 """)21}$this is the parent User object, so none of these fields makes a request of its own.
Why the grammar changed
Connectors work by annotating the schema with @source and @connect, and the mapping language does the rest. It’s the string you pass to selection:, and to http: for URLs, headers and request bodies.
Two of the language’s stated design principles are about how it reads: where it looks like a GraphQL selection set, it should behave like one, and where it looks like JSON, it should behave like JSON, to the point that you can paste any JSON value and have a valid mapping.
Before v0.4, the JSON principle held in most places, but not at the top level of a mapping or inside an object literal, which are exactly the places a virtual connector depends on:
- Object literals needed a
$(...)wrapper, except as->method arguments, where they didn’t. - Commas were required between properties in an object literal but rejected between field selections.
- The top level accepted only a list of field selections or a single path expression, and selecting a single field was ambiguous.
v0.4 removes those exceptions with four changes:
- Object literals and sub-selections are the same rule, so
{ ... }means the same thing wherever it appears. - Aliases and … spreads accept any expression.
__typename: "Book"no longer needs to be written as__typename: $("Book"). - Commas between properties are optional, but all-or-nothing. Use them throughout an object, JSON style, or leave them all out, GraphQL style.
{ a, b c }is a parse error. - The top level accepts any expression, such as an array or a string literal, not just field selections or a single path expression.
What happens when you get it wrong
A virtual connector has one new failure mode: reading data that doesn’t exist. There’s no request, so there’s no response body, no status code and no response headers.
Composition catches it:
`@connect(selection:)` on `Query.trace`: selection consumes `$response`
(the response headers) but the directive has no transport (no `http:`)
Details: $response { headers { trace } }The message names the coordinate, the variable you read from, what that variable would have held, and the rule. The Details line lists the paths that triggered it, and the reported source locations point at those paths inside the mapping string rather than at the directive containing it. Without the check, the field would be coerced to null at runtime and nothing would report why.
Static analysis
An error message with details like these requires a thorough understanding of the mapping code in question, and because you don’t want to find out at runtime when you could have found out before deploying, that understanding of the selection’s output shape must be available using purely static analysis. v0.4 extends this from outputs to inputs. For any selection, connectors can now work out both the shape of the data it produces (its structural type) and every input it reads to produce it. The Details: line in the error above comes from that input analysis: it lists the $response paths the mapping reads.
Static analysis is also critical for supporting abstract types, where conditional mapping turns polymorphic REST data, which has no standard way to say which type each object is, into GraphQL’s __typename convention:
1union Product = Book | Film23type Query {4 products: [Product]5 @connect(6 source: "products-api"7 http: { GET: "/products" }8 selection: """9 upc10 ... category->match(11 ["book", { __typename: "Book" title author { id } }],12 ["film", { __typename: "Film" title director { id } }],13 [@, null]14 )15 """16 )17}Imagine the API returns a flat list with a category field that can be "book" or "film" at runtime, and the mapping examines category with ->match, producing a static __typename in each arm. For the schema to compose, something has to work out that the selection yields one of two object shapes, __typename: "Book" with a title and an author, or __typename: "Film" with a title and a director, and check both against the union’s members. Interface and union return types are gated to v0.4 for that reason.
Before v0.4, that schema didn’t compose at all. A union or interface type in a connectors subgraph was rejected outright, with the message that abstract schema types aren’t supported when using connectors. The usual workaround was to stop typing that part of the response:
1scalar JSON23type Query {4 products: [JSON]5 @connect(6 source: "products-api"7 http: { GET: "/products" }8 selection: "$"9 )10}The data arrives intact. What you give up is the ability to use GraphQL query syntax to select against the product data (not even conditional fragments), since it’s now behind an opaque JSON scalar type.
You can write application code to unpack the data, but it won’t be validated automatically, codegen has nothing to work with, and query planning can’t help beyond either fetching the opaque blob, or not. Abstract types allow you to express the internal structure of polymorphic REST data so GraphQL can query it effectively.
The type system responsible for this static analysis is still in development and is considered an implementation detail for now, but we are planning to turn its errors into developer-facing warnings in the next version of connectors (v0.5), so developers can easily verify their selections satisfy their GraphQL schemas, without having to discover preventable problems in production.
Arrow methods
category->match(...) in that example is a method call. GraphQL has syntax for passing named arguments to query fields, to be consumed and interpreted by resolvers, but connectors don’t use resolvers, so it seemed misleading (to both humans and agents) to reuse familiar GraphQL syntax to mean something else.
That’s why input->method(arg1, arg2, ...) adopts a syntax that’s definitely not GraphQL, for behavior that more closely resembles inline transformations or the helper functions in a database query. Router 2.14 and 2.15 added several methods to the standard library, aimed at the small cleanup jobs REST responses tend to need:
->split(separator)splits a string into an array, like JavaScript’sString.prototype.split, with an optional limit.$.email->split("@")->lastpulls the domain out of an email address.->trim,->trimStartand->trimEndremove whitespace from both ends of a string, the start, or the end.->jsonParseturns a string containing JSON into a structured value, for APIs that return JSON inside JSON. It’s the inverse of the existing->jsonStringify.->keysToCamelCaseconverts an object’s top-level keys fromsnake_case,PascalCaseorSCREAMING_SNAKE_CASEto camelCase, so REST field names line up with GraphQL conventions.->keysToCamelCaseDeepdoes the same through nested objects, including objects inside arrays.
These join the existing methods for arithmetic, comparison, and collections (->map, ->filter, ->find, ->first, ->entries and others).
Each method declares how it transforms shape, so composition knows that $.email->split("@")->last produces a string and can check it against the type you declared. The methods themselves aren’t version-gated and work in older schemas; type checking through -> method chains is the part v0.4 adds.
Writing connectors with agents
Most of the above came from human developers telling us where the mapping language tripped them up: the $(...) wrapper they didn’t know they needed (and no longer do), the field that came back empty (and now has a reason), the interface or union they couldn’t handle.
Of course, developers increasingly delegate the writing of connector code to an agent, or ask an agent to maintain a connector someone else wrote. Agents make mistakes too, and they’re biased toward the languages they’ve been trained on. Given a mapping language that looks like JSON and GraphQL, an agent mixes JSON and GraphQL freely, including in places the older grammar didn’t accept. Neither humans nor agents are very good at remembering a list of exceptions to their intuitions.
So there’s a second reason for both the grammar changes and the new diagnostics. A syntax that always behaves the same as the syntaxes it resembles is one agents get right on the first try more often, with fewer misunderstandings (on both sides) of example JSON pasted in by humans who have no patience for translating their data into a specialized language. A diagnostic that names the coordinate, the rule, and the offending path is one an agent can act on directly.
As helpful as agents can be when it comes to authoring connector schemas, the benefits continue when it’s time to consume the data: whether agents are querying connectors you just built with standard GraphQL, or invoking them as tools through Apollo MCP Server, agents work with the same schema your applications use.
What’s coming in connect/v0.5
v0.5 is the next connectors version and will be Router v3 only. It’s where the type system starts reporting to developers rather than only to composition: warnings when a mapping doesn’t produce the type you declared, with schemas still composing, and an option to treat those warnings as errors once you’ve fixed or silenced the ones you can.
None of that is available to build on yet. It’s where the work described above is heading, and it needs Router v3 and Apollo Federation 2.16 or later.
Also in flight
Several connector improvements are landing in Router v2. Response caching for connectors is configured per source and driven by the upstream Cache-Control. Connector errors are reported to clients as structured data. And the connector request stage is available to Rust plugins, for request signing, custom routing and similar work. We’re doing this work in Router v2 deliberately, so you won’t have to wait for Router v3 to stabilize.
On Router v3, alongside connect/v0.5: reusable selection definitions, which allow defining your own -> methods for reuse in any path expression, plus a way of auto-generating selections for GraphQL object types, so you don’t have to type all those field names twice; and ->reduce, for iterative folds not served by ->map.
We’re also exploring how agents can help generate and maintain connector schemas through our service factory work. It generates connector-enabled services from existing APIs, including internal APIs and those without OpenAPI specifications, with an agent writing the first draft and preserving developers’ manual changes across future updates. We’ll share more about that work soon.
Upgrading to v0.4
v0.4 changes the meaning of one kind of mapping, and nothing else. That second half has been proved (with Lean), not just tested.
The one change: a primitive literal in a value position is now read as a literal rather than as a field lookup. currencyCode: "USD" used to look for a property named USD and produce nothing when there wasn’t one. In v0.4 it produces the string "USD".
Everything else carries over unchanged. Using the Lean proof assistant, we mechanically verified that every v0.3 mapping is accepted by v0.4 and evaluates identically, apart from that one parsing change, which covers a handful of literal forms.
To see how often the literal case comes up in practice, we dual-parsed 25,703 @connect selections drawn from 7,375 supergraphs in a corpus of customer schemas. 97.4% behave identically under both versions. Most of the remaining sites are ones where the v0.4 reading matches what the developer meant, so the “breaking” change tends to be a healing change. The exception is a quoted string used as the name of a REST field that isn’t a valid GraphQL identifier, such as nextLink: "@odata.nextLink", where the v0.3 lookup is the correct reading and the fix is to prefix the quoted field access with $., as in nextLink: $."@odata.nextLink".
connect-migrate (GitHub) handles the rest. It ships as a CLI plus an agent skill: the CLI does the analysis, and the skill carries the per-site semantic criteria for deciding what to do about each result. Because the divergent set is characterized exactly, this tool can find every affected site rather than pattern matching or attempting to duplicate parser behavior outside the connectors project.
To upgrade, in this order: get onto Router 2.16 or later; run connect-migrate analyze while your schemas still declare their current spec version (before you change the @link, not after), so it can identify any reasons the upgrade might not be safe; apply what it reports; re-run analyze to confirm nothing’s left; then change the @link to connect/v0.4:
1extend schema2 @link(3 url: "https://specs.apollo.dev/connect/v0.4"4 import: ["@connect", "@source"]5 )FAQs
Do I have to upgrade? No. Subgraphs linking connect/v0.2 or v0.3 keep working as they do today. Everything in this post needs v0.4, though.
Does anything change if I leave my @link alone? No. Every v0.4 behavior difference, including the literal-versus-lookup change, is gated on the spec version your subgraph declares, whichever router version runs it.
What about a REST field whose name isn’t a valid GraphQL identifier? That’s the one case where the v0.4 reading isn’t what you want. nextLink: "@odata.nextLink" becomes a constant string instead of a lookup, so connect-migrate rewrites it to $."@odata.nextLink".
Can I try v0.5 now? Not yet. Its type-system warnings are still being built, and it will ship with Router v3.
Where to go next
- Connectors documentation
- The mapping playground, for trying selections in a browser
- The Connectors changelog
- The mapping language reference, if you want the grammar itself
connect-migrate(GitHub)- Bugs, gaps and requests: github.com/apollographql/router/issues