Skip to content

Commit 3110f81

Browse files
sarahxsanderssaihajJoviDeCroock
authored
docs: add guides for custom scalars (#4380)
Co-authored-by: Saihajpreet Singh <[email protected]> Co-authored-by: Jovi De Croock <[email protected]>
1 parent 9a6d8ac commit 3110f81

File tree

4 files changed

+332
-0
lines changed

4 files changed

+332
-0
lines changed

cspell.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ overrides:
2525
- swcrc
2626
- noreferrer
2727
- xlink
28+
- composability
2829
- deduplication
2930

3031
ignoreRegExpList:

website/pages/docs/_meta.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ const meta = {
1919
'constructing-types': '',
2020
'oneof-input-objects': '',
2121
'defer-stream': '',
22+
'custom-scalars': '',
23+
'advanced-custom-scalars': '',
2224
'n1-dataloader': '',
2325
'resolver-anatomy': '',
2426
'graphql-errors': '',
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
---
2+
title: Best Practices for Custom Scalars
3+
---
4+
5+
# Custom Scalars: Best Practices and Testing
6+
7+
Custom scalars must behave predictably and clearly. To maintain a consistent, reliable
8+
schema, follow these best practices.
9+
10+
### Document expected formats and validation
11+
12+
Provide a clear description of the scalar’s accepted input and output formats. For example, a
13+
`DateTime` scalar should explain that it expects [ISO-8601](https://www.iso.org/iso-8601-date-and-time-format.html) strings ending with `Z`.
14+
15+
Clear descriptions help clients understand valid input and reduce mistakes.
16+
17+
### Validate consistently across `parseValue` and `parseLiteral`
18+
19+
Clients can send values either through variables or inline literals.
20+
Your `parseValue` and `parseLiteral` functions should apply the same validation logic in
21+
both cases.
22+
23+
Use a shared helper to avoid duplication:
24+
25+
```js
26+
function parseDate(value) {
27+
const date = new Date(value);
28+
if (isNaN(date.getTime())) {
29+
throw new TypeError(`DateTime cannot represent an invalid date: ${value}`);
30+
}
31+
return date;
32+
}
33+
```
34+
35+
Both `parseValue` and `parseLiteral` should call this function.
36+
37+
### Return clear errors
38+
39+
When validation fails, throw descriptive errors. Avoid generic messages like "Invalid input."
40+
Instead, use targeted messages that explain the problem, such as:
41+
42+
```text
43+
DateTime cannot represent an invalid date: `abc123`
44+
```
45+
46+
Clear error messages speed up debugging and make mistakes easier to fix.
47+
48+
### Serialize consistently
49+
50+
Always serialize internal values into a predictable format.
51+
For example, a `DateTime` scalar should always produce an ISO string, even if its
52+
internal value is a `Date` object.
53+
54+
```js
55+
serialize(value) {
56+
if (!(value instanceof Date)) {
57+
throw new TypeError('DateTime can only serialize Date instances');
58+
}
59+
return value.toISOString();
60+
}
61+
```
62+
63+
Serialization consistency prevents surprises on the client side.
64+
65+
## Testing custom scalars
66+
67+
Testing ensures your custom scalars work reliably with both valid and invalid inputs.
68+
Tests should cover three areas: coercion functions, schema integration, and error handling.
69+
70+
### Unit test serialization and parsing
71+
72+
Write unit tests for each function: `serialize`, `parseValue`, and `parseLiteral`.
73+
Test with both valid and invalid inputs.
74+
75+
```js
76+
describe('DateTime scalar', () => {
77+
it('serializes Date instances to ISO strings', () => {
78+
const date = new Date('2024-01-01T00:00:00Z');
79+
expect(DateTime.serialize(date)).toBe('2024-01-01T00:00:00.000Z');
80+
});
81+
82+
it('throws if serializing a non-Date value', () => {
83+
expect(() => DateTime.serialize('not a date')).toThrow(TypeError);
84+
});
85+
86+
it('parses ISO strings into Date instances', () => {
87+
const result = DateTime.parseValue('2024-01-01T00:00:00Z');
88+
expect(result).toBeInstanceOf(Date);
89+
expect(result.toISOString()).toBe('2024-01-01T00:00:00.000Z');
90+
});
91+
92+
it('throws if parsing an invalid date string', () => {
93+
expect(() => DateTime.parseValue('invalid-date')).toThrow(TypeError);
94+
});
95+
});
96+
```
97+
98+
### Test custom scalars in a schema
99+
100+
Integrate the scalar into a schema and run real GraphQL queries to validate end-to-end behavior.
101+
102+
```js
103+
const { graphql, buildSchema } = require('graphql');
104+
105+
const schema = buildSchema(`
106+
scalar DateTime
107+
108+
type Query {
109+
now: DateTime
110+
}
111+
`);
112+
113+
const rootValue = {
114+
now: () => new Date('2024-01-01T00:00:00Z'),
115+
};
116+
117+
async function testQuery() {
118+
const response = await graphql({
119+
schema,
120+
source: '{ now }',
121+
rootValue,
122+
});
123+
console.log(response);
124+
}
125+
126+
testQuery();
127+
```
128+
129+
Schema-level tests verify that the scalar behaves correctly during execution, not just
130+
in isolation.
131+
132+
## Common use cases for custom scalars
133+
134+
Custom scalars solve real-world needs by handling types that built-in scalars don't cover.
135+
136+
- `DateTime`: Serializes and parses ISO-8601 date-time strings.
137+
- `Email`: Validates syntactically correct email addresses.
138+
139+
```js
140+
function validateEmail(value) {
141+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
142+
if (!emailRegex.test(value)) {
143+
throw new TypeError(`Email cannot represent invalid email address: ${value}`);
144+
}
145+
return value;
146+
}
147+
```
148+
149+
- `URL`: Ensures well-formatted, absolute URLs.
150+
151+
```js
152+
function validateURL(value) {
153+
try {
154+
new URL(value);
155+
return value;
156+
} catch {
157+
throw new TypeError(`URL cannot represent an invalid URL: ${value}`);
158+
}
159+
}
160+
```
161+
162+
- `JSON`: Represents arbitrary JSON structures, but use carefully because it bypasses
163+
GraphQL's strict type checking.
164+
165+
## When to use existing libraries
166+
167+
Writing scalars is deceptively tricky. Validation edge cases can lead to subtle bugs if
168+
not handled carefully.
169+
170+
Whenever possible, use trusted libraries like [`graphql-scalars`](https://www.npmjs.com/package/graphql-scalars). They offer production-ready
171+
scalars for DateTime, EmailAddress, URL, UUID, and many others.
172+
173+
### Example: Handling email validation
174+
175+
Handling email validation correctly requires dealing with Unicode, quoted local parts, and
176+
domain validation. Rather than writing your own regex, it’s better to use a library scalar
177+
that's already validated against standards.
178+
179+
If you need domain-specific behavior, you can wrap an existing scalar with custom rules:
180+
181+
```js
182+
const { EmailAddressResolver } = require('graphql-scalars');
183+
184+
const StrictEmail = new GraphQLScalarType({
185+
...EmailAddressResolver,
186+
parseValue(value) {
187+
if (!value.endsWith('@example.com')) {
188+
throw new TypeError('Only example.com emails are allowed.');
189+
}
190+
return EmailAddressResolver.parseValue(value);
191+
},
192+
});
193+
```
194+
195+
By following these best practices and using trusted tools where needed, you can build custom
196+
scalars that are reliable, maintainable, and easy for clients to work with.
197+
198+
## Additional resources
199+
200+
- [GraphQL Scalars by The Guild](https://the-guild.dev/graphql/scalars): A production-ready
201+
library of common custom scalars.
202+
- [GraphQL Scalars Specification](https://github.com/graphql/graphql-scalars): This
203+
specification is no longer actively maintained, but useful for historical context.

website/pages/docs/custom-scalars.mdx

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
---
2+
title: Using Custom Scalars
3+
---
4+
5+
# Custom Scalars: When and How to Use Them
6+
7+
In GraphQL, scalar types represent primitive data like strings, numbers, and booleans.
8+
The GraphQL specification defines five built-in scalars: `Int`, `Float`,
9+
`String`, `Boolean`, and `ID`.
10+
11+
However, these default types don't cover all the formats or domain-specific values real-world
12+
APIs often need. For example, you might want to represent a timestamp as an ISO 8601 string, or
13+
ensure a user-submitted field is a valid email address. In these cases, you can define a custom
14+
scalar type.
15+
16+
In GraphQL.js, custom scalars are created using the `GraphQLScalarType` class. This gives you
17+
full control over how values are serialized, parsed, and validated.
18+
19+
Here’s a simple example of a custom scalar that handles date-time strings:
20+
21+
```js
22+
const { GraphQLScalarType, Kind } = require('graphql');
23+
24+
const DateTime = new GraphQLScalarType({
25+
name: 'DateTime',
26+
description: 'An ISO-8601 encoded UTC date string.',
27+
serialize(value) {
28+
return value instanceof Date ? value.toISOString() : null;
29+
},
30+
parseValue(value) {
31+
return typeof value === 'string' ? new Date(value) : null;
32+
},
33+
parseLiteral(ast) {
34+
return ast.kind === Kind.STRING ? new Date(ast.value) : null;
35+
},
36+
});
37+
```
38+
Custom scalars offer flexibility, but they also shift responsibility onto you. You're
39+
defining not just the format of a value, but also how it is validated and how it moves
40+
through your schema.
41+
42+
This guide covers when to use custom scalars and how to define them in GraphQL.js.
43+
44+
## When to use custom scalars
45+
46+
Define a custom scalar when you need to enforce a specific format, encapsulate domain-specific
47+
logic, or standardize a primitive value across your schema. For example:
48+
49+
- Validation: Ensure that inputs like email addresses, URLs, or date strings match a
50+
strict format.
51+
- Serialization and parsing: Normalize how values are converted between internal and
52+
client-facing formats.
53+
- Domain primitives: Represent domain-specific values that behave like scalars, such as
54+
UUIDs or currency codes.
55+
56+
Common examples of useful custom scalars include:
57+
58+
- `DateTime`: An ISO 8601 timestamp string
59+
- `Email`: A syntactically valid email address
60+
- `URL`: A well-formed web address
61+
- `BigInt`: An integer that exceeds the range of GraphQL's built-in `Int`
62+
- `UUID`: A string that follows a specific identifier format
63+
64+
## When not to use a custom scalar
65+
66+
Custom scalars are not a substitute for object types. Avoid using a custom scalar if:
67+
68+
- The value naturally contains multiple fields or nested data (even if serialized as a string).
69+
- Validation depends on relationships between fields or requires complex cross-checks.
70+
- You're tempted to bypass GraphQL’s type system using a catch-all scalar like `JSON` or `Any`.
71+
72+
Custom scalars reduce introspection and composability. Use them to extend GraphQL's scalar
73+
system, not to replace structured types altogether.
74+
75+
## How to define a custom scalar in GraphQL.js
76+
77+
In GraphQL.js, a custom scalar is defined by creating an instance of `GraphQLScalarType`,
78+
providing a name, description, and three functions:
79+
80+
- `serialize`: How the server sends internal values to clients.
81+
- `parseValue`: How the server parses incoming variable values.
82+
- `parseLiteral`: How the server parses inline values in queries.
83+
84+
The following example is a custom `DateTime` scalar that handles ISO-8601 encoded
85+
date strings:
86+
87+
```js
88+
const { GraphQLScalarType, Kind } = require('graphql');
89+
90+
const DateTime = new GraphQLScalarType({
91+
name: 'DateTime',
92+
description: 'An ISO-8601 encoded UTC date string.',
93+
94+
serialize(value) {
95+
if (!(value instanceof Date)) {
96+
throw new TypeError('DateTime can only serialize Date instances');
97+
}
98+
return value.toISOString();
99+
},
100+
101+
parseValue(value) {
102+
const date = new Date(value);
103+
if (isNaN(date.getTime())) {
104+
throw new TypeError(`DateTime cannot represent an invalid date: ${value}`);
105+
}
106+
return date;
107+
},
108+
109+
parseLiteral(ast) {
110+
if (ast.kind !== Kind.STRING) {
111+
throw new TypeError(`DateTime can only parse string values, but got: ${ast.kind}`);
112+
}
113+
const date = new Date(ast.value);
114+
if (isNaN(date.getTime())) {
115+
throw new TypeError(`DateTime cannot represent an invalid date: ${ast.value}`);
116+
}
117+
return date;
118+
},
119+
});
120+
```
121+
122+
These functions give you full control over validation and data flow.
123+
124+
## Learn more
125+
126+
- [Custom Scalars: Best Practices and Testing](./advanced-custom-scalars): Dive deeper into validation, testing, and building production-grade custom scalars.

0 commit comments

Comments
 (0)