Note: Some experience with GraphQL (introductory knowledge of schemas, types, GraphiQL explorers, etc.) is assumed in this blog post. If you'd like to learn more, check out our blog post on directives by Brian Rinaldi, or this post by my colleague Leo Losoviz. This post was originally published at stepzen.com/blog
Intro
As an individual developer, I've grown pretty starry-eyed for GraphQL over the past year. One of the biggest reasons I'm a fan is the room the GraphQL spec makes for custom directives.In many cases, the power of custom directives completely erases the need for business logic in the front end of my applications.
To help you see what I mean, I've dreamt up a little example: say I'm working with the SpaceX GraphQL API, the Wikipedia REST API, and a MySQL database I've created on Railway. There are theoretical nodes in both the Wikipedia API and the database that are connected to the SpaceX API:
The SpaceX API has a Wikipedia URL string, while the Wikipedia API has an endpoint to return information from a page by the Wikipedia title/URL.
The Railway database holds a space poem in the same row as a rocket title that the SpaceX API has.
Let's concretize this theoretical connection between all three datasources with GraphQL and StepZen. We're going to do this by using three StepZen directives to connect your backends using GraphQL SDL!
@graphql Directive To Connect a GraphQL API
StepZen's custom @graphql directive connects a schema to a GraphQL API in a matter of a few lines. You can dive deeper by visiting our docs.
To generate my entire graphql schema, I ran stepzen import graphql. This command introspects the endpoint provided to it, and autogenerates a schema inside the working directory. It prompted me with these questions:
? What would you like your endpoint to be called? api/autogenerated-name
I was satisfied with the autogenerated endpoint in this case, so I hit enter.
Next up, the StepZen command line interface prompted me with these questions:
? What is the GraphQL endpoint? https://api.spacex.land/graphql
? Do you want to add an Authorization header? No
? Do you want to add a prefix? Yes
? What prefix should we use? spacex_
Generating schemas...... done
Successfully imported 1 schemas from StepZen
Then I had a fully loaded schema (including the requisite types), ready to explore in a GraphiQL browser upon stepzen start!
Here's a sample graphql query type:
dragons(limit: Int, offset: Int): [Dragon]
@graphql(
endpoint: "https://api.spacex.land/graphql/"
)
As you can see, StepZen's custom @graphql directive makes it a matter of a few lines to integrate a GraphQL API into my data layer. The endpoint argument takes in the pinged url. The SpaceX API doesn't require authentication, but I could add it in this directive if I needed.
When I opened up the GraphiQL interface with stepzen start, I could then run:
dragons(limit: 1) {
description
crew_capacity
}
And get back:
"dragons": [
{
"description": "Dragon 2 (also Crew Dragon, Dragon
V2, or formerly DragonRider) is the second version
of the SpaceX Dragon spacecraft...",
"crew_capacity": 7
}
]
Fantastic! Now to set up my REST API endpoint.
@rest Directive To Connect To a REST API
The custom @rest directive connects your schema to a REST API. You can find out more about it in our docs.
I'm going to create my own schema for the REST API this time, inside the same folder as my instrospected GraphQL schema. (I connect them using index.graphql.)
For my schema that connects a REST API backend, I kept things short:
type wikiResult {
extract: String
}
type Query {
getWikiByTitle(title: String): wikiResult
@rest(
endpoint: "https://en.wikipedia.org/api/rest_v1/page/summary/$title"
)
}
Here, the @rest directive takes in the endpoint argument to connect to the API, similar to the @graphql directive.
Again, it surfaces in my GraphiQL explorer so I can run:
getWikiByTitle(title: "Dragon_2") {
extract
}
to return
{
"data": {
"getWikiByTitle": {
"extract": "Dragon 2 is a class of partially reusable
spacecraft developed and manufactured by American aerospace manufacturer SpaceX,
primarily for flights to the International Space Station (ISS).
There are two variants: Crew Dragon,
a spacecraft capable of ferrying up to seven crew, and Cargo Dragon..."
}
}
}
Sweet! I'm wiring up the backends to my GraphQL layer one-by-one. The last one I have is the database.
@dbquery Directive To Connect To a Database
StepZen's @dbquery directive connects schemas to databases. Read up on it in our docs.
Again, I only needed to return a string from my database, so I kept the type definition simple:
type mysql_Result {
space_poem: String
}
type Query {
retrieveByName(nameParam: String!): mysql_Result
@dbquery(
type: "mysql"
query: "SELECT space_poem FROM dragons WHERE name > ? "
configuration: "MySQL_config"
)
}
You can see the arguments inside @dbquery are a little different from those in @rest or @graphql.
type sets the type of database, in this case, a MySQL one.
query specifies the query I want to execute on the table
configuration points to a config.yaml file I've made in the same directory that specifies my dsn like so:
configurationset:
- configuration:
name: MySQL_config
dsn: user:password@tcp(host)/railway
Now I'm able to query my database!
retrieveByName(nameParam: "Dragon 2") {
space_poem
}
returns
"retrieveByName": {
"space_poem":
"The night is come, but not too soon;
And sinking silently,
All silently, the little moon
Drops down behind the sky."
}
Merging Data From Different Sources with @materializer
Now we'll use a new directive called @materializer to merge data from all three schemas into one query.
Let's go back to our SpaceX schema, with the dragon type:
type Dragon {
active: Boolean
crew_capacity: Int
description: String
diameter: Distance
dry_mass_kg: Int
first_flight: String
heat_shield: DragonHeatShield
id: ID
name: String
orbit_duration_yr: Int
pressurized_capsule: DragonPressurizedCapsule
wikipedia: String
wikipedia_content: wikiResult
@materializer(
query: "getWikiByTitle"
arguments: [{ name: "title", field: "name" }]
)
}
As you can see, there's a directive called @materializer at the bottom. @materializer has a different purpose than the previous three directives we've talked about, in that it surfaces data to a GraphQL type -- while the rest connect queries to backends.
This directive is execute the "getWikiByTitle" query -- and it's feeding the value of the name from the Dragon type into the Wikipedia API's title parameter. That way, the results of the Wikipedia query surface when this type is returned!
query MyQuery {
dragons(limit: 1) {
id
name
wikipedia_content {
extract
}
}
}
returns:
{
"data": {
"dragons": [
{
"id": "dragon2",
"name": "Dragon 2",
"wikipedia_content": {
"extract": "Dragon 2 is a class of partially reusable
spacecraft developed and manufactured by American aerospace manufacturer
SpaceX, primarily for flights to the International Space Station (ISS)..."
}
}
]
}
}
Hurray! Now to make the final connection -- can we surface information from our database connection to the same query?
Again, @materializer comes to the rescue, providing the value for the poem field on the Dragon type:
poem: mysql_Result
@materializer(
query: "retrieveByName"
arguments: [{ name: "nameParam", field: "name" }]
)
Now I can make one query that returns information from all three backends:
query MyQuery {
dragons(limit: 1) {
id
name
wikipedia_content {
extract
}
poem {
space_poem
}
}
}
id
and name
come from the SpaceX API, wikipedia_content
comes from the Wikipedia API, and poem
comes from my database!
{
"data": {
"dragons": [
{
"id": "dragon2",
"name": "Dragon 2",
"wikipedia_content": {
"extract": "Dragon 2 is a class of partially
reusable spacecraft developed and manufactured
by American aerospace manufacturer SpaceX,
primarily for flights to the International
Space Station (ISS). There are two variants:...."
},
"poem": {
"space_poem": "The night is come, but not too
soon; And sinking silently, All silently, the
little moon Drops down behind the sky."
}
}
]
}
}
Conclusion
If I'd been using another solution, I'd have needed to write resolvers on the backend, or business logic on the frontend to connect and filter data. These directives simplify the architecture of my project and save me lines of code. That's why when it comes to schema design, I'm a directive-first fan.