Skip to content

Leveraging GraphQL Scalars to Enhance Your Schema

Leveraging GraphQL Scalars to Enhance Your Schema

Introduction

GraphQL has revolutionized the way developers approach application data and API layers, gaining well-deserved momentum in the tech world. Yet, for all its prowess, there's room for enhancement, especially when it comes to its scalar types. By default, GraphQL offers a limited set of these primitives — Int, Float, String, Boolean, and ID — that underpin every schema. While these types serve most use cases, there are scenarios where they fall short, leading developers to yearn for more specificity in their schemas.

Enter graphql-scalars, a library designed to bridge this gap. By supplementing GraphQL with a richer set of scalar types, this tool allows for greater precision and flexibility in data representation. In this post, we'll unpack the potential of enhanced Scalars, delve into the extended capabilities provided by graphql-scalars, and demonstrate its transformative power using an existing starter project. Prepare to redefine the boundaries of what your GraphQL schema can achieve.

Benefits of Using Scalars

GraphQL hinges on the concept of "types." Scalars, being the foundational units of GraphQL's type system, play a pivotal role. While the default Scalars — Int, Float, String, Boolean, and ID — serve many use cases, there's an evident need for more specialized types in intricate web development scenarios.

  1. Precision: Using default Scalars can sometimes lack specificity. Consider representing a date or time in your application with a String; this might lead to ambiguities in format interpretation and potential inconsistencies.
  2. Validation: Specialized scalar types introduce inherent validation. Instead of using a String for an email or a URL, for example, distinct types ensure the data meets expected formats at the query level itself.
  3. Expressiveness: Advanced Scalars provide clearer intentions. They eliminate ambiguity inherent in generic types, making the schema more transparent and self-explanatory.

Acknowledging the limitations of the default Scalars, tools like graphql-scalars have emerged. By broadening the range of available data types, graphql-scalars allows developers to describe their data with greater precision and nuance.

Demonstrating Scalars in Action with Our Starter Project

To truly grasp the transformative power of enhanced Scalars, seeing them in action is pivotal. For this, we'll leverage a popular starter kit: the Serverless framework with Apollo and Contentful. This kit elegantly blends the efficiency of serverless functions with the power of Apollo's GraphQL and Contentful's content management capabilities.

Setting Up the Starter:

  1. Initialize the Project:
npm create @this-dot/starter -- --kit serverless-framework-apollo-contentful
  1. When prompted, name your project enhance-with-graphql-scalars.
Welcome to starter.dev! (create-starter)
✔ What is the name of your project? … enhance-with-graphql-scalars
> Downloading starter kit...
✔ Done!

Next steps:
 cd enhance-with-graphql-scalars
 npm install (or pnpm install, yarn, etc)
  1. For a detailed setup, including integrating with Contentful and deploying your serverless functions, please follow the comprehensive guide provided in the starter kit here.
  2. And we add the graphql-scalars package
npm install graphql-scalars

Enhancing with graphql-scalars:

Dive into the technology.typedefs.ts file, which is the beating heart of our GraphQL type definitions for the project. Initially, these are the definitions we encounter:

export const technologyTypeDefs = gql`
	type Technology {
		id: ID!
		displayName: String!
		description: String
		url: URL
	}

	type Query {
		"Technology: GET"
		technology(id: ID!): Technology
		technologies(offset: Int, limit: Int): [Technology!]
	}

	type Mutation {
		"Technology: create, read and delete operations"
		createTechnology(displayName: String!, description: String, url: String): Technology
		updateTechnology(id: ID!, fields: TechnologyUpdateFields): Technology
		deleteTechnology(id: ID!): ID
	}

	input TechnologyUpdateFields {
		"Mutable fields of a technology entity"
		displayName: String
		description: String
		url: String
	}
`;

Our enhancement strategy is straightforward:

  • Convert the url field from a String to the URL scalar type, bolstering field validation to adhere strictly to the URL format.

Post-integration of graphql-scalars, and with our adjustments, the revised type definition emerges as:

export const technologyTypeDefs = gql`
	type Technology {
		id: ID!
		displayName: String!
		description: String
		url: URL
	}

	type Query {
		"Technology: GET"
		technology(id: ID!): Technology
		technologies(offset: Int, limit: Int): [Technology!]
	}

	type Mutation {
		"Technology: create, read and delete operations"
		createTechnology(displayName: String!, description: String, url: URL): Technology
		updateTechnology(id: ID!, fields: TechnologyUpdateFields): Technology
		deleteTechnology(id: ID!): ID
	}

	input TechnologyUpdateFields {
		"Mutable fields of a technology entity"
		displayName: String
		description: String
		url: URL
	}
`;

To cap it off, we integrate the URL type definition along with its resolvers (sourced from graphql-scalars) in the schema/index.ts file:

import { mergeResolvers, mergeTypeDefs } from '@graphql-tools/merge';
import { technologyResolvers, technologyTypeDefs } from './technology';
import { URLResolver, URLTypeDefinition } from 'graphql-scalars';

const graphqlScalars = [URLTypeDefinition];

export const typeDefs = mergeTypeDefs([...graphqlScalars, technologyTypeDefs]);

export const resolvers = mergeResolvers([{ URL: URLResolver }, technologyResolvers]);

This facelift doesn't just refine our GraphQL schema but infuses it with innate validation, acting as a beacon for consistent and accurate data.

Testing in the GraphQL Sandbox

Time to witness our changes in action within the GraphQL sandbox. Ensure your local server is humming along nicely.

Kick off with verifying the list query:

query {
  technologies {
    id
    displayName
    url
  },
}

Output:

{
  "data": {
    "technologies": [
      {
        "id": "4UXuIqJt75kcaB6idLMz3f",
        "displayName": "GraphQL",
        "url": "https://graphql.framework.dev/"
      },
      {
        "id": "5nOshyir74EmqY4Jtuqk2L",
        "displayName": "Node.js",
        "url": "https://nodejs.framework.dev/"
      },
      {
        "id": "5obCOaxbJql6YBeXmnlb5n",
        "displayName": "Express",
        "url": "https://www.npmjs.com/package/express"
      }
    ]
  }
}

Success! Each url in our dataset adheres to the pristine URL format. Any deviation would've slapped us with a format error.

Now, let's court danger. Attempt to update the url field with a wonky format:

mutation {
  updateTechnology(id: "4UXuIqJt75kcaB6idLMz3f", fields: { url: "aFakeURLThatShouldThrowError" }) {
    id
    displayName
    url
  }
}

As anticipated, the API throws up a validation roadblock:

{
  "data": {},
  "errors": [
    {
      "message": "Expected value of type \"URL\", found \"aFakeURLThatShouldThrowError\"; Invalid URL",
      "locations": [
        {
          "line": 18,
          "column": 65
        }
      ],
      "extensions": {
        "code": "GRAPHQL_VALIDATION_FAILED",
        "stacktrace": [
          "TypeError [ERR_INVALID_URL]: Invalid URL",
          "    at new NodeError (node:internal/errors:399:5)",
          "    at new URL (node:internal/url:560:13)",
          ...
        ]
      }
    }
  ]
}

For the final act, re-run the initial query to reassure ourselves that the original dataset remains untarnished.

Conclusion

Enhancing your GraphQL schemas with custom scalars not only amplifies the robustness of your data structures but also streamlines validation and transformation processes. By setting foundational standards at the schema level, we ensure error-free, consistent, and meaningful data exchanges right from the start.

The graphql-scalars library offers an array of scalars that address common challenges developers face. Beyond the URL scalar we explored, consider diving into other commonly used scalars such as:

  • DateTime: Represents date and time in the ISO 8601 format.
  • Email: Validates strings as email addresses.
  • PositiveInt: Ensures integer values are positive.
  • NonNegativeFloat: Guarantees float values are non-negative.

As a potential next step, consider crafting your own custom scalars tailored to your project's specific requirements. Building a custom scalar not only offers unparalleled flexibility but also provides deeper insights into GraphQL's inner workings and its extensibility.

Remember, while GraphQL is inherently powerful, the granular enhancements like scalars truly elevate the data-fetching experience for developers and users alike. Always evaluate your project's needs and lean into enhancements that bring the most value.

To richer and more intuitive GraphQL schemas!