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:
-
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 Transform REST to GraphQL documentation.
-
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 creatign a GraphQL API for a REST backend 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 isGET
. Supported values areDELETE
,GET
,PATCH
,POST
, andPUT
.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.