How to Connect to a REST Service

How to use a REST API as a data source for your GraphQL API

You can use StepZen to connect your GraphQL API to a REST service. This enables you to send HTTP requests and data to that service's endpoints, and to receive and transform responses.

There are two broad ways in which you can connect to a REST service:

  1. Let StepZen introspect the REST service and create a GraphQL API for you. The API will reflect how the backend has exposed the data. This has been covered in Quick Start with a REST API documentation.

  2. Start with an application view of the API, and then use the @rest directive to connect it to the backend REST service. This allows you to have a curated application experience. We discuss this way of connecting to a REST service in this document.

Note: You can mix and match the steps -- get started quickly with introspection, and then customize and curate using the arguments in the @rest directive. In this section we will describe how to use the @rest directive.

We continue to enhance our REST manipulation capabilities. Hit us on our Discord if you would like to see something more or want more details on what we discuss below.

Basics

Any Query or Mutation field in your StepZen GraphQL schema can be annotated with the @rest directive. Briefly, the form is:

type EchoResponse {
  url: String
}
type Query {
  echo (number: Int, message: String): EchoResponse
  @rest (endpoint: "https://httpbin.org/anything")
}

This schema instructs StepZen to attempt to resolve the return type EchoResponse by making an HTTP call to the address given in the argument endpoint of the directive @rest when the field echo is selected in a query. There are several other arguments that allow you to make the right HTTP request and shape the response. We cover these below.

Building the HTTP Request

The only required argument to the @rest directive is endpoint. The endpoint argument specifies the URL for the HTTP request. By default, any field arguments (e.g. number and message in the example above) are automatically appended to the endpoint as the query string. The optional arguments for the @rest directive which can be used to build the request are as follows:

  • method: the HTTP method to use for the request. The default value is GET. Supported values are DELETE, GET, PATCH, POST, and PUT.
  • headers: the HTTP headers to send with the request. Variables are supported. See Directives Reference to learn more.
  • contenttype: the Content-Type header to send with the request (shortcut argument).
  • postbody: the HTTP message body for the request. Variables are supported. See Directives Reference to learn more.
  • arguments: specifies to rename the field arguments used in the request query string. See Directives Reference to learn more.
  • configuration: specifies a configuration entry from which variables, commonly secrets such as API keys, can be used in the request. See Directives Reference to learn more.
  • ecmascript: a pre-processing script which modifies the message body for the request. See Directives Reference to learn more.

This combination of arguments allows you to customize every aspect of the HTTP request StepZen uses to satisfy operation requirements.

Let's look at a more complete example of using the @rest directive to configure an HTTP request. Consider the following schema, which defines a single field that when selected in a query sends a request to the https://httpbin.org/anything endpoint:

type Query {
  anything(message: String): JSON
  @rest (endpoint: "https://httpbin.org/anything")
}

This is a fully-functional StepZen schema. The 'anything' field on the Query type returns the JSON scalar type, which makes it easy for us to focus on showing the effect of arguments used to build the HTTP Request.

Setting HTTP Headers

Let's add two headers to the request. The first header is the User-Agent header, and the second is the X-Api-Key header.

type Query {
  anything(message: String): JSON
  @rest (
    endpoint: "https://httpbin.org/anything"
    headers: [
      {name: "User-Agent", value: "StepZen"}
      {name: "X-Api-Key", value: "12345"}
    ]
    )
}

If you deploy this schema and request the anything field from a query operation, you will see your headers reflected in the response.

Note: Rather than hard-coding your API key in the schema, you can use the configuration argument to specify a configuration from which the API key can be retrieved. See Directives Reference to learn more.

Setting the HTTP Method

Now let's change this to a POST request, and see how the message argument is used in the request body. Edit the schema to add the method argument:

type Query {
  anything(message: String): JSON
  @rest (
    endpoint: "https://httpbin.org/anything"
    method: POST
    headers: [
      {name: "User-Agent", value: "StepZen"}
      {name: "X-Api-Key", value: "12345"}
    ]
    )
}

If you deploy this schema and request the anything field from a query operation, supplying "Hello World" for the message argument, you will see that StepZen has sent a POST request with the request body as json, like this:

{"message":"Hello World"}

Setting the HTTP Request Body

Instead of the default request body, let's specify a more complex template. Update the schema as follows, setting the postbody argument as shown:

type Query {
  anything(message: String): JSON
  @rest (
    endpoint: "https://httpbin.org/anything"
    method: POST
    headers: [
      {name: "User-Agent", value: "StepZen"}
      {name: "X-Api-Key", value: "12345"}
    ]
    postbody: """
      {
        "user": {
          "id": "1000",
          "name": "The User"
         }
      }
    """
    )
}

if you deploy this schema and request the anything field from a query operation, you will that StepZen has sent a POST request with the request body as json, like this:

{
  "user": {
    "id": "1000",
    "name": "The User"
  }
}

For more information on the postbody argument, see Directives Reference

Using Variables

Let's look at an example that uses variables in the endpoint and the postbody. In the endpoint argument, we use $ to indicate where to insert a variable. In the postbody argument, we use the GoLang template format to indicate where to insert variables. Update the schema as follows:

type Query {
  anything(id: ID, name: String, message: String): JSON
  @rest (
    endpoint: "https://httpbin.org/anything/users/$id"
    method: POST
    headers: [
      {name: "User-Agent", value: "StepZen"}
      {name: "X-Api-Key", value: "12345"}
    ]
    postbody: """
      {
        "user": {
          "name": "{{.Get "name"}}",
          "message": "{{.Get "message"}}"
         }
      }
    """
    )
}

Now deploy this schema and send a few queries to see how StepZen converts the arguments into portions of the HTTP request. Notice that the id argument is substituted into a path element in the HTTP request URL and the name and message arguments are substituted into the HTTP request body.

Learn More

We've taken a fairly simple HTTP GET request and converted it to a POST request with a custom path and request body. If necessary, even more complex requests can be created. For more information on the @rest directive arguments, see the Directives Reference

Shaping the HTTP Response

Often the data that comes back from a REST backend is not identical to the result type of the GraphQL operation. The @rest directive supports five additional arguments to help you shape the REST response to the requested GraphQL type.

  • resultroot: a path to extract from the JSON response to map to the operation type.
  • setters: mapping from JSON response values to the fields of the GraphQL result.
  • filter: condition for selecting rows from the JSON response. See Directives Reference to learn more.
  • transforms: editors for simplifying common and more complex transformations. See Directives Reference to learn more.
  • ecmascript: javascript for transforming a REST response to JSON or XML. See Directives Reference to learn more.

These five arguments allow you to convert any response from the HTTP request to match the GraphQL type of the operation.

Let's create a new Query type field using @rest and transform the HTTP response to match the field's type. We'll be using a sample REST API available at https://introspection.apis.stepzen.com/customers.

We'll start with the following schema:

type Customer {
  email: String
  id: ID
  name: String
}
type Query {
  customerById(id: ID!): Customer
  @rest(
    endpoint: "https://introspection.apis.stepzen.com/customers/$id"
  )
}

Using setters

The above schema works correctly, since the JSON response from the REST API specified by endpoint has fields that match the fields of Customer, which is the GraphQL object type of the query field customerById. However, if instead of name, the type Customer had the field fullName, we would need to make use of the setters argument for the @rest directive as follows:

type Customer {
  email: String
  id: ID
  fullName: String
}
type Query {
  customerById(id: ID!): Customer
  @rest(
    endpoint: "https://introspection.apis.stepzen.com/customers/$id"
    setters: [{field: "fullName", path: "name"}]
  )
}

You can see the effect of the setters by deploying this schema to StepZen and running a query, then commenting out the setters argument and running the query again. Without this argument, the fullName field will be null.

The setters argument can also be used to "pull-up" values from inside a nested JSON object to the fields in the GraphQL object. For example, the JSON response of the customers API includes an Address field that contains a countryRegion field. We can pull-up the countryRegion field from the JSON response to the countryRegion field of the GraphQL response object Customer as follows:

type Customer {
  email: String
  id: ID
  fullName: String
  countryRegion: String
}
type Query {
  customerById(id: ID!): Customer
  @rest(
    endpoint: "https://introspection.apis.stepzen.com/customers/$id"
    setters: [
      {field: "fullName", path: "name"}, 
      {field: "countryRegion", path: "address.countryRegion"}
    ]
  )
}

Using resultroot

REST APIs will often return more data than you wish to expose to your users. For example, our sample customers API returns customer order information along with customer address information. If we want to query only the order information for a customer, we can use the resultroot argument to specify how to exract only this information from the JSON response. Consider the following schema:

type Order {
  carrier: String
  customerId: ID
  id: ID
  trackingId: ID
}

type Query {
  customerOrders(customerId: ID!): [Order]
  @rest(
    endpoint: "https://introspection.apis.stepzen.com/customers/$customerId"
    resultroot: "orders[]"
  )
}

Here, setting the resultroot argument to "orders[]" instructs StepZen to resolve the customerOrders field to a list of Order objects from the JSON response. The "[]" at the end of "orders" informs StepZen to expect a list of objects.

Using transforms

Now that you are warmed up with setters and resultroot, lets take a deeper look into more advanced capabilities that can be used to shape responses to the desired GraphQL type. The transforms argument of @rest allows for some very common and sophisticated transformations of the responses. It takes the following form:

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

The list of objects of the form {pathpattern: "...", editor: "..."} must contain at least one object. If there is more than one object, the objects are processed in the order in which they appear, the output of one providing the input for the next.

Each pathpattern is a list of strings each specifying a part of the path. The resulting path pattern indicates which parts of the JSON value will be processed by the corresponding editor. See the Directives Reference for more information about the pathpattern and editor arguments.

Transform using jq

One of the available transform editors is jq. This editor supports transforming responses using formulas from the excellent jq tool, and takes the form:

transforms[{pathpattern: ["...","..."], editor: "jq:jq-expression"}]

Let's apply a jq editor transform to a field in the Query type of our customer schema. Using the same customers API as before, we will flatten the records returned from the API, and instead of returning details on each order, we will calculate the total shipping cost for the customer.

The StepZen schema for this example looks like:

type Customer {
  id: ID
  email: String
  name: String
  city: String
  countryRegion: String
  totalShippingCost: Float
}

type Query {
  customerShippingCost(id: ID!): Customer
  @rest (
    endpoint: "https://introspection.apis.stepzen.com/customers/$id"     
    transforms: [
      {
        pathpattern:[],
        editor:"jq:map({id,email,name,city:.address.city,countryRegion:.address.countryRegion,totalShippingCost:([.orders[].shippingCost]|add)})"
        }
    ]
  )
}

In this example, the jq editor is the only object of the transforms argument, so we expect that the HTTP response from the endpoint request will be a JSON type. Also in this example, the pathpattern is empty, indicating that the editor will be applied to the entire JSON response. The jq editor transforms this JSON by implicitly mapping the id, email, and name fields from the JSON to the like-named fields of the GraphQL Customer type. It extracts values for Customer's city and countryRegion fields from the JSON's address.city and address.countryRegion fields. Finally, it calculates the value for the Customer's totalshippingcost field by adding together shippingcosts of all the orders in the JSON. In total, this jq formula converts a JSON object from this:

[
  {
    "address": {
      "city": "Edinburgh",
      "countryRegion": "UK",
      "id": 4,
      "postalCode": "EH1 1DR",
      "stateProvince": "Midlothian",
      "street": "777 Highlands Dr"
    },
    "email": "jane.xiu@example.com",
    "id": 4,
    "name": "Jane Xiu",
    "orders": [
      {
        "carrier": "ups",
        "createdAt": "2020-08-05",
        "customerId": 4,
        "id": 1,
        "shippingCost": 3,
        "trackingId": "1Z881F5V0397269080"
      },
      {
        "carrier": "ups",
        "createdAt": "2020-08-02",
        "customerId": 4,
        "id": 7,
        "shippingCost": 1,
        "trackingId": "1Z881F5V0397269080"
      }
    ]
  }
]

to this:

[
  {
    "id": 4,
    "email": "jane.xiu@example.com",
    "name": "Jane Xiu",
    "city": "Edinburgh",
    "countryRegion": "UK",
    "totalShippingCost": 4
  }
]

The transforms argument enables you to transform the HTTP response data in many powerful ways. To see additional capabilities and examples of transforms, refer to the Directives Reference.

Recap

In this section, we've covered the basics of the @rest directive. We've covered ways to configure the HTTP request StepZen sends to the API, and we've also looked at how to transform the responses. What we reviewed in this document is not an exhaustive list of the capabilities of the @rest directive.

For more information on how to format the HTTP request body, review the POST for REST services page.

For information on mapping REST API paging to GraphQL paging, review the Pagination for REST services page.

Please review the Directives Reference for a full list of the capabilities of the @rest directive.

This site uses cookies: By using this website, you consent to our use of cookies in accordance with our Website Terms of Use and Cookie Policy.