The complete GraphQL scalar guide
Scalar type is one of the most important GraphQL concepts to understand. Knowing how scalar works and using the right tools can help us build type safe, robust and extendable schemas. In this article, we cover various essential scalar topics:
- What is GraphQL scalar?
- Create custom GraphQL scalar
- GraphQL scalar tools for type safety
What is GraphQL scalar?
GraphQL scalar type is a primitive type that represents a value in the input or output field’s return type. GraphQL comes with a few native scalars: ID
, Int
, Float
, String
, Boolean
. However, custom scalars can be declared in schemas to represent more complex data types.
Scalar value coercion
One important concept of GraphQL scalar is coercion. This happens when a scalar is used as either input or output. Coercion is the process of taking the incoming value, which could be one or many types, and turning it into one outgoing type.
Let’s take the native ID
scalar as an example:
- When used as input: a client can send either
string
ornumber
value and the value is coerced intostring
before it gets to a resolver on the server. - When used as output: a server can return
string
ornumber
and it is coerced tostring
before it is sent to the client.
Here’s the full list of native scalar input and output TypeScript types:
Custom GraphQL scalar
As our schemas evolve, we may need to present more complex data types with custom validation rules. We can create custom GraphQL scalars to solve this use case.
There are many examples of popular custom scalars that are used in a lot of projects:
DateTime
: string representing an exact point in timeBigInt
: representing values which are too large to be represented bynumber
. Similar to JavaScript'sbigint
typeEmailAddress
: string representing valid email formats
There are 3 steps to create a custom GraphQL scalar:
1. Declare custom scalar in the schema
We can declare custom GraphQL scalars using the scalar
keyword:
scalar CustomScalar
2. Create custom scalar resolver
Now, we can create a resolver for the scalar using GraphQLScalarType
from the graphql
package:
# Install `graphql` package if it's not installed already
yarn add graphql
// resolvers/CustomScalar.ts
import { GraphQLScalarType } from 'graphql'
export const CustomScalar = new GraphQLScalarType({
name: 'CustomScalar',
description: 'Custom Scalar description',
parseValue(inputValue: unknown) {
// (1) Used for input
},
parseLiteral(ast) {
// (2) Used for input
},
serialize(outputValue: unknown) {
// (3) Used for output
}
})
There are 3 main functions to keep in mind when creating a custom scalar:
1. parseValue
: This function is called when the scalar is used as a variable in an input. The validated and returned value of this function is passed to resolvers. Below is an example of a GraphQL operation that would trigger parseValue
:
query Example($var: CustomScalar!) {
example(arg: $var) # a variable is used so `parseValue` is called on the server
}
2. parseLiteral
: This function is called when the scalar is used as a literal value in an input. The ast
parameter contains the GraphQL kind (e.g. int or string) and the value of the input. The validated and returned value of this function is passed to resolvers. Below is an example of a GraphQL operation that would trigger parseLiteral
:
query Example {
example(arg: "Hello") # a literal string is used so `parseLiteral` is called on the server
}
3. serialize
: This function is called on the output returned by resolvers, before the value is sent to the client. Since most GraphQL servers send results as JSON over HTTP, the return value of the serialize
function must turn the value into string
, number
or boolean
.
Check out this visualisation of the flow of a scalar value from the client to the server, and then back to the client:
- When a scalar is used as input, the variable or literal value is parsed using
parseValue
orparseLiteral
respectively. - The parsed value reaches the second param (usually called
args
) of the receiving resolver. - If the scalar is used as output, the value reaches the first param (usually called
parent
) of the receiving resolver. - Once ready to be returned to the client, the scalar value is sent to the scalar resolver’s
serialize
function to be turned into JSON-compatible values. - The serialised scalar value is sent back to the client.
3. Add custom scalar to resolver map
Finally, we can add the custom scalar resolver to the resolvers map in the GraphQL server. Here’s an example of how to do it with GraphQL Yoga:
# Install `graphql-yoga` to create a GraphQL server
yarn add graphql-yoga
import { createServer } from 'http'
import { createSchema, createYoga } from 'graphql-yoga'
// 1. Import the custom resolver
import { CustomScalar } from './resolvers/CustomScalar.ts'
const yoga = createYoga({
schema: createSchema({
typeDefs /* GraphQL typeDefs */,
resolvers: {
CustomScalar // 2. Put the custom scalar resolver into resolvers map
}
})
})
const server = createServer(yoga)
server.listen(4000, () => {
console.info('Server is running on http://localhost:4000/graphql')
})
Now, running the server allows us to use custom scalars in our project. 🎉
GraphQL scalar tools for type safety
There are lots of tools in the GraphQL ecosystem to support working with both native and custom scalars:
1. GraphQL Code Generator
GraphQL Code Generator (Codegen) is a CLI tool with an extensive plugin ecosystem to make implementing both GraphQL client and server easier and safer.
For scalars, GraphQL Code Generator can generate input and output types correctly. On top of that, we can customise the types for both native and custom types to fit our needs.
Codegen config for clients
typescript is the plugin to generate the base TypeScript types, including the Scalars
type for both native and custom scalars. This type is then used by other client plugins like typescript-operations .
First, install GraphQL Code Generator CLI and typescript
plugin:
yarn add -D @graphql-codegen/cli @graphql-codegen/typescript
Here’s an example config that generates native scalar types and CustomScalar
scalar for clients:
// codegen.ts
import type { CodegenConfig } from '@graphql-codegen/cli'
const config: CodegenConfig = {
schema: '**/schema.graphql',
generates: {
'src/schema/types.generated.ts': {
plugins: ['typescript'],
config: {
scalars: {
// Recommended ID scalar type for clients:
ID: {
input: 'string | number',
output: 'string'
},
// Setting custom scalar type:
CustomScalar: {
input: 'string', // this means our server can take CustomScalar as string
output: 'number' // this means our server will return CustomScalar as number
}
}
}
}
}
}
export default config
Running yarn graphql-codegen
generates the following scalar type:
// src/schema/types.generated.ts
export type Scalars = {
ID: { input: string | number; output: string };
String: { input: string; output: string };
Boolean: { input: boolean; output: boolean };
Int: { input: number; output: number };
Float: { input: number; output: number };
CustomScalar: { input: string; output: number };
};
By default, the typescript
plugin generates string
for both ID input and output. This approach allows the plugin to be used for both client and server type generation without extra configuration.
On the other hand, using the above recommended config to generate type that’s closer to GraphQL’s native behaviour (i.e. clients can send either string
or number
as ID input) can make the experience better. A lot of systems use number
as the type for ID of an object. If the recommended config is used for these cases, we don't have to manually convert the ID to a string
before sending it to the server.
Codegen config for servers
For GraphQL servers, we can use the typescript-resolvers plugin with the same scalars
config as the typescript plugin. This combination changes how scalar types are used:
- The scalar input is the type of coerced value that resolvers receive after
parseValue
orparseLiteral
functions are called. - The scalar output is the type that resolvers return before
serialize
is called.
First, install the GraphQL Code Generator CLI, and the typescript
and typescript-resolvers
plugins:
yarn add -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-resolvers
Here are the recommended config server types:
import type { CodegenConfig } from '@graphql-codegen/cli'
const config: CodegenConfig = {
schema: '**/schema.graphql',
generates: {
'src/schema/types.generated.ts': {
plugins: ['typescript', 'typescript-resolvers'],
config: {
scalars: {
// Recommended ID scalar type for servers:
ID: {
input: 'string',
output: 'string | number'
},
// Setting custom scalar type:
CustomScalar: {
input: 'string', // this means our server can take CustomScalar as string
output: 'number' // this means our server will return CustomScalar as number
}
}
}
}
}
}
export default config
Running yarn graphql-codegen
generates the following scalar type:
export type Scalars = {
ID: { input: string; output: string | number };
String: { input: string; output: string };
Boolean: { input: boolean; output: boolean };
Int: { input: number; output: number };
Float: { input: number; output: number };
CustomScalar: { input: string; output: number };
};
The recommended server config makes it more convenient to handle ID scalar types. This approach is particularly useful if you have objects with numeric IDs. Without using the recommended config, we would have to create custom mappers or manually convert the number to string to satisfy TypeScript typecheck.
Note: The recommended setup is suitable for the most common use cases. It does not cover some edge cases that may make writing resolvers awkward.
For example, Int
's output is technically string | number | boolean
but it is typed as number
by default because most resolvers would never return string
or boolean
.
2. GraphQL Scalars (graphql-scalars
)
GraphQL Scalars is a library that contains many commonly used custom GraphQL scalars such as DateTime
, BigInt
, etc.
Even if it is not too hard to create custom scalars, testing and publishing scalars to share between GraphQL servers can quickly distract from building the core business logic. Therefore, it is usually better to use a well-maintained and documented library like graphql-scalars
.
Here’s how we can use DateTime
scalar from graphql-scalars
:
1. Install graphql-scalars:
yarn add graphql-scalars
2. Declare DateTime
in the schema:
scalar DateTime
3. Import DateTime
resolver into resolvers map:
import { createServer } from 'http'
// 1. Import scalar resolver
import { DateTimeResolver } from 'graphql-scalars'
import { createSchema, createYoga } from 'graphql-yoga'
const yoga = createYoga({
schema: createSchema({
typeDefs /* GraphQL typeDefs */,
resolvers: {
DateTime: DateTimeResolver // 2. Put resolver to resolvers map
}
})
})
const server = createServer(yoga)
server.listen(4000, () => {
console.info('Server is running on http://localhost:4000/graphql')
})
4. Use GraphQL Code Generator to create types:
Most scalar resolvers from graphql-scalars
come with recommended server type that can be used in Codegen config. We can import a resolver and use said type very easily:
// 1. Import scalar resolver
import { DateTimeResolver } from 'graphql-scalars'
import type { CodegenConfig } from '@graphql-codegen/cli'
const config: CodegenConfig = {
schema: '**/schema.graphql',
generates: {
'src/schema/types.generated.ts': {
plugins: ['typescript', 'typescript-resolvers'],
config: {
scalars: {
ID: {
input: 'string',
output: 'string | number'
},
DateTime: DateTimeResolver.extensions.codegenScalarType // 2. Use scalar resolver's recommended server type
}
}
}
}
}
export default config
What about recommended client types for graphql-scalars
's custom scalars?
Currently, there’s no official package for this ( yet! ). However, there’s an issue on the GraphQL Scalars repo.
Here’s the general tips for picking the right client type for graphql-scalars
:
- Check the
serialize
function's return type, then set the scalaroutput
type in the Codegen config. - If your client converts the returned scalar value to another type, you can set the final
output
type in the Codegen config.
3. GraphQL Codegen Server Preset
GraphQL Code Generator and graphql-scalars
should be enough to manage our scalar implementation and type requirements. However, Server Preset (@eddeee888/gcg-typescript-resolver-files) can simplify the scalar setup further:
- uses the recommended ID scalar server config by default
- detects if a custom scalar exists in the installed
graphql-scalars
package, then automatically imports the scalar resolver to the resolvers map and sets up recommended type in Codegen config - detects if a custom scalar does not exist in the installed
graphql-scalars
package (or if the package is not installed), then automatically creates a custom scalar resolver and puts it into resolvers map
1. Install GraphQL Code Generator CLI, Server Preset and (optionally) GraphQL Scalars:
yarn add -D @graphql-codegen/cli @eddeee888/gcg-typescript-resolver-files
# (Optional) Install `graphql-scalars` if you intend to use custom scalars from this library
yarn add graphql-scalars
2. Use Server Preset in the Codegen config:
import { defineConfig } from '@eddeee888/gcg-typescript-resolver-files'
import type { CodegenConfig } from '@graphql-codegen/cli'
const config: CodegenConfig = {
schema: '**/schema.graphql',
generates: {
'src/schema': defineConfig()
}
}
export default config
Run yarn graphql-codegen
, and... that's it! Now, every time a custom scalar like DateTime
is added to the schema, the implementation and type automatically come from graphql-scalars
! 🚀
The Server Preset does more than just handling custom scalars. There are existing guides and blog posts with more details on its features and concepts:
- Read how to set up GraphQL Yoga / Apollo Server with Server Preset
- Read how to build Scalable APIs with GraphQL Server Codegen Preset
Summary
In this article, we explored the importance of GraphQL scalar types, how it works and how to create custom scalar resolvers to extend our schemas. We also explored tools to help working with native and custom scalars such as GraphQL Code Generator, GraphQL Scalars and Server Preset.
Originally published at https://the-guild.dev on June 27, 2023.