I’m building a GraphQL server to provide and API for a fictitious software consultancy firm. So far a user can query for a list of developers, their competencies and for their project assignments.
In this post I’m going to take this a bit further and look at how arguments are another useful tool in the GraphQL arsenal. They make your server more interactive, allowing for callers to be more specific when querying for data. With the use of GraphQL’s introspection feature, it’s also possible for a user to query which arguments are supported. Interactive environments like GraphiQL implement this feature to provide as-you-type auto-completion. It’s a fantastic way of conveying the ability to “explore” your API surface to a user.
Arguments are added to fields, either at the root query level, or an individual field on a type. They’re defined in the schema and they have too have types. I’m going to support some arguments in my GraphQL API, in order to demonstrate the different ways in which arguments can be used:
- Getting the highest-rated N competencies for a developer - Optional Arguments
- Getting an individual developer or project - Mandatory Arguments
- Getting all assigned (or unassigned) developers
- Getting skills, ordered A-Z or Z-A - Default Arguments
Let’s dive in!
Getting the Highest-Rated Competencies
When defining new GraphQL arguments, I always like to think of a question that is being asked. Allowing to display the highest-rated competencies for the developers answers “What are the staff’s specialisms?”. This could be useful when the consultancy firm is looking for new projects to take on.
I already have a field that retrieves a list of competencies, on the
Developer type. I add the new argument to this field -
top, of type
Although the schema has changed, this is not a breaking change for existing callers. As the argument is optional it has no effect on any existing consumers of this field. The change is purely additive.
Before jumping into the underlying implementation, a quick recap. Previously the
Developer class looked like this:
There are two functions that resolve data dynamically, to show how fields can be resolved asynchronously. The first argument to these functions was previously unused. When arguments are queried for on a particular field, they’ll be defined on this first object. Therefore I retrieve the
top argument from this object as such:
getCompetenciesForDeveloper function queries the database and maps the results to objects of type
Competency. I have updated this to use the
top argument, but as it’s optional, it may not be defined. To achieve the effect of providing the highest n competencies for a given developer, the SQL query sorts the data and then (optionally) applies a restriction to the number of items returned:
Getting an Individual Developer or Project
Arguments can be specified as mandatory, meaning that they must be defined. GraphQL will return with an error if you execute a query with a mandatory argument missing. I can use this technique to define queries to return a single item: either a
Developer or a
Project. As with other mandatory fields in GraphQL, mandatory arguments are identified with a
!. I add these new queries to the root query object:
Retrieving the single item from the database is a simple
SELECT WHERE query. If the result is not defined then an error is returned to the client instead:
A quick note about GraphQL error handling. With a REST API one would expect a 400, 404 or 500 depending on the context of the error. With GraphQL, you always get a 200 OK response, even for errors! This is because with GraphQL you’re not always querying for a single item. Indeed, it’s encouraged to query deeply nested data. The decision is therefore to “fail forward” - return what can be returned, and identify what aspects of the query failed.
To demonstrate this, for this example query:
A response object is received with the following structure:
A GraphQL response is always wrapped in the outer object with the
data keys, although
errors is omitted in the case of a fully-successful query. As the project field on the query produced an error, it’s set to
null in the response, but a helpful error is also specified.
Should the default error format not be appropriate for you (perhaps displaying a location or path risks a leakage of implementation detail), then there are projects out there that can improve or customise GraphQL errors.
Get all assigned (or unassigned) Developers
I can imagine a project manager in a resourcing meeting wishing to see at-a-glance which developers are currently assigned to a project, or are still waiting assignment.
As I have an existing query to return all developers, I can make a simple adjustment to define an
assigned argument of type
Boolean. By leaving it optional I am indicating that if it’s not present, it will fetch all developers. Again, this makes the change non-breaking for existing callers.
The root query now becomes:
This new argument is passed through to the API-layer function, just like the previous examples:
getDevelopers function itself is inherently now more complicated. The
SELECT aspect is still the same, but I now optionally add a
WHERE clause if the
undefined are actually different but as
undefined is falsey,
!assigned is true for both values. You need to be more specific with your undefined checks!
When the argument is set, I build up a subquery and concatenate it to the
SELECT statement. ES6 string templating makes this approach simple. In SQL, a
WHERE EXISTS statement lets you filter a result set based on the success of a sub-query. In this case, the sub-query asks if there is an entry in the
assignments table for a given developer’s id.
WHERE NOT EXISTS, as you’ll expect, does the inverse:
Getting Skills, Sorted Alphabetically or Reverse-Alphabetically
So far I’ve only exposed competencies in terms of a developer, where it comes alongside with an ability rating. But on a more general level, a caller could wish to just see a raw list of all the skills in the organisation. I could also imagine these wishing to be sorted either A-Z, or Z-A.
It’s possible to use an enum as the type of a particular argument. Furthermore, with any argument, you can also supply a default value. If the caller does not specify the argument, then the default value is taken. If you’re wishing to add a new mandatory argument to an existing field, you can retain backwards-compatibility this way.
Skill is an entity with a unique id and a name. I’ll also need an enum for a sort order, and a new field on the root query:
This results in a new resolver function:
When querying the database I add an
ORDER BY to the selection from the
skills table. The direction of the ordering is handled depending on the argument, respecting the default direction. Again, ES6 templating makes this incredibly easy:
In the examples above, I’m sorting and filtering the data that’s returned to the caller. I’ve chosen to implement these at the database layer, in the SQL queries. Remember though that as GraphQL makes no assumptions about where it retrieves its data from, it may be necessary for you to apply the effects of your arguments somewhere else, even possibly in the resolver functions directly. That’s absolutely fine and entirely your decision to make. The context of your application and profiling tools will assist you in determining where to make the choice. You may be retrieving your data from a static text file and have to do the
The API is currently one way - the work in this post only allows for a caller to be a bit more specific when asking for data. It would be nice if the user could post back data, just like a HTTP POST request would in a REST API. In the next post, I’ll take a look into how GraphQL’s mutations can be used for this purpose.