Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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](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.

### 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`](https://www.npmjs.com/package/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.