An interface is a data type with implementations that map the type to different backends (i.e., to define more than one backend against the same type). This enables StepZen to connect multiple backends to a single type.
Typical examples of GraphQL show how different data can be assembled from different domains. For example, a query like this:
{ customer (email: "john.doe@example.com") { name orders { createdOn } weather { temp } } }
assembles customer
, order
and weather
data.
But what if you have multiple backends that provided customer information? You will want to do two things:
- Combine the responses into one format.
- Make sure that the right backends get called with the right protocol and parameters.
GraphQL has a powerful feature called interface
. We will leverage that to show how this feature can be used to collect data from the various backends. In StepZen, it works like this:
Combine responses into one format
As a first step, define an interface and its common fields (this is pretty straightforward). And define (concrete) types that implement this interface, one for each backend that needs to be supported.
The concrete type must implement all the fields of the interface, but it is free to implement more. This ensures that all responses from the backends can be combined into one interface.
Ensure that the right backends are called
And now define queries on the interface, and queries for concrete types with a new directive @supplies
.
This directive lets StepZen know that when a query comes on an interface it must call the concrete query that supplies the interface query. These queries can be against REST, Database, GraphQL or any backend. The only requirement is that the return types of these queries must be the concrete types.
Let's break this down with a full example, and then examine its various parts.
Example: Using Interfaces to Access Multiple Backends
- Create a working directory and
cd
to that directory. - Create a file
interface.graphql
with the following code in it:interface Customer { id: Int name: String email: String city: String street: String zip: String state: String country: String } type Query { customerById(id: ID!): Customer customersByCity(city: String!): [Customer] }
- Build the first backend that connects to a REST endpoint. Create a file
customer1.graphql
with the following code in it:type Customer1 implements Customer { id: Int name: String email: String city: String street: String zip: String state: String country: String } type Query { customer1ById(id: ID!): Customer1 @supplies(query: "customerById") @rest(endpoint: "https://json2api-anant-p2axj4bzta-uw.a.run.app/customers/$id", transforms: [{pathpattern:"<>*", editor: "jq:.[]|{name,email,id,city:.address.city,state:.address.stateProvince,street:.address.street,zip:.address.postalCode,country:.address.countryRegion}"}]) customers1ByCity(city: String!): [Customer1] @supplies(query: "customersByCity") @rest(endpoint: "https://json2api-anant-p2axj4bzta-uw.a.run.app/customers?q=address.city+eq+$city", setters: [{field: "city", path: "address.city"}, {field: "state", path: "address.stateProvince"}, {field: "country", path: "address.countryRegion"}, {field: "street", path: "address.street"}, {field: "zip", path: "address.postalCode"}]) }
- Build the second backend that connect to a MySQL database. Create a file
customer2.graphql
with the following code in it:type Customer2 implements Customer { id: Int name: String email: String city: String street: String zip: String state: String country: String } type Query { customer2ById(id: ID!): Customer2 @supplies(query: "customerById") @dbquery (type: "mysql", query: "select c.email, c.id, c.name, a.street, a.city, a.postalcode as zip, a.countryRegion as country, a.stateProvince as state from customer c, address a, customerAddress ca where ?=ca.customerId and ca.addressId=a.id", configuration: "mysql") customers2ByCity(city: String!): [Customer2] @supplies(query: "customersByCity") @dbquery (type: "mysql", query: "select c.email, c.id, c.name, a.street, a.city, a.postalcode as zip, a.countryRegion as country, a.stateProvince as state from customer c, address a, customerAddress ca where c.id=ca.customerId and ca.addressId=a.id and a.city=?", configuration: "mysql") }
- Give the credentials for the MySQL. Create a file
config.yaml
with the following code in it:configurationset: - configuration: name: mysql dsn: testUserIntrospection:HurricaneStartingSample1934@tcp(35.224.227.100)/introspection
- Finally, create a file
index.graphql
with the following code in it:schema @sdl (files: ["interface.graphql", "customer1.graphql", "customer2.graphql"]) { query: Query }
- Now issue
stepzen start
, pick an endpoint, and issue the following two queries.
The first query goes against both the backends and picks all customers who live in Boston. And the second query picks the right customer from one backend (in this case REST).query MyQuery { customersByCity(city: "Boston") { email name } customerById(id: "101") { email name } }
Deconstructing the GraphQL endpoint
-
The file
interface.graphql
defines the types and queries for the interface. -
The file
customer1.graphql
defines the concrete implementations of these interface types and queries for the REST backend.-
type Customer1
implementsinterface Customer
. Pretty straightforward. You must implement all the fields of the interface, but can implement others. In this case, there are no others. -
type Query
contains two queries, one for each query in the interface. The query definition is as follows:
- Its return type is the concrete type.
- It
@supplies
the interface type (so that StepZen knows to call this query). - Its implementation is a REST call made against an endpoint.
- If you do the following curl and note the shape of the response, it needs to be modified to conform to the GraphQL type structure. The
transforms
argument in the first query does that.
curl https://json2api-anant-p2axj4bzta-uw.a.run.app/customers/101`
In this case, we use jq to do the transformation. The
pathpattern
can be ignored, withjq
editor, it will always be"<>*"
. The jq specification is a simple JSON transformation that takes the response from the backend, and converts it into the JSON that is right for the GraphQLtype Customer1
.- If you do the following curl
curl https://json2api-anant-p2axj4bzta-uw.a.run.app/customers?q=address.city+eq+Boston`
and note the shape of the response, it needs to be modified to conform to the GraphQL type structure.
The
setters
argument is a different way of shaping the response from the backend into a JSON shape that conforms to the concrete type declarations. It is a different and simpler way to do these transformations, but cannot do the rich stuff thatjq
can do. These two examples here are equivalent, pick whichever works for you. -
-
The file
customer2.graphql
defines the concrete implementations of these interface types and queries for the MySQL backend.type Customer2
implementsinterface Customer
.type Query
contains two queries, one for each query in the interface. The query definition is as follows, as in thecustomer1.graphql
file:
- Its return type is the concrete type.
- It
@supplies
the interface type (so that StepZen knows to call this query). - Its implementation is a SQL call made against an endpoint (it is doing a three way join). The database is the same as the database in this blog. Feel free to explore it.
- The shaping of the response is done using the
as
clause.
That's it. StepZen routes a query on the interface to the right concrete queries that supply that query.
Each of those queries, in turn, get called with the right protocol (in this case REST, but could be SQL or GraphQL). And the transforms
ensures that the shape of the returned data conforms to the type.