GraphQL is a language and set of technologies that make querying data easier for modern frontend applications. Named the technology developers are most interested in learning in the “2019 State of JavaScript” report, GraphQL is recognized as a scalable and production-ready tool that is used by many industry giants such as Github, Netflix, and Shopify, to name a few.
While most features of GraphQL are well-understood (e.g., directives, input types, resolvers), enum types are often underrated and their usefulness largely unknown. In this tutorial, we’ll shed light on why enums are important and explore the essential role this simple type plays in building robust GraphQL APIs.
GraphQL APIs rely on GraphQL schemas to expose data. GraphQL provides it owns scalar types (Int
, Float
, String
, Boolean
, and ID
) and lets you create your own data types using the following keywords.
type
to define complex object types used by queries (read data)input
to define input types used as arguments by mutations (create, update, delete data)We’ll base all our examples on the following schema describing a user management API.
# A User (profile) is linked to some Accounts that carries all authentication information type Account { id: ID! # "!" indicate a non-null (mandatory) field authType: String! # "google-auth", "github-auth", "outlook-auth" email: String! token: String archived: Boolean! disabled: Boolean! verified: Boolean! } type User { id: ID! accounts: [Account!]! # (a non-null array of non-null Account values) givenName: String! } # a single query is exposed to list all users and filter by status and authType type Query { users(first: Int!, from: ID, status: String, authType: String): [User!]! } # we define an "input" type for our updateAccount mutation input UpdateAccountInput { id: ID! state: String! # "verified", "disabled", "archived" } type Mutation { updateAccount(input: UpdateAccountInput!): Account! }
Our GraphQL API allows us to list all users with some optional filters.
status
, of String
type, allows us to filter the multiple boolean fields of User
using a simple String
value: “verified”, “disabled”, “archived”
authType
is based on Account.authType
The API uses the Account.authType
value (“google-auth”, “github-auth”, “outlook-auth”
) internally to leverage the proper provider to authenticate but also to get user profile information. “google-auth”
leads to the appropriate logic under the GoogleProfileProvide
namespace.
We can get the first 10 active users to use Google by doing the following.
user(first: 10, authType: "google-auth", status: "verified") { id # ... }
Our API also exposes an updateAccount
mutation, which allows us to update an account status during its lifecycle.
This schema is functional as-is. However, we can make it way more robust by leveraging GraphQL enums.
Although when using String
, Account.authType
, and UpdateAccountInput.state
we can expect to receive specific values, this can introduce issues at multiple levels.
Below is an example of how you might use the users
query.
query { users(first: 10, status: "Verified") { id } }
GraphQL-wise, on the client side (e.g., Apollo), this query is entirely valid. However, can you spot the typo?
The status
value “Verified”
, should be written as “verified”
.
Having a free-form String
type here might trigger unexpected 500s, silent errors, or, worse, security vulnerabilities.
To prevent this, we’ll have to add some validation logic to our users
query resolver.
interface UsersArgs { first: number, from?: string, status?: String, authType?: string, } const STATUSES = ['', '', ] export const resolvers = { Query: { users: (_record: never, args: UsersArgs, _context: never) { if (args?.status && !STATUSES.includes(args.status)) { // throw error } // ... } } }
This is quite counterproductive since the principal value of GraphQL type-based schemas is to provide validation out of the box on operations and data. Using enums will limit the status value to a given set of defined values and avoid duplicate validations logic.
We saw that Account.authType
relys on a String
type, which leads to the following kind of users
query.
query { users(first: 10, authType: "google-auth") { id } }
Such a pattern is not optimal because exposing internal values goes against the principle that APIs should expose a simple, decoupled interface with data.
Again, we can improve our users
query resolver to allows public-friendly values for filtering.
interface UsersArgs { first: number, from?: string, status?: String, authType?: string, } const AUTH_TYPES_FILTER_MAP = { 'GOOGLE': 'google-auth', 'GITHUB': 'github-auth, 'OUTLOOK': 'outlook-auth', } export const resolvers = { Query: { users: (_record: never, args: UsersArgs, _context: never) { // does both mapping and validation (invalid value will be ignored) const authType = AUTH_TYPES_FILTER_MAP[args.authType] // ... } } }
This would allow the following query.
query { users(first: 10, authType: "GOOGLE") { id } }
However, here again, we have to build a feature already provided by GraphQL via enums.
In the next section, we’ll walk through how to map enums to internal values by migrating our schema to enums.
The last downside of relying on the String
type for filter- or choice-based values is discoverability.
If you have an UpdateAccountInput.status
that relies on the String
type, GraphQL won’t be able to inspect the possible values of UpdateAccountInput.status
during the introspection query since only the resolvers know about them.
GraphQL introspection is one of the core features of GraphQL that allows clients to dynamically inspect the schema definition by doing a GraphQL query (inception).
Here’s an example listing all the queries defined on our schema:
query { __schema { queryType { fields { name args { name type { name description } } description deprecationReason } } } } { "data": { "__schema": { "queryType": { "fields": [ { "args": [ { "name": "authType", "type": { "name": "String", "description": null } }, { "name": "first", "type": { "name": null, "description": null } }, { "name": "from", "type": { "name": "ID", "description": null } }, { "name": "status", "type": { "name": "String", "description": null } } ], "deprecationReason": null, "name": "users", "description": "perform the action: \"users\"" } ] } } } }
Tools like GraphiQL use introspection to provide autocomplete functionality. GraphQL Code Generator also uses it to generate TypeScript types.
String
types do not allow us to inspect the possible values.
GraphQL enums are introspectable, allowing the app to autocomplete possible values and generate corresponding TypeScript types.
Relying on String
may lead to security issues, unnecessary validation logic on the resolvers side, and a poor developer experience.
Let’s make our GraphQL Schema more robust by migrating to enums.
GraphQL enums can be defined using the enum
keyword.
enum AuthType { GOOGLE GITHUB OUTLOOK }
According to the official specification, you should name enums values using the all-caps convention. Enum values don’t need any common prefix since they are namespaced by the enum’s name.
Let’s take a look at our new enums-based schema:
enum AuthType { GOOGLE GITHUB OUTLOOK } # A User (profile) is linked to some Accounts that carries all authentication information type Account { id: ID! # "!" indicate a non-null (mandatory) field authType: AuthType! email: String! token: String archived: Boolean! disabled: Boolean! verified: Boolean! } type User { id: ID! accounts: [Account!]! # (a non-null array of non-null Account values) givenName: String! } # a single query is exposed to list all users and filter by status and authType type Query { users(first: Int!, from: ID, status: String, authType: String): [User!]! } enum AccountState { VERIFIED, DISABLED, ARCHIVED } # we define an "input" type for our updateAccount mutation input UpdateAccountInput { id: ID! state: AccountState! } type Mutation { updateAccount(input: UpdateAccountInput!): Account! }
Enum internal values
Enum value resolution (at resolver and client side) is an implementation detail, meaning that each library chooses how to resolve a given enum value to a language-specific value.
With Apollo Server, each enum value will resolve to its string equivalent.
AuthType.GOOGLE
resolves to “GOOGLE”
AuthType.GITHUB
resolves to “GITHUB”
AuthType.OUTLOOK
resolves to “OUTLOOK”
However, our API expected manipulated internal values (“google-auth”, “github-auth”, “outlook-auth”
) to operate appropriately.
Per the official documentation, Apollo Server enables you to provide enum resolvers, as shown below with our updated users
query resolver.
JavaScript version:
export const resolvers = { AuthType: { GOOGLE: 'google-auth', GITHUB: 'github-auth', OUTLOOK: 'outlook-auth', }, Query: { users: (_record, args, _context) { // args.authType will always be 'google-auth' or 'github-auth' or 'outlook-auth' // ... } } }
TypeScript version:
enum AuthType { GOOGLE = 'google-auth', GITHUB = 'github-auth', OUTLOOK = 'outlook-auth', } interface UsersArgs { first: number, from?: string, status?: String, authType?: AuthType, } export const resolvers = { AuthType, Query: { users: (_record: never, args: UsersArgs, _context: never) { // args.authType will always be 'google-auth' or 'github-auth' or 'outlook-auth' // ... } } }
Note that with TypeScript, an enum type can be directly passed as a resolver.
Apollo Server will resolve enum values to their proper internal values (resolvers.AuthType
) for both input and output data (Mutation
arguments and Query
returned data).
Now that our schema relies on enums, let’s see how it improves our queries and mutations usage.
We can now query our users and filter using our AuthType
enum.
query { users (first: 10, authType: GOOGLE) { id } }
Our query is now safe, typoless, and fully discoverable.
But the enum value as String
is not valid. The field using an enum type requires an enum reference, so passing the enum value isn’t considered valid.
GraphQL enums are introspectable, as shown below.
Introspectable enums mean that tools like GraphQL Code Generator will generate the corresponding TypeScript enum type for your frontend application.
With our schema, GraphQL code generator will be able to create the corresponding TypeScript enum:
export enum AuthType { GOOGLE = 'google-auth', GITHUB = 'github-auth', OUTLOOK = 'outlook-auth', }
This will come in handy for later use (useQuery()
, useMutation()
):
const ActiveUsersList = () => { const { loading, data } = useQuery( usersQueryDocument, { variables: { first: 10, authType: AuthType.GOOGLE, } }) // ... }
Relying on both GraphQL enums and TypeScript-generated enums makes for a robust and powerful developer experience.
Even on small GraphQL APIs, such as the User Management API, enums bring a lot of consistency and make your schema more robust.
In this tutorial, we explored how GraphQL enums can help:
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your site. Instead of guessing why problems happen, you can aggregate and report on problematic GraphQL requests to quickly understand the root cause. In addition, you can track Apollo client state and inspect GraphQL queries' key-value pairs.
LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. Start monitoring for free.Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.
4 Replies to "What you need to know about GraphQL enums"
Great write-up, thank you!
I have a question though: It isn’t clear to me how the client side can know the internal values for the enum. Since Apollo has devs define those internal values in a resolver, they don’t show up in the server schema itself, and therefore when you tell codegen to run for the client, it can’t know about the internal values. Am I missing some helpful setting or something? Do you have a complete version of your example in a repo?
Hi Micah,
Thank you for you comment!
Although I’m not an expert of Apollo Server inner working, I do believe that Apollo Server is using enums “internal values” passed as resolvers to build the schema and pass them as default value to graphql-js (https://github.com/graphql/graphql-js/issues/1604)
Since those enum internal values are passed to the Schema definition “magically” by Apollo Server, it explains how you end-up having them in the introspection query results used by tools like GraphQL code generator.
Please let me know if it answers your question!
Thanks for this excellent article!
You left out one important thing. Enums can’t be nullable.