What is GraphQL?
As described on the project’s homepage, GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data. GraphQL provides a complete and understandable description of the data in your API, gives clients the power to ask for exactly what they need and nothing more, makes it easier to evolve APIs over time, and enables powerful developer tools.
GraphQL represents your data as a graph to make it more familiar and natural. It makes API evolution clear and concise without versioning. It is also designed to be used with web components, and this will be the main port of this article.
Benefits
ServiceNow acts like a GraphQL server. You can create a client which will communicate with it based on schemas. This method makes it easy to discover available fields and objects through introspection and get exactly what you ask for, no more and no less. It is also worth mentioning that with a single request, you can get data from multiple sources.
Setup in ServiceNow instance
Create custom schema
OK, let’s think about users. Let’s assume we want to enable an external system to get a user name based on sys_id. How would a business user describe it?
I want my system to query users. I will give you an id, and will ask for a name in return. Nothing more, nothing less.
This single sentence is enough for us to get started. And to be honest, our schema is almost complete. We just have to translate business language into GraphQL Schema Definition Language (SDL).We start with a schema. A schema has a query. A query has some fields (these can also be scripts). Each field (or script) has a type. A type can be predefined (also called a scalar type) or we can define it. Our final schema may look like this:
schema { # a schema
query: Query # has a query
}
type Query { # a query
getUser(id: ID!): User # has a field (a method)
} # and returns type "User"
type User { # type "User"
id: ID @source(value: "sys_id") # has field "id"
name: String @source(value: "userName") # and field "name"
} # fields are mapped
Great, we have it written down. And now what? How is ServiceNow instance going to know where to search for data? This is done with resolvers.
Resolver script and mapping
Next step in schema creation is to create resolver scripts and map them to the schema path. In our query type, we said I will give you an id and I want a user type back. So we create a resolver (getUser) with a single parameter (id) of type ID (non-nullable, hence the ! sign) and we tell the schema that the type returned will be User type. Since it’s not scalar, we have to define it. Our resolver will have to return exactly what we set in the return type in schema definition – in our case, it is an object with two keys: sys_id and userName. As you can see, what is returned by the resolver script can be named differently than what can later be queried.
With all that information, the script may look like that:
(function process(/*ResolverEnvironment*/ env) {
const userid = env.getArguments().id != null ? env.getArguments().id : env.getSource();
let gqUser = new global.GlideQuery('sys_user')
.get(userid, ['sys_id', 'first_name', 'last_name'])
.orElse({
sys_id: '-1',
first_name: 'Unknown',
last_name: 'User'
});
let userObj = {
sys_id: gqUser.sys_id,
userName: gqUser.first_name + ' ' + gqUser.last_name
};
return userObj;
})(env);
Important parts are in bold. First line references id – an argument of our Query type resolver. The last line returns an object userObj – it has exactly the same fields as our User type @source
value mappings. The rest is up to us.
Now the final step, mapping of the script to the schema path. This is required because otherwise how would a GraphQL server know what to do with requests querying getUser(“some_id_value”)?
In ServiceNow this is done easily. We just assign a script to a path on the related list of the GraphQL schema definition:
Use it!
Project setup
The first thing you want to do is to set up your local development environment. ServiceNow provides some guidance. Just make sure you follow these tips (or things I wish I knew before starting) :
- Read this great article by Jon G Lind
- This should be enough, if not continue here
- Start by installing nvm, not by installing node.js – nvm is a handy tool that lets you manage and switch between multiple node.js versions
- Once done, use it to install node v12.22.12
- Finally, install the snc command line tool (a.k.a servicenow cli). NOTE: If you don’t have access to the ServiceNow Store, you can use this GitHub repository.
- Now you can continue with the regular setup path:
If you encounter any issues, make sure to check this great guide by Arnoud Kooi.
Custom component
I will start with the most basic one because this article is not about component building. I will simply present the name of the user retrieved by sys_id.
In order to use GraphQL in our custom component, we have to define a data fetch handler. In nodejs there is a package called @servicenow/ui-effect-graphql which can be used to do that.
Write a query
You can learn more about GraphQL queries on the official project documentation, in our case it is really basic:
const GQL_QUERY = `
query ($id: ID!) {
applicationNamespace {
schemaNamespace {
getUser (id: $id){
name
}
}
}
}`;
Create data fetch handler
We will use the method createGraphQLEffect from the package @servicenow/ui-effect-graphql. It takes two parameters – a GraphQL query and action handlers:
const dataFetchHandler = createGraphQLEffect(GQL_QUERY, {
variableList: ['id'],
startActionType: DATA_FETCH_STARTED,
successActionType: DATA_FETCH_SUCCEEDED,
errorActionType: DATA_FETCH_FAILED
});
Call it from component’s action handler
Now if we want to call it from the action handler, we have to pass the id parameter value somehow. As you may have noticed so far, it’s not that intuitive. The way it can be done (I don’t know if it’s the only possible option, but it works) is through combination of custom properties and initial state values on the createCustomElement method:
createCustomElement('component-name', {
renderer: {type: snabbdom},
...
initialState: {
user: {},
loading: true,
data: false,
errors: {}
},
properties: {
sysId: { default: "user_sys_id" }
},
actionHandlers: {
[COMPONENT_BOOTSTRAPPED]: (coeffects) => {
coeffects.dispatch(
"FETCH_DATA_REQUESTED",
{ id: coeffects.properties.sysId }
);
},
[COMPONENT_ERROR_THROWN]: ({action: {payload}}) => {
console.log(payload);
},
"FETCH_DATA_REQUESTED": dataFetchHandler,
[DATA_FETCH_SUCCEEDED]: ({action, updateState}) =>
updateState({
user: action.payload.data.applicationNamespace.schemaNamespace.getUser,
data: true,
loading: false,
errors: action.payload.data.errors
});
}
});
The most important part is the COMPONENT_BOOTSTRAPPED action handler. You can see where it lies in the component lifecycle events. It is called exactly once, when the component is bootstrapped. In our case, we use it to dispatch another event and pass our sys_id value to it. That new event happens to be connected with the dataFetchHandler method, which is eventually running our GraphQL query.
Also worth mentioning is the object we receive as a query result in the DATA_FETCH_SUCCEEDED action handler. It may seem complex at first, but it’s JSON after all, with everything you asked for, nothing more and nothing less.
Conclusion
Using GraphQL may seem scary at the beginning. But believe me, once you get a good grasp of it, build one or two queries and schemas, you will want to use it everywhere. During my career I’ve seen many scripted REST APIs that were so cluttered and complex that you only wanted to delete it and start from scratch. With GraphQL schemas, thanks to resolvers and mappings, we don’t have it. Business logic layer is separated from the implementation. You can easily present your schema to the end users without scaring them.I have one more recommendation to read from the GraphQL project page – start thinking in graphs.