Skip to content

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

Open
wants to merge 4 commits into
base: 16.x.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions website/pages/docs/_meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ const meta = {
'constructing-types': '',
'oneof-input-objects': '',
'defer-stream': '',
'custom-scalars': '',
'advanced-custom-scalars': '',
'-- 3': {
type: 'separator',
title: 'FAQ',
Expand Down
203 changes: 203 additions & 0 deletions website/pages/docs/advanced-custom-scalars.mdx
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
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.
126 changes: 126 additions & 0 deletions website/pages/docs/custom-scalars.mdx
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.
Loading