Argument and Input validation
Scalars
The standard way to ensure that inputs and arguments are correct, such as an email
field that really contains a proper e-mail address, is to use custom scalars e.g. GraphQLEmail
from graphql-custom-types
. However, creating scalars for all single cases of data types (credit card number, base64, IP, URL) might be cumbersome.
That's why TypeGraphQL has built-in support for argument and input validation.
By default, we can use the class-validator
library and easily declare the requirements for incoming data (e.g. a number is in the range 0-255 or a password that is longer than 8 characters) thanks to the awesomeness of decorators.
We can also use other libraries or our own custom solution, as described in custom validators section.
class-validator
How to use
First, we need to install the class-validator
package:
npm install class-validator
Then we decorate the input/arguments class with the appropriate decorators from class-validator
.
So we take this:
@InputType()
export class RecipeInput {
@Field()
title: string;
@Field({ nullable: true })
description?: string;
}
...and turn it into this:
import { MaxLength, Length } from "class-validator";
@InputType()
export class RecipeInput {
@Field()
@MaxLength(30)
title: string;
@Field({ nullable: true })
@Length(30, 255)
description?: string;
}
Then we need to enable the auto-validate feature (as it's disabled by default) by simply setting validate: true
in buildSchema
options, e.g.:
const schema = await buildSchema({
resolvers: [RecipeResolver],
validate: true, // Enable 'class-validator' integration
});
And that's it! 😉
TypeGraphQL will automatically validate our inputs and arguments based on the definitions:
@Resolver(of => Recipe)
export class RecipeResolver {
@Mutation(returns => Recipe)
async addRecipe(@Arg("input") recipeInput: RecipeInput): Promise<Recipe> {
// 100% sure that the input is correct
console.assert(recipeInput.title.length <= 30);
console.assert(recipeInput.description.length >= 30);
console.assert(recipeInput.description.length <= 255);
}
}
Of course, there are many more decorators we have access to, not just the simple @Length
decorator used in the example above, so take a look at the class-validator
documentation.
This feature is enabled by default. However, we can disable it if we must:
const schema = await buildSchema({
resolvers: [RecipeResolver],
validate: false, // Disable automatic validation or pass the default config object
});
And we can still enable it per resolver's argument if we need to:
class RecipeResolver {
@Mutation(returns => Recipe)
async addRecipe(@Arg("input", { validate: true }) recipeInput: RecipeInput) {
// ...
}
}
The ValidatorOptions
object used for setting features like validation groups can also be passed:
class RecipeResolver {
@Mutation(returns => Recipe)
async addRecipe(
@Arg("input", { validate: { groups: ["admin"] } })
recipeInput: RecipeInput,
) {
// ...
}
}
Note that by default, the skipMissingProperties
setting of the class-validator
is set to true
because GraphQL will independently check whether the params/fields exist or not.
Same goes to forbidUnknownValues
setting which is set to false
because the GraphQL runtime checks for additional data, not described in schema.
GraphQL will also check whether the fields have correct types (String, Int, Float, Boolean, etc.) so we don't have to use the @IsOptional
, @Allow
, @IsString
or the @IsInt
decorators at all!
However, when using nested input or arrays, we always have to use @ValidateNested()
decorator or { each: true }
option to make nested validation work properly.
Response to the Client
When a client sends incorrect data to the server:
mutation ValidationMutation {
addRecipe(
input: {
# Too long!
title: "Lorem ipsum dolor sit amet, Lorem ipsum dolor sit amet"
}
) {
title
creationDate
}
}
the ArgumentValidationError
will be thrown.
By default, the apollo-server
package from the bootstrap guide will format the error to match the GraphQLFormattedError
interface. So when the ArgumentValidationError
occurs, the client will receive this JSON with a nice validationErrors
property inside of extensions.exception
:
{
"errors": [
{
"message": "Argument Validation Error",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": ["addRecipe"],
"extensions": {
"code": "INTERNAL_SERVER_ERROR",
"exception": {
"validationErrors": [
{
"target": {
"title": "Lorem ipsum dolor sit amet, Lorem ipsum dolor sit amet"
},
"value": "Lorem ipsum dolor sit amet, Lorem ipsum dolor sit amet",
"property": "title",
"children": [],
"constraints": {
"maxLength": "title must be shorter than or equal to 30 characters"
}
}
],
"stacktrace": [
"Error: Argument Validation Error",
" at Object.<anonymous> (/type-graphql/src/resolvers/validate-arg.ts:29:11)",
" at Generator.throw (<anonymous>)",
" at rejected (/type-graphql/node_modules/tslib/tslib.js:105:69)",
" at processTicksAndRejections (internal/process/next_tick.js:81:5)"
]
}
}
}
],
"data": null
}
Of course we can also create our own custom implementation of the formatError
function provided in the ApolloServer
config options which will transform the GraphQLError
with a ValidationError
array in the desired output format (e.g. extensions.code = "ARGUMENT_VALIDATION_ERROR"
).
Automatic Validation Example
To see how this works, check out the simple real life example.
Caveats
Even if we don't use the validation feature (and we have provided { validate: false }
option to buildSchema
), we still need to have class-validator
installed as a dev dependency in order to compile our app without errors using tsc
.
An alternative solution that allows to completely get rid off big class-validator
from our project's node_modules
folder is to suppress the error TS2307: Cannot find module 'class-validator'
TS error by providing "skipLibCheck": true
setting in tsconfig.json
.
Custom validator
We can also use other libraries than class-validator
together with TypeGraphQL.
To integrate it, all we need to do is to provide a custom function. It receives three parameters:
argValue
which is the injected value of@Arg()
or@Args()
argType
which is a runtime type information (e.g.String
orRecipeInput
)resolverData
which holds the resolver execution context, described as generic typeResolverData<TContext>
This function can be an async function and should return nothing (void
) when validation passes, or throw an error when validation fails.
So be aware of this while trying to wrap another library in validateFn
function for TypeGraphQL.
Then we provide this function as a validateFn
option in buildSchema
.
Example using decorators library for Joi validators (joiful
):
const schema = await buildSchema({
// ...
validateFn: argValue => {
// Call joiful validate
const { error } = joiful.validate(argValue);
if (error) {
// Throw error on failed validation
throw error;
}
},
});
The validateFn
option is also supported as a @Arg()
or @Args()
decorator option, e.g.:
@Resolver()
class SampleResolver {
@Query()
sampleQuery(
@Arg("sampleArg", {
validateFn: (argValue, argType) => {
// Do something here with arg value and type...
},
})
sampleArg: string,
): string {
// ...
}
}
Be aware that when using custom validator, the error won't be wrapped with
ArgumentValidationError
like for the built-inclass-validator
validation.
Custom Validation Example
To see how this works, check out the simple custom validation integration example.