My take on GraphQL naming conventions
Design consistency is important, because it frees up developer brainpower for important work. Naming standards help avoid internal bikeshedding, and makes it easier for external developers to get up to speed on the API. Almost every company has its own internal guidelines which describe the internal coding style they use.
There are a large number of GraphQL style guides out there on the web. Apollo’s one immediately comes to mind, and even members of the GraphQL working group have offered their two cents on the matter. These resources are great starting points, and I think you should go read a few different naming guides to feel out how different people see this topic.
Personally, my contention with existing resources is that they tend to offer up naming suggestions without any justification for why their idea makes sense from a developer experience perspective.
An expert coffee grader is able to break down and explain why a particular cup is quality. This isn’t an innate skill; graders develop their taste over an extended period of time. I think the same is true for developer experience.
We should be able to break down why an API feels good to use, and why a particular design is more or less extensible than another. I’m not convinced when someone merely tells me to “do X.” I want to hear a concrete rationale, because I’m concerned with the long-term maintainability of my schema. I’m happy to trade off some aesthetics or purity in the name of developer experience—you’ll see that bias bleed through when I talk about naming mutation fields (INTERNAL).
So the big point of difference between this post and other GraphQL naming guides is that I’m going to try to tie my recommendations back to real-world use cases. In particular I’m going to talk about how different naming strategies impact your ability to evolve your schema over time, because
This post is particularly exciting, because I’ve built a table of contents system and this is the first post it’s being used on. Feel free to jump straight to the section that interests you most.
Table of contents
Disclaimer
For example, I’m going to talk about how to name and structure your mutation’s result types. My guidance here is two-legged:
- You should suffix these types with
Result
. For example:PostCreateResult
. - You should envelope the object under operation. So,
PostCreateResult.post: Post
rather than returning aPost
at the top-level of the result.
The first leg here is prescriptive insofar as I think you should suffix these types with something, and then be consistent with it. But you don’t need to use Result
suffix; I’ve seen Payload
, Response
, and other suffixes all used in the wild and they’re all perfectly reasonable choices. I personally prefer Result
when designing my own schemas, the point is to pick something and be consistent with it.
The second leg is prescriptive, because following this advice yields a schema that is strictly better when it comes to forwards evolution. Enveloping the created Post
object gives us far more flexibility compared to simply returning the Post
.
Keep this in mind this distinction when reading through the guidelines in the next section.
Guidelines
Query fields
Query fields should avoid verbs.
type Query { # ❌ getPosts: [Post!]! # ❌ listPosts: [Post!]! # ✅ posts: [Post!]!}
Why? Because it creates symmetry between the root-level Query
fields, and nested fields on your other schema types:
# ❌query ListBlogPosts { listPosts { id # this is an eyesore listComments { id content } # this is inconsistent comments { id content } }] # ✅query ListBlogPosts { posts { id comments { id content } }}
Mutation fields
Mutation fields should be named nounVerb
and not verbNoun
.
# ❌type Mutation { createPost( input: CreatePostInput! ): CreatePostResult!} # ✅type Mutation { postCreate( input: PostCreateInput! ): PostCreateResult!}
Why? Because consumers will almost always know what kind of object they are working with, but might not always know what actions are available. In a RESTful API operations are naturally namespaced by object by the URL path or by the client library itself (consider: stripe.paymentIntents.create
, not stripe.createPaymentIntent
). It’s theoretically possible to do this kind of namespacing in GraphQL, too:
# ❌type PostMutation { create( input: CreatePostInput! ): CreatePostResult!} type Mutation { posts: PostMutation!} mutation CreateAPost { posts { create(input: ...) { # ... } }}
But in practice this causes issues because the GraphQL spec has special-cased behavior for top-level fields on the Mutation
type. While other selection sets are free to run in parallel, top-level mutations are guaranteed to run sequentially. When you group mutations like this you are opting out of this special-cased behavior and it can introduce subtle bugs to your system.
Why might a consumer not know what actions are available? In GraphQL, we want to name our API operations according to their semantic meaning rather than in terms of their technical implementation details. RESTful APIs tend to do the opposite, which means there’s a rough standardization on verbs supported by those APIs (typically: “create”, “update”, “get”, “list”, “delete”).
In the case of our blog post—all of these verb choices could be legitimate depending on what our business requirements are:
type Mutation { # Maybe this moves the post to an "archives" page # and hides it from the home page? postArchive(...): ... # Perhaps this is a hard delete postDelete(...): ... # Hide from home page, but permalink still works? postHide(...): ... # This could be a soft delete? postUnpublish(...): ...}
How is a consumer to know that your platform prefers archiving posts instead of deleting them? They can’t. In the name of improved schema discoverability we are therefore forced into using alphabetization to group related operations together, hence nounVerb
is preferred.
List fields
List fields should always be pluralized.
# ❌type Post { comment: [Comment!]!} # ✅type Post { comments: [Comment!]!}
Why? Because it’s common to start out thinking something should be singular, only to later evolve the schema to take a list.
Say we started building out blog a year ago. At the time, we just wanted the ability to display a nice feature image from Unsplash at the top of our posts so we specified a single image: Image
field on our Post
type. Since then, we’ve now decided that this is a bit too limiting and we want to add support for displaying a carousel of feature images.
Before making any changes to support carousels, our schema looks like this:
type Post { "Optional feature image" image: Image}
Changing image
to a list type would break existing consumers, which we want to avoid. The natural evolution from here is to introduce a new field named images
typed like so:
type Post { image: Image @deprecated(reason: "Use images[0] instead") images: [Image!]!}
This works pretty well. You might even opt not to deprecate the image
field, and keep it around long-term as an application of the collection lookup pattern.
This also works in reverse; if we start out with a field images: [Image!]!
then it’s natural for us to add an image: Image
field later on down the line.
When you break pluralization rules, things get messy. Imagine we started out with a field image: [Image!]!
and then decided to add the singular field. There’s no good API surface area available for us to use, which means we need to invent a creative field name. You generally want to avoid being put in this position, because a creative name is almost always going to be unintuitive to developers trying to use the schema.
Enum types
Always suffix enum names with Type
.
# ❌enum PostVisibility { PUBLIC UNLINKED} # ✅enum PostVisibilityType { PUBLIC UNLINKED}
Why? Because down the line you might want to introduce an actual PostVisibility
resource to your schema. For instance:
type PostVisibility { state: PostVisibilityType! updatedAt: DateTime! updatedBy: User!} type Post { # ... visibility: PostVisibility!}
The Type
suffix doesn’t tend to add much friction to the developer experience. Most usage of your schema will come from reading fields (in which case no types need to be specified by the consumer), or from populating an input
argument (in which case the consumer only needs to specify the input object’s type). For certain GraphQL frameworks, this suffix also makes defining your schema a bit more ergonomic. Consider the following example using Nexus:
// services/some-feature.tsexport enum PostVisibility { PUBLIC = 'PUBLIC', UNLINKED = 'UNLINKED',} // graphql/types.tsimport { enumType } from 'nexus'; import { PostVisibility } from '@/services/some-feature'; // we've avoided a name conflict!export const PostVisibilityType = enumType({ // ...});
Input object types
Always suffix input object type names with Input
.
# ❌input PostCreate { title: String! content: String!} # ✅input PostCreateInput { title: String! content: String!}
Why? GraphQL doesn’t have support for namespaces, which means every single one of your schema’s types need to co-exist with each other inside a shared global namespace. Presumably your input types will be named similarly to your object types, which means it is very likely you’ll run into a name collision if you don’t suffix with something:
input Post { title: String! content: String!} type Mutation { # I want to return the created Post here :/ postCreate(input: Post!): ...}
You could work around this by using PostCreate
as the name, but then a common pattern in GraphQL is to reuse input types for both the create and update mutations—leaving you in a weird spot.
# calling this `PostCreate` would be weird# if `postUpdate` can also take it...input PostInput { title: String content: String} type Mutation { postCreate(input: PostInput!): Post postUpdate(id: ID!, input: PostInput!): Post}
If you don’t suffix from the start, you’ll likely end up needing to down the line anyway. And in that case you introduce inconsistency in naming conventions across your input types. You are better off suffixing from day one.
Mutation return types
Result types from a mutation should always be suffixed with Result
, and the object being operated on should be returned under an envelope.
# ❌type Mutation { postCreate( input: PostCreateInput! ): Post} # ✅type PostCreateResult { post: Post} type Mutation { postCreate( input: PostCreateInput! ): PostCreateResult!}
Why? Because nesting like this yields a more flexible path for future evolution. Say we wanted the ability to return typed validation errors from postCreate
; it’s impossible for us to support this if we are returning a raw Post
object and trivially easy to do when we envelope:
enum PostErrorCodeType { EMPTY_TITLE EMPTY_CONTENT # ...} type PostError { code: PostErrorCodeType! message: String!} type PostCreateResult { "Null if post creation failed, look at `errors`" post: Post """ List of reasons why this post could not be created. This is empty if the mutation completed successfully. """ errors: [PostError!]!} type Mutation { postCreate( input: PostCreateInput! ): PostCreateResult!}
Casing style
So long as it’s consistent throughout your API, I don’t think this matters too much. The “conventional” casing style used by most GraphQL APIs looks like this:
- Field names are
camelCase
- Type names are
PascalCase
- Enum values are
ALL_CAPS
- Abbreviations / acronyms are
ALL_CAPS
in type names, andcamelCase
in field names - Fragment names should be
camelCase
, as they are used in the same contexts that field names are
The only point here that I’m particularly opinionated on is the fourth one. Global object identification special-cases the id
field on an interface named Node
, which means if you want to be consistent you can’t have other abbreviated fields in PascalCase
or ALL_CAPS
.
# ❌type Post implements Node { id: ID! # would be consistent with `ID: ID!` contentHTML: HTML! # would be consistent with `id: Id!` contentHtml: Html!} # ✅type Post implements Node { id: ID! contentHtml: HTML!}
You might think that type names being pascal cased is also worth being opinionated about, given that the basic Mutation
, Subscription
, and Query
types are all in pascal case. True, but it’s also possible to opt out of these default type names and provide your own:
schema { query: my_query} type my_query { post(id: ID!): post_type}
This is not particularly idiomatic, but some teams may prefer this if their tech stack generally uses snake cased names. Nothing wrong with this decision, so long as the convention used throughout your schema is consistent.
I will note, though, that the casing recommendations I’ve made in this section are fairly idiomatic across GraphQL APIs. In cases where you are exporting a public API—like we do at Rye—you probably want to align with these naming conventions regardless of your tech stack, solely so your API ‘feels’ the same as other GraphQL APIs that your customers might be using.
Fixing mistakes
Most software companies don’t start out with a principled approach to naming or API design, because in the early days of a company you are desperately trying to find product-market fit and naming conventions just aren’t going to move the dial anywhere near as much as other activities. After finding PMF, it’s not uncommon to end up with a pretty messy API which requires breaking changes to make it more uniform.
One way of achieving this is through my graphql-sunset
library, which makes it easy to leverage the Sunset
header from within a GraphQL service. You can learn more about this library here, where I walk through flagging the removal of a getUser
query field in favor of a new user
query field.