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. 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 header directive arguments. They follow the format $name or $name;. An expansion variable is replaced by the:

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

  2. JWT private claims from the incoming request when enabled

  3. named field argument

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 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.

Arguments

arguments: [StepZen_Argument!]

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

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

configuration: String

The configuration argument specifies the configuration entry by name from the configurationset. Configuration values provide a hidden database of values that can be used in endpoint, postbody, and headers.

Values for configuration keys name and id (optional) 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.

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.

arguments provides a renaming mechanism.

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

  • transformREST: modifies the HTTP response body before the response is converted into the annotated field's GraphQL type

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

endpoint: String!

The endpoint argument defines the rest URI to call.

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 query string generation will append all non-null arguments except those which have already been used in the endpoint path or in a header. If the endpoint URI has a query string, then only the non-null nullable arguments will be appended.

Query string parameters may be renamed by using arguments below.

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.

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.

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.

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.

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.

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.

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: "..."},
           ...]

Locations

Type System: FIELD_DEFINITION

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.