From 4ddf2f4dc64a92c8b41c100b15538e7e847270a9 Mon Sep 17 00:00:00 2001 From: sarahxsanders Date: Sat, 26 Apr 2025 11:23:46 -0400 Subject: [PATCH 1/5] add guides for custom scalars --- website/pages/docs/_meta.ts | 2 + .../pages/docs/advanced-custom-scalars.mdx | 196 ++++++++++++++++++ website/pages/docs/custom-scalars.mdx | 122 +++++++++++ 3 files changed, 320 insertions(+) create mode 100644 website/pages/docs/advanced-custom-scalars.mdx create mode 100644 website/pages/docs/custom-scalars.mdx diff --git a/website/pages/docs/_meta.ts b/website/pages/docs/_meta.ts index 39ac3a1486..3b6981497a 100644 --- a/website/pages/docs/_meta.ts +++ b/website/pages/docs/_meta.ts @@ -19,6 +19,8 @@ const meta = { 'constructing-types': '', 'oneof-input-objects': '', 'defer-stream': '', + 'custom-scalars': '', + 'advanced-custom-scalars': '', '-- 3': { type: 'separator', title: 'FAQ', diff --git a/website/pages/docs/advanced-custom-scalars.mdx b/website/pages/docs/advanced-custom-scalars.mdx new file mode 100644 index 0000000000..34c2cc9ed1 --- /dev/null +++ b/website/pages/docs/advanced-custom-scalars.mdx @@ -0,0 +1,196 @@ +--- +title: Best Practices for Custom Scalars +--- + +# Custom Scalars: Best Practices and Testing + +Custom scalars must behave predictably and clearly. To maintain a consistent, reliable +schema, follow these best practices. + +### Document expected formats and validation + +Provide a clear description of the scalar’s accepted input and output formats. For example, a +`DateTime` scalar should explain that it expects ISO-8601 strings ending with `Z`. + +Clear descriptions help clients understand valid input and reduce mistakes. + +### Validate consistently across `parseValue` and `parseLiteral` + +Clients can send values either through variables or inline literals. +Your `parseValue` and `parseLiteral` functions should apply the same validation logic in +both cases. + +Use a shared helper to avoid duplication: + +```js +function parseDate(value) { + const date = new Date(value); + if (isNaN(date.getTime())) { + throw new TypeError(`DateTime cannot represent an invalid date: ${value}`); + } + return date; +} +``` + +Both `parseValue` and `parseLiteral` should call this function. + +### Return clear errors + +When validation fails, throw descriptive errors. Avoid generic messages like "Invalid input." +Instead, use targeted messages that explain the problem, such as: + +```text +DateTime cannot represent an invalid date: `abc123` +``` + +Clear error messages speed up debugging and make mistakes easier to fix. + +### Serialize consistently + +Always serialize internal values into a predictable format. +For example, a `DateTime` scalar should always produce an ISO string, even if its +internal value is a `Date` object. + +```js +serialize(value) { + if (!(value instanceof Date)) { + throw new TypeError('DateTime can only serialize Date instances'); + } + return value.toISOString(); +} +``` + +Serialization consistency prevents surprises on the client side. + +## Testing custom scalars + +Testing ensures your custom scalars work reliably with both valid and invalid inputs. +Tests should cover three areas: coercion functions, schema integration, and error handling. + +### Unit test serialization and parsing + +Write unit tests for each function: `serialize`, `parseValue`, and `parseLiteral`. +Test with both valid and invalid inputs. + +```js +describe('DateTime scalar', () => { + it('serializes Date instances to ISO strings', () => { + const date = new Date('2024-01-01T00:00:00Z'); + expect(DateTime.serialize(date)).toBe('2024-01-01T00:00:00.000Z'); + }); + + it('throws if serializing a non-Date value', () => { + expect(() => DateTime.serialize('not a date')).toThrow(TypeError); + }); + + it('parses ISO strings into Date instances', () => { + const result = DateTime.parseValue('2024-01-01T00:00:00Z'); + expect(result).toBeInstanceOf(Date); + expect(result.toISOString()).toBe('2024-01-01T00:00:00.000Z'); + }); + + it('throws if parsing an invalid date string', () => { + expect(() => DateTime.parseValue('invalid-date')).toThrow(TypeError); + }); +}); +``` + +### Test custom scalars in a schema + +Integrate the scalar into a schema and run real GraphQL queries to validate end-to-end behavior. + +```js +const { graphql, buildSchema } = require('graphql'); + +const schema = buildSchema(` + scalar DateTime + + type Query { + now: DateTime + } +`); + +const rootValue = { + now: () => new Date('2024-01-01T00:00:00Z'), +}; + +async function testQuery() { + const response = await graphql({ + schema, + source: '{ now }', + rootValue, + }); + console.log(response); +} + +testQuery(); +``` + +Schema-level tests verify that the scalar behaves correctly during execution, not just +in isolation. + +## Common use cases for custom scalars + +Custom scalars solve real-world needs by handling types that built-in scalars don't cover. + +- `DateTime`: Serializes and parses ISO-8601 date-time strings. +- `Email`: Validates syntactically correct email addresses. + +```js +function validateEmail(value) { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(value)) { + throw new TypeError(`Email cannot represent invalid email address: ${value}`); + } + return value; +} +``` + +- `URL`: Ensures well-formatted, absolute URLs. + +```js +function validateURL(value) { + try { + new URL(value); + return value; + } catch { + throw new TypeError(`URL cannot represent an invalid URL: ${value}`); + } +} +``` + +- `JSON`: Represents arbitrary JSON structures, but use carefully because it bypasses +GraphQL's strict type checking. + +## When to use existing libraries + +Writing scalars is deceptively tricky. Validation edge cases can lead to subtle bugs if +not handled carefully. + +Whenever possible, use trusted libraries like `graphql-scalars`. They offer production-ready +scalars for DateTime, EmailAddress, URL, UUID, and many others. + +### Example: Handling email validation + +Handling email validation correctly requires dealing with Unicode, quoted local parts, and +domain validation. Rather than writing your own regex, it’s better to use a library scalar +that's already validated against standards. + +If you need domain-specific behavior, you can wrap an existing scalar with custom rules: + +```js +const { EmailAddressResolver } = require('graphql-scalars'); + +const StrictEmail = new GraphQLScalarType({ + ...EmailAddressResolver, + parseValue(value) { + if (!value.endsWith('@example.com')) { + throw new TypeError('Only example.com emails are allowed.'); + } + return EmailAddressResolver.parseValue(value); + }, +}); +``` + +By following these best practices and using trusted tools where needed, you can build custom +scalars that are reliable, maintainable, and easy for clients to work with. \ No newline at end of file diff --git a/website/pages/docs/custom-scalars.mdx b/website/pages/docs/custom-scalars.mdx new file mode 100644 index 0000000000..044f17f4fe --- /dev/null +++ b/website/pages/docs/custom-scalars.mdx @@ -0,0 +1,122 @@ +--- +title: Using Custom Scalars +--- + +# Custom Scalars: When and How to Use Them + +In GraphQL, scalar types represent primitive data like strings, numbers, and booleans. +The GraphQL specification defines five built-in scalars: `Int`, `Float`, +`String`, `Boolean`, and `ID`. + +However, these default types don't cover all the formats or domain-specific values real-world +APIs often need. For example, you might want to represent a timestamp as an ISO 8601 string, or +ensure a user-submitted field is a valid email address. In these cases, you can define a custom +scalar type. + +In GraphQL.js, custom scalars are created using the `GraphQLScalarType` class. This gives you +full control over how values are serialized, parsed, and validated. + +Here’s a simple example of a custom scalar that handles date-time strings: + +```js +const { GraphQLScalarType, Kind } = require('graphql'); + +const DateTime = new GraphQLScalarType({ + name: 'DateTime', + description: 'An ISO-8601 encoded UTC date string.', + serialize(value) { + return value instanceof Date ? value.toISOString() : null; + }, + parseValue(value) { + return typeof value === 'string' ? new Date(value) : null; + }, + parseLiteral(ast) { + return ast.kind === Kind.STRING ? new Date(ast.value) : null; + }, +}); +``` +Custom scalars offer flexibility, but they also shift responsibility onto you. You're +defining not just the format of a value, but also how it is validated and how it moves +through your schema. + +This guide covers when to use custom scalars and how to define them in GraphQL.js. + +## When to use custom scalars + +Define a custom scalar when you need to enforce a specific format, encapsulate domain-specific +logic, or standardize a primitive value across your schema. For example: + +- Validation: Ensure that inputs like email addresses, URLs, or date strings match a +strict format. +- Serialization and parsing: Normalize how values are converted between internal and +client-facing formats. +- Domain primitives: Represent domain-specific values that behave like scalars, such as +UUIDs or currency codes. + +Common examples of useful custom scalars include: + +- `DateTime`: An ISO 8601 timestamp string +- `Email`: A syntactically valid email address +- `URL`: A well-formed web address +- `BigInt`: An integer that exceeds the range of GraphQL's built-in `Int` +- `UUID`: A string that follows a specific identifier format + +## When not to use a custom scalar + +Custom scalars are not a substitute for object types. Avoid using a custom scalar if: + +- The value naturally contains multiple fields or nested data (even if serialized as a string). +- Validation depends on relationships between fields or requires complex cross-checks. +- You're tempted to bypass GraphQL’s type system using a catch-all scalar like `JSON` or `Any`. + +Custom scalars reduce introspection and composability. Use them to extend GraphQL's scalar +system, not to replace structured types altogether. + +## How to define a custom scalar in GraphQL.js + +In GraphQL.js, a custom scalar is defined by creating an instance of `GraphQLScalarType`, +providing a name, description, and three functions: + +- `serialize`: How the server sends internal values to clients. +- `parseValue`: How the server parses incoming variable values. +- `parseLiteral`: How the server parses inline values in queries. + +The following example is a custom `DateTime` scalar that handles ISO-8601 encoded +date strings: + +```js +const { GraphQLScalarType, Kind } = require('graphql'); + +const DateTime = new GraphQLScalarType({ + name: 'DateTime', + description: 'An ISO-8601 encoded UTC date string.', + + serialize(value) { + if (!(value instanceof Date)) { + throw new TypeError('DateTime can only serialize Date instances'); + } + return value.toISOString(); + }, + + parseValue(value) { + const date = new Date(value); + if (isNaN(date.getTime())) { + throw new TypeError(`DateTime cannot represent an invalid date: ${value}`); + } + return date; + }, + + parseLiteral(ast) { + if (ast.kind !== Kind.STRING) { + throw new TypeError(`DateTime can only parse string values, but got: ${ast.kind}`); + } + const date = new Date(ast.value); + if (isNaN(date.getTime())) { + throw new TypeError(`DateTime cannot represent an invalid date: ${ast.value}`); + } + return date; + }, +}); +``` + +These functions give you full control over validation and data flow. \ No newline at end of file From adaa254235c55e1b5f090e56e1a7429b73d83a66 Mon Sep 17 00:00:00 2001 From: sarahxsanders Date: Mon, 28 Apr 2025 19:31:25 -0400 Subject: [PATCH 2/5] add community resources --- website/pages/docs/advanced-custom-scalars.mdx | 9 ++++++++- website/pages/docs/custom-scalars.mdx | 6 +++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/website/pages/docs/advanced-custom-scalars.mdx b/website/pages/docs/advanced-custom-scalars.mdx index 34c2cc9ed1..e8bb0329aa 100644 --- a/website/pages/docs/advanced-custom-scalars.mdx +++ b/website/pages/docs/advanced-custom-scalars.mdx @@ -193,4 +193,11 @@ const StrictEmail = new GraphQLScalarType({ ``` By following these best practices and using trusted tools where needed, you can build custom -scalars that are reliable, maintainable, and easy for clients to work with. \ No newline at end of file +scalars that are reliable, maintainable, and easy for clients to work with. + +## Additional resources + +- [GraphQL Scalars by The Guild](https://the-guild.dev/graphql/scalars): A production-ready +library of common custom scalars. +- [GraphQL Scalars Specification](https://github.com/graphql/graphql-scalars): This +specification is no longer actively maintained, but useful for historical context. diff --git a/website/pages/docs/custom-scalars.mdx b/website/pages/docs/custom-scalars.mdx index 044f17f4fe..b5d1959867 100644 --- a/website/pages/docs/custom-scalars.mdx +++ b/website/pages/docs/custom-scalars.mdx @@ -119,4 +119,8 @@ const DateTime = new GraphQLScalarType({ }); ``` -These functions give you full control over validation and data flow. \ No newline at end of file +These functions give you full control over validation and data flow. + +## Learn more + +- [Custom Scalars: Best Practices and Testing](./advanced-custom-scalars): Dive deeper into validation, testing, and building production-grade custom scalars. \ No newline at end of file From 762da692cc40016c2359baf7c34ac9320f8a29b2 Mon Sep 17 00:00:00 2001 From: Sarah Sanders <88458517+sarahxsanders@users.noreply.github.com> Date: Tue, 29 Apr 2025 18:11:42 -0400 Subject: [PATCH 3/5] Update website/pages/docs/advanced-custom-scalars.mdx Co-authored-by: Saihajpreet Singh --- website/pages/docs/advanced-custom-scalars.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/pages/docs/advanced-custom-scalars.mdx b/website/pages/docs/advanced-custom-scalars.mdx index e8bb0329aa..7b42a4aae7 100644 --- a/website/pages/docs/advanced-custom-scalars.mdx +++ b/website/pages/docs/advanced-custom-scalars.mdx @@ -10,7 +10,7 @@ schema, follow these best practices. ### Document expected formats and validation Provide a clear description of the scalar’s accepted input and output formats. For example, a -`DateTime` scalar should explain that it expects ISO-8601 strings ending with `Z`. +`DateTime` scalar should explain that it expects [ISO-8601](https://www.iso.org/iso-8601-date-and-time-format.html) strings ending with `Z`. Clear descriptions help clients understand valid input and reduce mistakes. From f94f64daf7e27e07d22b138ee0363265ab148972 Mon Sep 17 00:00:00 2001 From: Sarah Sanders <88458517+sarahxsanders@users.noreply.github.com> Date: Tue, 29 Apr 2025 18:11:51 -0400 Subject: [PATCH 4/5] Update website/pages/docs/advanced-custom-scalars.mdx Co-authored-by: Saihajpreet Singh --- website/pages/docs/advanced-custom-scalars.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/pages/docs/advanced-custom-scalars.mdx b/website/pages/docs/advanced-custom-scalars.mdx index 7b42a4aae7..a7e7119a56 100644 --- a/website/pages/docs/advanced-custom-scalars.mdx +++ b/website/pages/docs/advanced-custom-scalars.mdx @@ -167,7 +167,7 @@ GraphQL's strict type checking. Writing scalars is deceptively tricky. Validation edge cases can lead to subtle bugs if not handled carefully. -Whenever possible, use trusted libraries like `graphql-scalars`. They offer production-ready +Whenever possible, use trusted libraries like [`graphql-scalars`](https://www.npmjs.com/package/graphql-scalars). They offer production-ready scalars for DateTime, EmailAddress, URL, UUID, and many others. ### Example: Handling email validation From 2aa0213762d0bc6e277ec49caa29bd4d95c73a6e Mon Sep 17 00:00:00 2001 From: sarahxsanders Date: Mon, 12 May 2025 20:53:30 -0400 Subject: [PATCH 5/5] add cspell.yaml --- cspell.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/cspell.yml b/cspell.yml index fb16fb4497..4e3247860e 100644 --- a/cspell.yml +++ b/cspell.yml @@ -25,6 +25,7 @@ overrides: - swcrc - noreferrer - xlink + - composability ignoreRegExpList: - u\{[0-9a-f]{1,8}\}