Field Arguments
Explore how to add field arguments to GraphQL schemas in Absinthe, allowing users to provide input parameters that tailor query results. Understand how to define arguments, implement resolvers that handle optional inputs, and refactor logic for clarity. This lesson helps you make your API queries more flexible and readable while integrating business logic effectively.
We'll cover the following...
Defining field arguments
GraphQL documents are made up of fields. The user lists the fields they would like, and the schema uses its definition of those fields to resolve the pieces of data that match. However, the system would be inflexible if it did not allow users to provide additional parameters to clarify exactly what information each field needs to find. A user requesting information about menuItems, for instance, may want to see certain menu items or a certain number of them. For this reason, GraphQL has the concept of field arguments, which are defined as “a way for users to provide input to fields that can be used to parameterize their queries.”
Let’s look at our example application and see how we can extend our Absinthe schema by defining the arguments our API will accept for a field. Then, we can see how we can use those arguments to tailor the result for users.
We’ve already built a field in the schema.ex file of our API, but we could make it more flexible. To do so, we’ll accept user input, which in this case is the list of menu items. Our schema’s menuItems field looks something like this:
On line 7, the field resolver returns all the menu items without any support for filtering, ordering, or other modifications to the scope or layout of the result. The field isn’t declaring any arguments, so the resolver doesn’t receive anything with which we could modify the list of retrieved menu items. Let’s add an argument to our schema to support filtering menu items by name. We’ll call it matching. Then, we’ll configure our field resolver to use it when provided:
On line 7, we define matching as a :string type. Because we don’t t make the matching argument mandatory here, we need to support resolving our menuItems field in the event it’s provided and also if it isn’t. On line 12, the second function head serves as the fall-through match and is identical to our original resolver. It’s the first function head, on line 9, that adds our new behavior. On line 10, we use the matched argument as a name to build our Ecto query. We pulled the Ecto.Query macros in line 2. By declaring our inputs upfront, Absinthe has a bounded set of inputs to work with and can thus give us an atom-keyed map to work with as arguments, unlike Phoenix’s action controller parameters.
Writing complicated resolvers as anonymous functions can negatively affect a schema’s readability. To keep the declarative look and feel of the schema alive and well, let’s do a little refactoring and extract the resolver into a new module.
Because filtering menu items is an important feature of our application and could be used generally, not just from the GraphQL API, we’ll also pull the core filtering logic into the PlateSlate.Menu module. This is where our business logic relating to the menu belongs.
Here’s our new resolver module:
We can see the resolver is calling PlateSlate.Menu.list_items/1, passing the arguments. The logic inside PlateSlate.Menu looks like this:
This code should look pretty familiar. It’s been extracted from our anonymous resolver function and restructured into a named function. This makes both the resolver and the overall schema more readable. Now, let’s wire our resolver back into our :menu_items field in the schema:
In the code above, Elixir’s & function capture special form lets us tie the function from our new module as the resolver for the field. It also keeps the schema declaration tight and focused.