-
Notifications
You must be signed in to change notification settings - Fork 2.1k
docs: add guides for custom scalars #4380
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 2 commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
4ddf2f4
add guides for custom scalars
sarahxsanders adaa254
add community resources
sarahxsanders 762da69
Update website/pages/docs/advanced-custom-scalars.mdx
sarahxsanders f94f64d
Update website/pages/docs/advanced-custom-scalars.mdx
sarahxsanders 5e7378c
Merge branch '16.x.x' into custom-scalars
JoviDeCroock 2aa0213
add cspell.yaml
sarahxsanders a6befc0
Merge branch '16.x.x' into custom-scalars
JoviDeCroock File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,203 @@ | ||
--- | ||
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 | ||
sarahxsanders marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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. | ||
|
||
## 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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
--- | ||
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. | ||
|
||
## Learn more | ||
|
||
- [Custom Scalars: Best Practices and Testing](./advanced-custom-scalars): Dive deeper into validation, testing, and building production-grade custom scalars. |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.