Virtual Connectors

Expose GraphQL fields that compute their value without calling a REST API


A virtual connector is an @connect directive that omits the http: argument. The field's value is computed entirely from the selection, drawing on field arguments, schema configuration, environment variables, and literal values. No outbound HTTP request happens at runtime.

This is a preview feature. For information about version requirements and how to enable it in your router, go to Preview Features.

When to reach for a virtual connector

A virtual connector fits whenever the value a field should return doesn't depend on a remote system:

  • Mock data during development. Return a fixed shape so the client team can build UI before the backing API exists.

  • Derived fields. Compose a string, look up an enum, or normalize an argument without involving a network round trip.

  • Static configuration exposure. Surface an environment variable or @source-level setting through the graph.

  • Type-safe constants. Map an argument to a concrete __typename or a fixed payload, exercising the same mapping language as HTTP-backed connectors.

For anything that needs data from a REST API, use a regular HTTP-backed connector. Virtual connectors are a complement, not a replacement.

Defining a virtual connector

Omit http: from @connect. Keep selection: and anything else the field needs:

GraphQL
schema.graphql
1extend schema
2  @link(
3    url: "https://specs.apollo.dev/connect/v0.4",
4    import: ["@source", "@connect"]
5  )
6
7type Query {
8  greeting(name: String!): String!
9    @connect(
10      selection: """
11      ["Hello", $args.name]->joinNotNull(", ")
12      """
13    )
14}

This connector has no source, no URL, and no HTTP method. At runtime the router evaluates the selection against the bound variables and returns the result.

A virtual connector can declare source: to inherit @source-level configuration like errors: or isSuccess:, but it doesn't gain a transport that way. The convention in most schemas is to drop source: along with http: so the field reads as a pure mapping.

Paste JSON, get a working connector

In connect/v0.4 the mapping language is a strict superset of JSON. Any JSON value—an object, an array, a primitive—is already a valid selection. Pair that with a virtual connector and you can take a sample payload from your API team, paste it into selection:, and you have a working stub:

GraphQL
schema.graphql
1type Query {
2  product: Product
3    @connect(
4      selection: """
5      {
6        "id": "p1",
7        "title": "Dune",
8        "price": 18.0,
9        "author": { "name": "Frank Herbert" }
10      }
11      """
12    )
13}

That literal JSON parses as a selection that produces the same object verbatim. No reshaping, no escaping, no $root paths—the selection is the response.

Anywhere the mapping language goes beyond JSON, it borrows extensions familiar from modern JavaScript: single-quoted strings, trailing commas, object-property shorthand ({ id } for { id: id }), the ... spread operator. So you can paste pure JSON to get started and edit toward something more expressive without rewriting from scratch:

GraphQL
schema.graphql
1type Query {
2  productPreview(id: ID!): ProductPreview
3    @connect(
4      selection: """
5      {
6        "id": $args.id,
7        "title": "Dune",
8        "price": 18.0
9      }
10      """
11    )
12}

The shape is still JSON, but the id field draws from the field's argument at runtime instead of a hard-coded string. Each value position can be any mapping expression—a literal, an argument, an ->arrow chain, or a nested selection.

What the selection can read

A virtual connector's selection has access to the same input-side variables as any other connector:

  • $args — the GraphQL field arguments.

  • $this — the parent object, when the field is on a non-Query type.

  • $config — the values you've configured under the router's connectors.sources.<name>.$config block.

  • $env — environment variables exposed to the router.

  • $context — request-scoped context values populated by router plugins.

  • Literal values and the full mapping method library.

Use them the same way you would in any other @connect selection:

GraphQL
schema.graphql
1type Query {
2  appVersion: String! @connect(selection: "$env.APP_VERSION")
3
4  fullName(first: String!, last: String!): String!
5    @connect(
6      selection: """
7      [$args.first, $args.last]->joinNotNull(" ")
8      """
9    )
10}

What the selection can't read

A virtual connector has no HTTP exchange, so the response-phase variables are unbound. Composition rejects a schema whose virtual-connector selection references any of them:

  • $root — the response body. There is no body; the router synthesizes the empty object {} so paths from $root resolve to nothing.

  • $status — the HTTP status code. There is no response, so no status.

  • $response — the response headers. Same reason.

$root covers more than the literal $root token. Inside a selection, the bare $ and any path that starts with a field name (id, user.name, $.results) are all reading from $root—that's the implicit base for response-shaped data. The validator catches all of these, not only explicit $root references.

The composer runs static analysis on every virtual-connector selection and emits a diagnostic for each request-phase namespace whose consumption subtree is non-empty. The message names the namespace and the field, so you don't have to guess which reference triggered the error.

In practice, virtual-connector selections build every output value from an input variable ($args, $config, $env, $context, $this) or a literal injected with $(...). If you need to start a path from the response body, you want a regular HTTP-backed connector instead.

Example: a typed lookup

A common shape is a virtual connector that uses ->match to map an argument to a fixed payload, including a __typename. This pattern works well for fixture data while the real API is being built:

GraphQL
schema.graphql
1type Query {
2  productPreview(id: ID!): ProductPreview
3    @connect(
4      selection: """
5      $args.id->match(
6        ["p1", { __typename: "ProductPreview", id: "p1", title: "Dune",    price: 18.0 }],
7        ["p2", { __typename: "ProductPreview", id: "p2", title: "Arrival", price: 14.0 }],
8        [@,    null]
9      )
10      """
11    )
12}
13
14type ProductPreview {
15  id: ID!
16  title: String!
17  price: Float!
18}

The catch-all branch [@, null] returns null for any unknown id, which the field's nullable return type permits. See Abstract Types for the full pattern, including unions and interfaces.

Limitations

  • Virtual connectors require connect/v0.4. Enable it with the preview_connect_v0_4 setting in your router config. See Preview Features for the full setup.

  • Virtual connectors omit http: and other transport-shaped arguments. A connector with batch: but no http: doesn't currently parse as a virtual connector.

  • Selections can't consume response-phase data ($root, $status, $response). The validator emits one error per offending namespace.

If you encounter a case the preceding patterns don't cover, tell us about it. Preview features evolve based on this feedback.