February 01, 2017
Designing Powerful APIs with GraphQL Query Parameters - Learnings & Best Practices for GraphQL API Design
GraphQL queries provide an efficient way to fetch data, even across multiple types. This results in a great abstraction layer on top of databases, but sometimes more is needed to satify specific data requirements of modern applications.
Our GraphQL API puts the client in control of the data and provides the best possible developer experience (DX) by exposing different query arguments. Similar to our nested mutations syntax which sees adoption by more and more companies, we at Prisma are happy to share our best practices designing powerful GraphQL APIs with the GraphQL community.
This article explores some of the capabilities of our GraphQL APIs including:
- Filters: Filters nodes by applying one or many matching rules.
- Sorting: Orders the set of results ascending or descending by a field.
- Pagination: Groups nodes in different pages that can be seeked, either forwards or backwards.
Filters reduce client complexity
Often you are interested to query specific parts of your data. In a movie database for example, you might search movies that are released after a certain date or have a specific title. While you can always query all movies and filter them client side, this would result in unnecessary data — something that GraphQL is set out to avoid.
This is a great use case for GraphQL query arguments! By adding a where
filter parameter to a query, the above scenario can quickly be solved. If you are interested in movies with the title
"The Dark Knight", you only have to specify the title
field on where
:
query darkKnightMovie {
movies(where: { title: "The Dark Knight" }) {
releaseDate
}
}
Filtering by title
works great for fetching single movies. In fact, the filter system is much more powerful than that, as it is encoding a boolean algebra. This allows a great level of expressiveness based on logical operators.
You can combine filter conditions using the OR
and AND
operators to select multiple movies you are interested in. For example, if you are interested in the movie "Inception" and additionally in movies released after 2009 that have a title
starting with "The Dark Knight", you can combine the two conditions with OR
:
query combineMovies {
movies(
where: {
OR: [{ AND: [{ releaseDate_gte: "2009" }, { title_starts_with: "The Dark Knight" }] }, { title: "Inception" }]
}
) {
title
releaseDate
}
}
By providing the filter query argument, the server is doing the heavy lifting while the client stays in full control over its data requirements. This results in a clear and expressive API and separates concerns between the frontend and the backend.
Expressing even complex data requirements
Another typical requirement is sorting data — again something that is better handled by the server than the client. Let’s combine sorting data with filters!
This time we’re interested in actors that appeared in movies released since 2009. The where
filter argument is a powerful system for requests like that. We can use movies_some
with a nested releaseDate_gte: "2009"
, meaning that we are only interested in actors where some of their movies fulfill the nested conditions.
On top of that, we are sorting the actors ascending by name with orderBy: name_ASC
:
query actorsAfter2009 {
allActors(where: { movies_some: { releaseDate_gte: "2009" } }, first: 3, orderBy: name_ASC) {
name
movies {
title
}
}
}
Combining filter and order parameters like that allows you to easily express even complex data requirements. We could also use movies_none
or movies_all
to express that no or all movies of an actor fulfill certain conditions.
If you look closely, you can also find the first: 3
argument in the above query. Let's see what this is about.
Browsing data in equally sized pages
Imagine a website that lists movies and their actors. Your data might contain thousands of movies each with dozens of actors. A typical scenario is then to only display a few movies at the same time, allowing the user to seek forwards and backwards through the whole list.
What you can do instead of fetching all movies at once in this case is to mirror the representation parameters in your query and only fetch the data you currently need. Again, this simplifies the needed client logic a lot while also reducing transmitted data. This approach is called offset-based pagination. An even more advanced pagination variant is the cursor-based pagination that is also available by combining first with after or last with before. Here we are focusing on offset-based pagination though.
The pagination arguments integrate seamlessly with filtering and ordering. This way, we end up with a simple query that fulfills a very complex request. Let’s have a look at how we can apply all what we’ve learned so far in the following query. We display the first
two movies that are released since 2000, order them by their releaseDate
and sort their actors
by name
. But see for yourself:
query paginateMoviesAndActors(
$movieFirst: Int
$movieSkip: Int
$actorFirst: Int
$actorSkip: Int
$movieOrder: MovieOrderBy
) {
allMovies(filter: { releaseDate_gt: "2000" }, orderBy: $movieOrder, first: $movieFirst, skip: $movieSkip) {
title
actors(orderBy: name_ASC, first: $actorFirst, skip: $actorSkip) {
name
}
releaseDate
}
}
You can experiment with the available query variables to see what effect they have. For example, you can change the movieSkip
to 2 and movieOrder
to title_DESC
and run the query again. This returns the second page of movies sorted descending by title
.
Conclusion
Our carefully designed GraphQL APIs offer great flexibility and control for client applications. With different rules to filter, sort and paginate your data, you can exactly express the data you are interested in and let the backend figure out the rest.
Do you have any questions about our API? Join our Slack channel to start a discussion or browse the docs for more information.