GraphQL: start using interfaces in your quries and mutations
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
}
}
}
}