Enums and scalars
Give values their real types: a registered enum the schema lists and checks, dates that travel as ISO strings and arrive as Date objects, and a custom Microchip scalar that validates what clients send and formats what they get.
What you'll learn
- Register a TypeScript enum with registerEnumType(), with descriptions, and use it for fields and arguments
- Use Date fields and arguments, which GraphQL carries as the DateTime scalar in ISO 8601
- Write a custom scalar with GraphQLScalarType: serialize for answers, parseValue for variables, parseLiteral for inline values
- Tell where each kind of value is checked: enums and scalars by GraphQL before any resolver runs
A cat's breed is not any string: the shelter knows three breeds, and "Persain" is a typo, not a fourth. Its arrival is a moment in time, not text. A microchip number has a format that staff type in three different ways. So far all of these would be String, and every resolver would check and convert them by hand. GraphQL's type system can carry the meaning instead: an enum lists the allowed values, and a scalar defines how a value is written on the wire and what it becomes in your code. The schema publishes both, and GraphQL checks both before any resolver runs.
Enums
A TypeScript enum becomes a GraphQL enum once it is registered:
export enum Shift { MORNING = 'MORNING', EVENING = 'EVENING' }
registerEnumType(Shift, {
name: 'Shift',
description: 'When a volunteer works',
valuesMap: { EVENING: { description: 'Until closing time' } },
});
A field or argument then names it: @Field(() => Shift). Leave the type function out and Nest sees only the runtime type of a string enum's values, a String, so the schema quietly loses the enum. Clients write enum values without quotes (shifts(when: EVENING)) and receive them as strings. A value that is not in the enum is refused with a 400, Value "NIGHT" does not exist in "Shift" enum., and your resolver receives the TypeScript enum's value.
Dates
GraphQL has no date type, so Nest provides scalars for one. A property typed Date becomes the DateTime scalar (GraphQLISODateTime): it travels as an ISO 8601 string, 2026-06-01T09:00:00.000Z, and resolvers see a real Date. An argument or an args-class field needs the type function, () => GraphQLISODateTime, because an optional Date property carries no type at runtime. buildSchemaOptions: { dateScalarMode: 'timestamp' } switches to milliseconds since the epoch, if clients prefer that.
Custom scalars
A scalar is a GraphQLScalarType from the graphql package with three functions, one for each way a value crosses the boundary:
export const PostcodeScalar = new GraphQLScalarType({
name: 'Postcode',
serialize: (value) => format(value as string),
parseValue: (value) => validate(value),
parseLiteral: (ast) => validate(ast.kind === Kind.STRING ? ast.value : undefined),
});
serializeturns your value into what goes out in an answer.parseValueturns a value that came as a variable into your value.parseLiteraldoes the same for a value written inline in the query. It receives the value as a syntax node, so it checks the node's kind first.
To refuse a value, throw. A GraphQLError is reported as it is: the client gets its message and a 400 before any resolver runs. Throw a plain Error and GraphQL wraps it in its own sentence (Expected value of type "Postcode!", found "X"; ...). A field or argument names the scalar through the type function, @Field(() => PostcodeScalar). The docs also register it under resolvers in forRoot(); code first finds the scalar through the fields that name it, so this lesson does not need that.
The point of a scalar is that the rest of the code never sees a badly formed value. Resolvers receive digits, not "whatever the client typed".
Your task
Cats now have a breed enum, an arrival date and an optional microchip. The service filters by breed and arrival, and records microchips as 15 bare digits.
- Register
Breedunder the nameBreed, described asWhat the cat looks like, withMAINE_COONdescribed asThe big ones. - Write the
Microchipscalar. A client sends 15 digits, grouped by spaces or dashes or not at all. Anything else is refused with aGraphQLErrorwhose message isA microchip is 15 digits. Inside, the value is the 15 digits alone. In an answer it is written in groups of three:985-112-345-678-901. - Type the
Catfields:breedaBreed,arrivedAta date and time,microchipaMicrochipthat may be null. - Type
CatsArgs: an optionalbreedand an optionalarrivedAfterdate. - Add a
registerMicrochip(catId: Int!, chip: Microchip!)mutation returning the cat.
When it fails
Cannot determine a GraphQL input type for the "breed"at startup: the enum was never registered.breedworks but the schema saysString: its field has no() => Breed.- Every microchip is refused, even valid ones:
parseLiteralreadast.valuewithout checking that the node is a string, orparseValueexpected grouping the client did not send. - Answers show
985112345678902:serializedoes not format the digits.
Remember
registerEnumType()makes a TypeScript enum a GraphQL enum; fields and arguments name it with a type function.Datetravels as the ISODateTimescalar; arguments need() => GraphQLISODateTime.- A scalar is
serializeout,parseValuefor variables,parseLiteralfor inline values. - Throw a
GraphQLErrorto refuse a value; it never reaches a resolver.
Stuck? Show a hint
breed.enum.ts: registerEnumType(Breed, { name: 'Breed', description, valuesMap: { MAINE_COON: { description } } }). cat.model.ts: @Field(() => Breed), a bare @Field() on a Date, @Field(() => MicrochipScalar, { nullable: true }). cats.args.ts: @Field(() => Breed, { nullable: true }) and @Field(() => GraphQLISODateTime, { nullable: true }). microchip.scalar.ts: serialize groups the digits, parseValue and parseLiteral (a Kind.STRING literal) strip spaces and dashes and throw a GraphQLError unless 15 digits remain. The mutation: @Args('chip', { type: () => MicrochipScalar }).