GraphQL: start using interfaces in your quries and mutations

Bastiaan Dewaele

--

Less known and explained feature about GraphQL is interfaces.

The trouble

With very few types, you don’t exactly need interfaces. But once you’re repeating a lot of fields, you may consider implementing interfaces to your code.

What is an interface?

It can be viewed as a contract that defines and enforces specific properties and methods for a given object (Typescript, PHP), and with a GraphQL schema, it establishes requirements as fields.

interface DataRow {
title: String!
excerpt: String!
# ...
}

type Article implements DataRow {
title: String!
excerpt: String!
}

type Page implements DataRow {
title: String!
excerpt: String!
}

Why implement interfaces?

In smaller applications that are very limited in complexity, you can perfectly do without ever implementing an interface.

But when faced with a scenario in which you need to handle a global search that yields various types in GraphQL, such as the one presented here, without utilizing interfaces.

query GlobalSearch($filter: GlobalSearch!) {
globalSearch(filter: $filter) {
... on Article {
title
excerpt
comments {
id
replies {
...Commentable
}
}
}
... on Page {
title
excerpt
comments {
...Commentable
}
}
# ... Repeate the same code for other types
}
}

fragment Commentable on Comment {
id
replies {
id
}
}
}

As you can imagine, the number of times you must repeat the same field for multiple types can be quite high. Therefore, when adding new features, there is a risk of forgetting to repeat them.

  • Forgetting to implement a type
  • Forgetting to add a field to each type

Don’t repeat yourself

Instead, you can refactor everything in a single (or multiple) interface(s) and implement it in your query.

Tip 1: Return interfaces

Let’s rewrite the query to change the return type to the interface DataRow.

interface DataRow {
title: String!
excerpt: String!
comments: [Comment!]!
users: [User!]!
}

extend type Query {
globalSearch(filter: GlobalSearch!): [DataRow!]!
}

And our actual query being called in the front-end will look similar to this.

query GlobalSearch($filter: GlobalSearch!) {
globalSearch(filter: $filter) {
title
excerpt
comments {
id
replies {
id
...
}
}
users {
id
...
}
}
}

Now, all results use the same structure. Another way to do this is to return a union listing possible types.

union ContentType = Article | Post | Podcast | ...;

extend type Mutation {
updateCollaborators(input: UpdateCollaborators!): ContentType!
}

Tip 2: return unions

This way, you can still access specific fields of certain types.

mutation UpdateCollaborators($input: UpdateCollaborators!) {
updateCollaborators(input: $input) {
... on DataRow {
id
collaborators {
id
user_id
fullname
}
}
# Specific fields of a type
... on Post {
id
images {
...
}
videos {
...
}
}
}
}

Important

There are a few things to consider when implementing an interface.

  • Each field defined in your interface must exist in your type
  • The field type String! must equal String! of that of the interface and not String

Summary

Overall, adding DataRow greatly reduces code repetition.

Tip 3: Split DataRow in multiple interfaces

Our first improvement makes things easier, but every result of a specific type requires implementing each field of DataRow.

So we start having issues if pages don’t support commenting compared to news articles.

Interface DataRow {
id: ID!
title: String!
excerpt: String!
}

interface Commentable {
id: ID!
comments: [Comment!]!
total_comments: Int!
}

interface Collaborators {
id: ID!
users: [User!]!
}

type Article implements DataRow & Commentable {
id: ID!
...
comments: [Comment!]!
total_comments: Int!
}

type Page implements DataRow & Collaborators {
id: ID!
...
users: [User!]!
}

But why? Well by spreading fields, you can now use them separately in other queries and mutations.

  • As the interface grows in terms of field definitions, it imposes more constraints and requirements on each of your types
  • A specific type, such as an Article post can use comments and has collaborators
  • But pages do not implement comments

Example of fetching data:

Here is an example of a query that pulls articles and pages with multiple GraphQL interfaces. It will give you more flexibility in managing your information.

query GlobalSearch($filter: GlobalSearch!) {
globalSearch(filter: $filter) {
__typename
... on DataRow {
title
}
... on Commentable {
comments {
id
total_comments
}
}
... on Collaborators {
users {
id # user_id
firstname
lastname
...
}
}
}
}

Each row that is returned by the GraphQL query will only contain the fields that are specific to the type of interface that was implemented.

{
"data": {
"globalSearch": [
{
"__typename": "Article",
"title": "Untitled article",
"comments": [{...}],
"total_comments": 3
},
{
"__typename": "Page",
"title": "Untitled page",
"users": [{...}]
}
]
}
}

So only rows for articles will return the field total_comments. Which means it will be undefined, and we have a reason not to render an element in the front-end displaying the total comments.

Other examples

Here are some other examples as reference, on what is possible.

Single query

You can also build queries, that have an interface as a return type.


union CommentableResult = Article | Post | Podcast

extend type Query {
comments(model_id: ID!, model_type: String!): CommentableResult!
}

query Comments($model_id: ID!, $model_type: String!) {
comments(model_id: $model_id, model_type: $model_type) {
... on Commentable {
id
total_comments
comments {
id
user {
id
}
}
}
}
}

Mutations

To enable comments through a mutation, you can implement a union in your comment type that returns the corresponding database model for articles, social posts, or podcasts.

union CommentModel = Article | Post | Podcast

type Comment {
id: ID!
message: String!
user: User!
model: CommentModel!
}

input CreateCommentInput {
model_id: ID!
model_type: String!
message: String!
}

extend type Mutation {
createComment(input: CreateCommentInput!): Comment!
}

If you are using a decent library such as GraphQL Apollo. The caching is handled by the library.

mutation CreateComment($input: CreateCommentInput!) {
createComment(input: $input) {
id
message
user {
id
}
model {
id
total_comments
}
}
}
}

Sign up to discover human stories that deepen your understanding of the world.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

Bastiaan Dewaele
Bastiaan Dewaele

Written by Bastiaan Dewaele

Senior back-end developer in Ghent who likes writing sometimes weird / creative solutions to a specific problem.

No responses yet

Write a response