Custom scalars
How to implement custom scalars for your GraphQL API
To view this content, buy the book! 馃槂馃檹
Or if you鈥檝e already purchased.
Custom scalars
If you鈥檙e jumping in here,
git checkout 6_0.2.0(tag 6_0.2.0, or compare 6...7)
In the last section we mentioned that the ID scalar is serialized like a string, but what does that process look like, and how do we make our own scalars? The only built-in scalars are Int, Float, String, Boolean, and ID. Another scalar type that most apps use is a date. For example, it would be nice to have a Review.createdAt. We could make it an Int, but then is it seconds or milliseconds since the [Unix epoch](https://en.wikipedia.org/wiki/Epoch_(computing\))? Or it could be a String, but there are a lot of string date formats out there. And both ways are missing validation (testing whether the string is a valid date string) and the improved understanding that comes from being able to know, looking at the schema, which fields are meant to be dates. So let鈥檚 make our own Date scalar. We can add it to our schema:
scalar Date
type Query {
hello: String!
isoString(date: Date!): String!
}
#import 'Review.graphql'
#import 'User.graphql'type Review {
id: ID!
text: String!
stars: Int
fullReview: String!
createdAt: Date!
updatedAt: Date!
}
...First we declare the new scalar type (scalar Date), and then we use it for a new isoString query as well as createdAt and updatedAt fields on Review. We make them non-nullable because all Review objects will have them.
We can use the word
Datefor our type because we don鈥檛 have other types of dates or times in our app. If we also had aDatethat had no time component, like a birthday, or aTimethat had no date component, like 14:00 (2 p.m.), we could call our new scalarDateTime.
isoString takes a Date as an argument and returns the date formatted as a string in the ISO format:
const resolvers = {
Query: {
hello: () => '馃實馃審馃寧',
isoString: (_, { date }) => date.toISOString()
}
}Next we add to our resolvers a GraphQLScalarType, which tells Apollo Server how to handle a custom scalar. It will look like this:
import { GraphQLScalarType } from 'graphql'
export default {
Date: new GraphQLScalarType({
name:
description:
parseValue(value) {}
parseLiteral(ast) {}
serialize(date) {}
})
}GraphQLScalarType takes five parameters:
namematches the scalar name we added to the schema, so'Date'descriptionis shown in the schema section of GraphiQL and Playground. It says what the scalar represents and how it appears in the JSON response from a server. The built-in description forID, for instance, is:
The
IDscalar type represents a unique identifier, often used to refetch an object or as a key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as"4") or integer (such as4) input value will be accepted as an ID.
parseValue(value)is a function called when the server receives a query variable for a Date argument. The variable鈥檚 value is passed toparseValue(), and the function should return the value in our desired format鈥攊n this case, a JavaScript Date object. For example, if the client sends this query:
query ISOString($date: Date!) {
isoString(date: $date)
}with this as the variables JSON:
{
"date": 1442188800000
}then parseValue is passed the integer 1442188800000 and should return a JS Date object, which Apollo Server will provide to our resolver, which calls .toISOString() on the JS Date object:
isoString: (_, { date }) => date.toISOString()
parseLiteral(ast)is called when the server receives a query with a literal argument鈥攎eaning the argument is written in the query document itself instead of being provided separately in JSON (as variables are).aststands for abstract syntax tree, which is an object that Apollo Server uses to parse the query document.ast.valuehas the literal value, and is always a string. Similar toparseValue(),parseLiteral()should return the server鈥檚 internal representation of the scalar type. If the client sends this query document:
{
isoString(date: 1442188800000)
}Then parseLiteral(ast) will be called, and ast.value will be "1442188800000".
serialize(date)is called when the server is formatting a JSON response for the client. A resolver returns a JS Date object, then Apollo Server callsserialize()with that object, andserialize()returns the date in a format that can be put into the JSON response鈥攚hich in our implementation of theDatescalar is an integer. For example, if theReview.createdAtresolver returns a JS Date, we would see an integer in the response:

If you're following along, this query won't work until we fill in
Date.jsand add it tosrc/resolvers/index.js.
Here鈥檚 a basic implementation of the above:
import { GraphQLScalarType } from 'graphql'
export default {
Date: new GraphQLScalarType({
name: 'Date',
description: `The \`Date\` scalar type represents a single moment in time.
It is serialized as an integer, equal to the number of milliseconds since
the Unix epoch.`,
parseValue: value => new Date(value),
parseLiteral: ast => new Date(parseInt(ast.value)),
serialize: date => date.getTime()
})
}parseValue() takes the integer and creates a Date. parseLiteral() gets the ast.value string, converts it into an integer, and creates a Date. serialize() takes the date and returns the milliseconds since epoch.
One important aspect of defining a custom scalar that we鈥檙e missing is validation. If we check the values we鈥檙e getting and throw errors with descriptive messages, it will help people using our API. Let鈥檚 do that:
import { GraphQLScalarType } from 'graphql'
import { Kind } from 'graphql/language'
const isValid = date => !isNaN(date.getTime())
export default {
Date: new GraphQLScalarType({
name: 'Date',
description:
`The \`Date\` scalar type represents a single moment in time. It is serialized as an integer, equal to the number of milliseconds since the Unix epoch.',
parseValue(value) {
if (!Number.isInteger(value)) {
throw new Error('Date values must be integers')
}
const date = new Date(value)
if (!isValid(date)) {
throw new Error('Invalid Date value')
}
return date
},
parseLiteral(ast) {
if (ast.kind !== Kind.INT) {
throw new Error('Date literals must be integers')
}
const date = new Date(parseInt(ast.value))
if (!isValid(date)) {
throw new Error('Invalid Date literal')
}
return date
},
serialize(date) {
if (!(date instanceof Date)) {
throw new Error(
'Resolvers for Date scalars must return JavaScript Date objects'
)
}
if (!isValid(date)) {
throw new Error('Invalid Date scalar')
}
return date.getTime()
}
})
}In parseValue() and parseLiteral(), we check whether the client sent an integer, then we create a JS Date and check whether it鈥檚 valid. In serialize() we check that the value returned from a resolver is a JS Date object, then we check if it鈥檚 a valid date, and finally we return the milliseconds since epoch.
We add this file to our resolvers in resolvers/index.js by importing and adding to our resolversByType array:
...
import Review from './Review'
import User from './User'
import Date from './Date'
export default [resolvers, Review, User, Date]We saw our isoString query working above, but now if we make a mistake, we get a helpful error message:


The last part of our schema change for which we have to implement resolvers is Review鈥檚 createdAt and updatedAt. In MongoDB, the creation time is included in the default ID format, ObjectId. The first 4 bytes are the seconds since Unix epoch, so we can get the creation time from that. (And since it鈥檚 the first 4 bytes, we can also sort by an ObjectId to order by most/least recently created.) The mongodb node library provides a method ObjectId.getTimestamp() that extracts the date for us:
export default {
Query: ...
Review: {
...
createdAt: review => review._id.getTimestamp()
},
Mutation: ...
}updatedAt is a field that we鈥檒l have to store in the database when reviews are created and update when reviews are modified. We don鈥檛 have a way of modifying reviews yet, so we鈥檒l just add a line to our creation method:
import { MongoDataSource } from 'apollo-datasource-mongodb'
export default class Reviews extends MongoDataSource {
...
create(review) {
review.updatedAt = new Date()
this.collection.insertOne(review)
return review
}
}Now we can include updatedAt in our reviews query, but we get the error Cannot return null for non-nullable field Review.updatedAt:

Apollo Server is telling us that it can鈥檛 return null for Review.updatedAt to the client because the schema says it鈥檚 a non-nullable field. Why is it trying to return null for Review.updatedAt? It鈥檚 not鈥攐ur resolver is. Our reviews resolver is returning reviews fetched from the database, but none of them have an updatedAt property because they were inserted before we updated our Reviews.create() data source method. We could fix our reviews in the database by adding an updatedAt field, but let鈥檚 just delete them and re-create. If you鈥檇 like a GUI (Graphical User Interface, i.e., a program that runs in its own window instead of in the command line) for interacting with MongoDB, we recommend MongoDB Compass. Here鈥檚 how to delete all of our reviews using the mongo command-line shell:
$ mongo
MongoDB shell version v4.0.3
connecting to: mongodb://127.0.0.1:27017
...
> use guide
switched to db guide
> db.reviews.find({})
{ "_id" : ObjectId("5cdfb1946df8548efb438535"), "text" : "Passing", "stars" : 3 }
{ "_id" : ObjectId("5cdfb1e4a1cf288f4d86dced"), "text" : "Passing", "stars" : 3 }
{ "_id" : ObjectId("5cdfb28e48435b90119bd2c6"), "text" : "Passing", "stars" : 3 }
> db.reviews.remove({})
WriteResult({ "nRemoved" : 3 })
> db.reviews.find({})
> exit
byeOur second call to db.reviews.find({}) doesn鈥檛 show results because the collection is now empty. And when we do our reviews query, we get back an empty array. Now if we use Playground to send a createReview mutation, then we can do a reviews query with the createdAt and updatedAt fields:

The last three digits of createdAt will always be 000 because the API returns milliseconds since Epoch, and all that鈥檚 stored in the ObjectId is seconds since Epoch.
An alternative to clearing the database collection would have been to add a resolver for Review.updatedAt that returns Review.createdAt when there鈥檚 no updatedAt property on the review object. In order to call another resolver, we鈥檇 need to name the resolver鈥檚 object and move export default to the end:
const resolvers = {
Query: {
reviews: ...
},
Review: {
id: ...
fullReview: ...
createdAt: review => review._id.getTimestamp(),
updatedAt: review => review.updatedAt || resolvers.Review.createdAt(review)
},
Mutation: {
createReview: ...
}
}
export default resolversThen we could reference another resolver function (resolvers.Review.createdAt(review)).
In this section we created a new Date scalar type, added Query.isoString, which has a Date argument, and Review.createdAt and Review.updatedAt, which resolve to Dates. We鈥檒l continue to use the Date type in the rest of our app, for instance for User.createdAt/updatedAt in the next section.