The Power of GraphQL Directives

Authors
Łukasz Czyszczonik
Software Engineer
@
Callstack

Recently, GraphQL has become a trending solution for the server side implementation. Most of the companies that I know have decided to use GraphQL because of the responsibility. The ecosystem of GraphQL is growing at a fast pace, but people still don’t know all its powerful features. And one of those features is called directives.

GraphQL directives have been created to deal with the problem of recurring tasks during the implementation. It’s kind of syntax sugar for your GraphQL server implementation.

Those common recurring tasks could be:

  • Creating a custom field
  • Getting data from the REST API
  • Permissions management

Computing custom properties

One of the common situations is when you need to create a new field, that is derived from previously defined fields. Let’s see how we can resolve that issue by taking the native approach.

Our front-end team tells us that they need to have <rte-code>fullName<rte-code> returned from the server (yes, I know that is a trivial example). Let’s start with updating our schema and add <rte-code>fullName<rte-code>

type User {
firstName: String!
lastName: String!
fullName: String!
}

The second thing we need to do is to update your resolvers. We need to create User resolver with <rte-code>fullName<rte-code> that will return the data that will fit our requirements:

{
Query: {
me: () => {
// return user with firstName and lastName
},
},
User: {
fullName: (user) => `${user.firstName} ${user.lastName}`
},
}

As you can see it’s not so hard to implement, but imagine when you have more tasks like this, also more complicated. Be smart and use a directive that will save your precious time.

To simplify that common task, I wrote a small directive called graphql-directive-computed-property. It allows you to create a computed property from every other property that lives in a particular type without touching your resolvers. Here is an example of the usage:

type User {
firstName: String
lastName: String
fullName: String @computed(value: "$firstName $lastName")
}

And that’s all that you need to do (apart from installation, of course)!

As you can see, we’ve added <rte-code>@computed<rte-code> directive definition to the new field and specified their content. Inside value argument, you have access to all fields on that type.

REST API

Another frequent use case is to manage integration with existing REST APIs. Let’s imagine we have the below schema on our server:

type User {
login: String
avatar_url: String
}
type Me {
gender: String
email: String
admin: String
}
type Query {
me(gender: String): Me
users: [User]
user(user: String): User
}
view raw schema.graphql hosted with ❤ by GitHub

In our case, we want to get all the data from various REST API. We start writing resolvers one by one and after some time we get something like this:

const resolvers = {
Query: {
me: (_, args) =>
fetch(`https://randomuser.me/api/?gender=${args.gender}`)
.then(res => res.json())
.then(data => data.results[0]),
users: () => fetch('https://api.github.com/users').then(res => res.json()),
user: (_, args) =>
fetch(`https://api.github.com/users/${args.user}`).then(res =>
res.json()
),
},
Me: {
admin: () =>
fetch('https://yesno.wtf/api')
.then(res => res.json())
.then(data => data.answer),
},
};
view raw resolver.js hosted with ❤ by GitHub

Sure, some of you may think that we can create an abstraction over the fetch library, which could remove unnecessary duplication. But there’s a better approach — we can use the directive for rest. It is a directive that helps you write the server which needs to integrate with the REST API. Here's how the implementation looks with the directive:

const GITHUB_URL = 'https://api.github.com';
const USER_URL = 'https://randomuser.me/api';
const ADMIN_URL = 'https://yesno.wtf/api';
const typeDefs = `
type User {
login: String
avatar_url: String
}
type Me {
gender: String
email: String
admin: String @rest(url: "${ADMIN_URL}" extractFromResponse: "answer")
}
type Query {
me(gender: String): Me @rest(url: "${USER_URL}/?gender=$gender" extractFromResponse: "results[0]")
users: [User] @rest(url: "${GITHUB_URL}/users")
user(user: String): User @rest(url: "${GITHUB_URL}/users/$user")
}
`;

Finally, this is our new schema definition. As you can see it looks almost like the previous one, the difference is that we have the extra @rest annotation on almost every field.

Let’s explain the above code. <rte-code>@rest<rte-code> annotation accepts two arguments. First of them is URL and takes the string with a path to the response from where you would like to get the data. The extra thing here to notice is that we have an access to query parameters inside the URL parameter. Our path can base on the query arguments. The second argument accepts a path to the data from the response.

Now is time to write the resolvers:

undefined

We don’t need any resolvers! Why? All work is getting done inside the directives.

A quick explanation of what we do here:

  • Me query will get the data from https://randomuser.me/api/?gender=$gender where <rte-code>$gender<rte-code> parameters we get from the query parameters. The <rte-code>extractFromResponse<rte-code> parameter tells us that we want to get the first element from the response array.
  • Users query return all the data from "users" - in that case we get all user and return them from our server.
  • Type Me contains the admin field where we want to get data from https://yesno.wtf/api, and extract from the response object <rte-code>answerproperty<rte-code>.

As you can see, we can quickly enhance our server with real data from REST API with no big effort.

I described two directives (permissions-manages is coming soon, stay tuned!) but you can check others directives at graphql-community There are other directives there that can be helpful.

How to create your own directives

I have created a template repository with all the necessary things to ease the start. All you need to do is clone the repository. After that, you are ready to start. Now I’m going to tell you about a few important things related to that repository:

  • Your directive implementation should start in <rte-code>index.js<rte-code> on the root folder,
  • The example server is placed <rte-code>onexample/index.js<rte-code>. For server implementation we used <rte-code>apollo-server-express<rte-code>,
  • Under <rte-code>example/schema.js<rte-code> you can find the schema definition — a place to use your directive and create the custom schema,
  • The <rte-code>__tests__ folder<rte-code> contains all the tests related to directives (we use Jest for testing).,
  • If you would like to start the example server, you can use yarn dev scripts – this also runs GraphiQL where you can test your queries; your server will be reloading itself after each change.

Summing up

Another great source to learn about the implementation details is the documentation of graphql-tools. If you come up with any interesting ideas after finishing your read, let me know and we’ll publish it on graphql-community.

I would like to thank Evans Hauser and his introduction into Directives. Also, I would like to thank all Callstack team members of Open Source Evangelists. I am convinced that without all the effort this community would have never been born.

Insights

Learn more about GraphQL

cover

Nov 14Article

Amazing React Native Apps with Apollo React

How to to handle communication with my GraphQL server using other solution like Apollo React client.

cover

Nov 2Article

Setting up GraphQL With Hapi.js

Learn how to set up GraphQL with Hapi.js by reading this step-by-step guide prepared by Mike Chudziak.

cover

Mar 19Tutorial

The Hard Way vs. The React Native AI SDK Way: Stop Writing Custom Modules for Every Model

Learn how React Native AI uses a unified provider model to switch between cloud and on-device LLMs without rewriting application logic, enabling reliable AI experiences online and offline.

cover

Mar 12Tutorial

Build Smarter Apps: Tool Calling & AI Orchestration Explained

Learn how on-device models in React Native can call tools, interact with external APIs, and return structured outputs you can use directly in application logic.

cover

Mar 5Tutorial

MLC LLM + React Native: On-Device AI Without the Pain

Learn how to run third-party on-device LLMs in React Native using MLC LLM. Choose your models, run them efficiently across platforms, and keep a unified JavaScript API with React Native AI.

cover

Feb 24Tutorial

Intelligent Fallbacks with Apple Intelligence in React Native

Learn how to use Apple’s built-in Foundation Models in React Native apps, reduce memory usage, and enable fast, fully on-device AI with React Native AI Apple.

cover

Feb 10Tutorial

What Is the React Native AI SDK? A Complete Intro & Quickstart

Learn why on-device AI matters for React Native apps, how local LLMs behave offline, and what problems React Native AI is designed to solve from day one.

cover

Jan 29Tutorial

Implementing an Android TurboModule with Kotlin

Learn how to implement a React Native TurboModule on Android, from the TypeScript spec and Codegen to the Kotlin implementation and package registration.

cover

Jan 19Tutorial

Working with Different Threads in Swift TurboModules

Learn how JavaScript, native module, background, and main threads interact in React Native TurboModules, and how to use GCD queues to run async work safely in Swift.

cover

Jan 9Tutorial

How to Add Type-Safe Constants to Swift TurboModules

Learn how to expose typed, immutable constants from a Swift TurboModule using Codegen, and get full end-to-end type safety between native and JavaScript.

cover

Dec 30Tutorial

Adding Event Emitters to Your TurboModule in Swift

Learn how to emit native events from a Swift TurboModule and subscribe to them in JavaScript, including proper memory management and Codegen integration.

cover

Dec 22Tutorial

Writing Your First TurboModule in Swift

Learn how to build a fully working TurboModule in Swift, integrate it with Codegen, and bridge it through Objective‑C inside an Expo app.