Why GraphQL Directives Are Out of This World

Lucia Cerchie - Jan 5 '22 - - Dev Community

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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/"
    )
Enter fullscreen mode Exit fullscreen mode

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
  }
Enter fullscreen mode Exit fullscreen mode

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
  }
]
Enter fullscreen mode Exit fullscreen mode

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"
        )
}
Enter fullscreen mode Exit fullscreen mode

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
  }
Enter fullscreen mode Exit fullscreen mode

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..."
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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"
        )
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Now I'm able to query my database!

  retrieveByName(nameParam: "Dragon 2") {
    space_poem
  }
Enter fullscreen mode Exit fullscreen mode

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."
}
Enter fullscreen mode Exit fullscreen mode

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" }]
        )
}
Enter fullscreen mode Exit fullscreen mode

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
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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)..."
        }
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

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" }]
    )
Enter fullscreen mode Exit fullscreen mode

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
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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."
        }
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .