directive @rest

The @rest directive specifies the REST endpoint for a Query or Mutation field. When the field is resolved, a request is made to the REST endpoint.

For a given API, it's not uncommon for API to require a number of similar @rest arguments for every call. You can define these as a schema level @rest(name) directive.

A field annotated with @rest can populate a deep tree from a REST response. For example, if a REST response contained a JSON object matching Customer and Address GraphQL types such as:

type Customer {
  id: ID!
  name: String
  address: Address
}

type Address {
  street: String
  city: String
  zip: String
}

then a Query field defined as:

extend type Query {
  customer(id:ID):Customer @rest(endpoint:"$url" configuration:"customers")
}

will resolve fields in Customer and Address.

@rest is optimized for JSON, and automatically maps the JSON to the return GraphQL type. You can customize the extraction process using resultroot and setters.

Expansion variables

Expansion variables refer to values that can be used within the endpoint, postbody, and headers directive arguments. They follow the format $name or $name;.

An expansion variable is replaced by the:

  1. named value in the configuration (referenced by @rest(configuration:) argument)

  2. JWT private claims from the incoming request when enabled

  3. named field argument

  4. injected expansion variables from @inject

These expansion variables are automatically into the configuration:

  • __host__ is replaced by the hostname of the incoming request, for example: alice.us-east-a.ibm.stepzen.net

  • __environment__ is replaced by the environment (account) name of the incoming request, for example: alice,

Expansion variables can be used in the endpoint in several ways:

  • As a query parameter value (e.g. email=$email) with automatic url encoding

  • As path element(s) (e.g. "https://api.a.org/users/$method/$id")

  • As the complete user name or password for basic authentication (e.g. "https://$user:$password@example.com")

  • As hostname segments or as a complete URL

  • Port cannot be a variable

Automatic generation

Automatic generation is provided for URL query strings and body content. This process uses the argument name as the key/parameter name and the argument value as the value. The name can be overridden using arguments. Automatic query strings can be disabled using the queryparameters argument.

Automatic generation supports only scalar arguments or arrays of scalars. GraphQL input type arguments are not processed.

An argument is considered non-null if any value (configuration, jwt, or argument) exists. This means, for example, a configuration can be used to establish a (hidden) default.

Automatic query string generation will append all non-null arguments except those which have already been used in the endpoint path or in a header or in an autogenerated request body (see contenttype for detailed rules on automated request body). If the endpoint URI has a query string, then only the non-null nullable arguments will be appended.

Arguments

arguments: [StepZen_ArgumentRename!]

arguments provides a renaming mechanism.

The arguments argument allows Graphql arguments to be renamed for automatic URL query string generation and automatic body generation.

Example: arguments:[{argument: "graphQLArg", name:"backingRestCall"}...] and with a value of graphQLArg:1 and type Int, then it would be rendered in the endpoint URI as ?backingRestCall=1 or in the (JSON) body as {"backingRestCall":1}.

Only complete GraphQL argument names are supported, expressions accessing input fields of GraphQL arguments are not supported.

cachepolicy: StepZen_CachePolicy

The cachepolicy argument defines the caching policy for the REST request. If applied to a Query field, the default is { strategy: DEFAULT }. And:

  • when method=GET the default is { strategy: ON }

  • when method=POST the default is { strategy: OFF }

  • when method=PATCH the default is { strategy: OFF } and you should not override it.

  • when method=PUT the default is { strategy: OFF } and you should not override it.

  • when method=DELETE the default is { strategy: OFF } and you should not override it. If applied to a field of Mutation the default is { strategy: OFF }.

cel: String

Deprecated: Modify REST responses with ecmascript or transforms arguments.

The cel argument is a string which expresses the extraction logic in the Content Extraction Language (CEL). If the cel parameter is provided, resultroot, setters, and filter will be ignored

cel is disallowed for schema level @rest

configuration: String

The configuration argument specifies the configuration entry by name from the configurationset.

Configuration values provide a private database of values that can be used in endpoint, postbody, and headers. However, values for configuration keys name (mandatory) and id are made available through the admin API and therefore it is recommended that these values do not contain secrets.

A convention is to use id to represent the REST api for usage in asset management and dependency tracking.

The stepzen.tls option for @rest is supported for legacy use only. Use @rest(tls:) to configure TLS instead.

Configuration keys with the prefix stepzen. are reserved.

contenttype: String

The contenttype argument specifies a Content-Type for the postbody. If postbody is provided, a Content-Type must be set via contenttype or headers.

If no postbody is set, the contenttype may trigger automatic body generation, for PATCH, POST and PUT, with all arguments except those that are null or have already been used in the endpoint path or in a header.

  • application/x-www-form-urlencoded generates form-encoded content with argument based key value pairs

  • application/json builds a JSON object with argument key value pairs. GraphQL types will be used.

ecmascript: String

The ecmascript argument contains ECMAScript code. ECMA 5.1 Scripts are supported. Limits:

  • JSON parse will fail on broken UTF-16 surrogate pairs

  • Date uses int instead of float per ECMAScript

  • WeakRef and FinalizationRegistry may result in unexpected memory usage

The ecmascript argument allows you to modify HTTP request and response bodies. You can implement one or both of the following function signatures to manipulate these bodies:

  • bodyPOST: modifies the HTTP request body before the request is made to the REST endpoint. The input string is the rendered postbody (or auto-generated body if not specified). The global headers object is available and mutable; changes to this object (including Content-Type) will be reflected in the outgoing HTTP request headers.

    • Header names are canonicalized (e.g., my-header becomes My-Header).

    • You can assign a string or an array of strings to a header key. Arrays will result in multiple header values (e.g., headers["X-Foo"] = ["a", "b"]).

    • If you set headers["Content-Type"], the outgoing request will use that value. If you delete it, the original value from the spec is used. If you set it to an empty string, the outgoing request will have an empty value for that header.

  • transformREST: modifies the HTTP response body before the response is converted into the annotated field's GraphQL type. The global headers object is available as a copy of the response headers, but changes are ignored.

Both functions can use the built-in get function to retrieve the annotated field's arguments.

Example: Mutating headers in bodyPOST

function bodyPOST(body) {
  headers["X-My-Header"] = "custom-value";
  headers["Authorization"] = "Bearer " + get("token");
  headers["X-Multi"] = ["a", "b"]; // Sends two X-Multi headers
  return body;
}

If using a profile, the profile ecmascript will be prefixed.

endpoint: String

The endpoint argument defines the rest URI to call.

path can be used to seperate out a (partial) path element.

Note: if endpoint resolves to null or the empty string, this will be flagged as an error instead of defaulting to another value

filter: String

When the JSON returns a list, the filter argument allows you to select specific JSON rows using a predicate defined based on the result field names.

forwardheaders: [String!]

The forwardheaders argument defines the list of headers forwarded from the incoming GraphQL HTTP request to the HTTP call made to endpoint. Nonexistent headers will forward as an empty header. Headers rendered via configuration entries take priority. If using a profile, the profile fowardHeaders will be prepended.

headers: [StepZen_RequestHeader!]

The headers argument specifies a list of headers to include in the request. All headers, including duplicates, will be added in order. Headers rendered via configuration entries and forwardheaders will appear first. The first Content-Type header will be accepted if contenttype is not set.

If using a profile, the profile headers will be prepended. Duplicate headers can occur.

method: StepZen_HTTPMethod

The method argument can be DELETE, GET, PATCH, POST, PUT. Default is GET. If postbody is present, then the default changes to POST. The selection of method affects other parameters.

name: String

name names this @rest as a profile.

All schema level @rest directives must be named. Field level @rest directives can not be named.

pagination: StepZen_RESTPagination

The pagination argument defines how pagination is handled by the REST API call. The annotated field's type must be a Connection type following the GraphQL pagination specification.

path: String

path specifies an additional path to be appended to endpoint

postbody: String

The postbody argument can be specified if the method is DELETE, PATCH, POST, or PUT. The content of postbody is treated as a golang template that is executed using the Getter before populating the body field of the request. postbody is ignored for GET requests.

Specifying postbody will disable automatic request body generation. This currently has the side-effect that automated query string generation will append any unused arguments. Use the queryparameter to STOP or POSTBODY_STOP

A profile specified postbody shall only apply to requests that normally have request bodies: PATCH, POST, or PUT so the profile can be shared with GET.

queryparameters: StepZen_QueryParameterPolicy

queryparameters define the handling of query parameters within rest where STANDARD auto-appends query parameters as defined. STOP stops any standard query parameter processing and equivalent to stepzen.queryextensionguard

resultroot: String

The resultroot argument defines the path in the returned JSON object where the parsing should start.

setters: [StepZen_FieldSetter!]

The setters argument defines how fields in the annotated field type should be set from the JSON result.

Sometimes the name or structure of the content returned by a REST API doesn't exactly match the GraphQL type that the REST request will populate. In such cases, you can use setters to map the values returned by a REST API response to the appropriate fields within the returned GraphQL type. Only fields that require remapping need to be specified; otherwise, appropriate defaults will be used.

To illustrate this concept, let's look at the following example JSON response:

 {
 "id": 194541,
 "title": "The Title",
 "slug": "the-url-2kgk",
 "published_at": "2019-10-24T13:52:17Z",
 "user": {
   "name": "Brian Rinaldi",
   "username": "remotesynth"
 }
}

If the corresponding Article GraphQL type has a field named published but not published_at, the published field will not be populated. To resolve this, you can use a setter as follows:

{ "field": "published", "path": "published_at" }

setters are also useful for extracting values from nested objects returned by a REST API.

For example, if Article has a field user: User then the value of user.name will map by default to the field Article.user.name.

If the requirement was instead to flatten the structure to set Article.author and Article.username then these instances of StepZen_FieldSetter would be required.

{ field: "author", path: "user.name" }
{ field: "username", path: "user.username" }

By using setters, you ensure that your GraphQL types accurately reflect the data returned by REST APIs, even when the API responses don't align perfectly with your GraphQL schema.

If using a profile, the profile setters will be prepended.

tls: String

The tls argument specifies a separate TLS configuration entry by name from a configurationset. The referenced TLS configuration can contain:

  • cert: Client certificate in PEM format for mutual TLS (mTLS) authentication

  • key: Private key for the client certificate in PEM format

  • ca: Root certificate chain in PEM format for custom CA validation Implementation is type specific. At present this is a placeholder as no type's currently support the tls argument.

transforms: [StepZen_Transform!]

The transforms argument is a sequence of transformations to apply to the fetched JSON payload before parsing and extracting the values of interest. The transformations will be applied in the sequence specified. The value of resultroot is not taken into consideration while processing tranformations.

transforms: [{pathpattern: "...", editor: "..."},
           {pathpattern: "...", editor: "..."},
           ...]

If using a profile, the profile transforms will be prepended.

use: String

The name argument defines a rest profile.

Profiles allow arguments from a @rest definition defined as a profile to be used as a template rest calls.

Example:


extend schema @rest(profile: "secure" tls: "tls")

type Query {
  postEndpoint: MyType @rest(endpoint: "https://api.example.com", profile: "secure")
}

Only applies to the schema level directive.

use specifies a previously named @rest profile. Arguments specified in this profile will be used in this @rest directive if not specified However, for headers, forwardheaders, transforms, ecmascript, and setters the values are combined with the profile arguments first.

Locations

repeatable

Type System: FIELD_DEFINITION, SCHEMA

Examples

GET

type Query {
  get(a: Int, b: String): Response @rest(endpoint: "https://httpbingo.org/get")
}
type Response {
  method: String
  origin: String
  url: String
  args: JSON
}

GraphQL field arguments are automatically added as URL query parameters.

POST

type Query {
  post(a: Int, b: String): Response
    @rest(endpoint: "https://httpbingo.org/post", method: POST)
}
type Response {
  method: String
  origin: String
  url: String
  json: JSON
}

GraphQL field arguments are automatically added as the POST body with content-type application/json.

Renaming fields in the response

To rename a field in the response, such as changing $id to id, use , use the following transforms argument:

transforms: [{pathpattern: ["item"], editor: "rename:$id,id"}]

The JSON will be transformed from:

{
  "item": {
    "$id": 23
  }
}

To:

{
  "item": {
    "id": 23
  }
}

Convert object to array format in response

If the backend response looks like:

{
 "data": {
   "2333": {
     "name": "John Doe",
     "age": 23
   },
   "3333": {
     "name": "Jane Smith",
     "age": 21
   }
 }
}

The objectToArray editor addresses this by converting the key-value pairs into an array format.

@rest (endpoint: "...", transforms: [
{pathpattern: ["data","<>*"], editor: "objectToArray"}
])

This converts the response into:

{
    "data": [
        {
        "name": "2333",
        "value": {
            "age": 23,
            "name": "John Doe"
        }
        },
        {
        "name": "3333",
        "value": {
            "age": 21,
            "name": "Jane Smith"
        }
        }
    ]
}

Delete field from a response

To delete a field from a response, such as removing the name field in one field while retaining it in another, use the drop editor. For example:

type Query {
  anonymous: [Customer]
    @rest(
      endpoint: "https://introspection.apis.stepzen.com/customers",
      transforms: [
        { pathpattern: ["[]", "name"], editor: "drop" }
      ]
    )
  known: [Customer]
    @rest(
      endpoint: "https://json2api-customers-zlwadjbovq-uc.a.run.app/customers"
    )
}

Manipulate JSON with jq

To restructure or manipulate JSON responses, use the jq editor with custom jq commands. For instance, to move the city field into a nested address object, apply the jq formula .[] | {name, address: {city: .city}} as follows:

transforms: [{pathpattern:[],editor:"jq:.[]|{name,address:{city:.city}}"}]

This modifies the original JSON:

[
  {
    "city": "Miami",
    "name": "John Doe"
  }
]

To the following:

{
  "data": {
    "customers": [
      {
        "name": "John Doe",
        "address": {
          "city": "Miami"
        }
      }
    ]
  }
}

Using ECMAScript to modify HTTP request and response

type Query {
  scriptExample(message: String!): JSON
  @rest(
    endpoint: "https://httpbin.org/anything",
    method: POST,
    ecmascript: """
      function bodyPOST(s) {
        let body = JSON.parse(s);
        body.ExtraMessage = get("message");
        return JSON.stringify(body);
      }

      function transformREST(s) {
        let response = JSON.parse(s);
        response.CustomMessage = get("message");
        return JSON.stringify(response);
      }
    """
  )
}

In this example, an HTTP POST request is sent to https://httpbin.org/anything. The request payload is modified by the bodyPOST function, adding an "ExtraMessage" field that contains the value of the message argument.

The transformREST function modifies the response, adding a "CustomMessage" field with the same message argument.

Testing with JSON return type

When using ecmascript for transformation, it's helpful to set the return type of the query to JSON initially. This makes it easier to test and debug before switching to a specific schema type.

type Query {
  scriptExample(message: String!): JSON
  @rest(
    endpoint: "https://httpbin.org/anything",
    method: POST,
    ecmascript: """
      function transformREST(s) {
        let response = JSON.parse(s);
        response.CustomMessage = get("message");
        return JSON.stringify(response);
      }
    """
  )
}

Simulating a REST response

You can use ecmascript to simulate a REST response by utilizing the stepzen:empty endpoint. This is useful for testing GraphQL fields without an actual REST call. Here's an example:

type Query {
  customers: JSON
  @rest(
    endpoint: "stepzen:empty",
    ecmascript: """
      function transformREST(s) {
        return JSON.stringify({
          "records": [
            { "name": "John Doe", "countryRegion": "US" },
            { "name": "Jane Smith", "countryRegion": "UK" }
          ]
        });
      }
    """
  )
}

Mapping nested data with resultroot

Let’s explore an example using the Contentful Delivery API response structure.

{
"fields": {
  "title": {
    "en-US": "Hello, World!"
  },
  "body": {
    "en-US": "Bacon is healthy!"
  }
},
"metadata": {
  // ...
},
"sys": {
  // ...
}
}

In this example, the fields object contains all the data required to populate a GraphQL type representing this content. Therefore, we set resultroot to fields to specify that as the path for the desired data:

contentfulPost(id: ID!): Post
    @rest(
      endpoint: "https://cdn.contentful.com/spaces/$spaceid/entries/$id"
      resultroot: "fields"
      configuration: "contentful_config"
    )

Note: Setting resultroot to fields means the data in other parts of the response, such as metadata and sys, will not be accessible.

Handling arrays in response

In some cases, the data you need is nested within an array of items in the API response. For example, the Contentful API might return multiple entries as follows:

{
  "sys": { "type": "Array" },
  "skip": 0,
  "limit": 100,
  "total": 1256,
  "items": [
    {
      "fields": {
        // fields for this entry
      }
    }
  ]
}

Here, the data resides inside the fields object within each item of the items array. To map this, you need to set resultroot to "items[].fields" to indicate that the data should be extracted from the fields object within the array items.

contentfulBlogs: [Blog]
    @rest(
      endpoint: "https://cdn.contentful.com/spaces/$spaceid/entries"
      resultroot: "items[].fields"
      configuration: "contentful_config"
    )

This ensures that the schema can correctly pull the relevant data from the array for each blog post entry.