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.

For information about version requirements and how to enable connect spec v0.4 in your instance of Router, go to Version Requirements.

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. To learn how to set it up, go to Version Requirements.

  • 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 those patterns don't cover, contact us.