Out of the box, GraphQL supports data types, such as String and Int. However, this doesn't cater for a more explicit set of validation rules, such as enforcing length checks.
Let's take a common example, a user sign up form.
The SDL (Schema Definition Language) in this example will be as follows:
https://gist.github.com/3fb8dc5e01b7f75f1aeb774784803cfd
The above enforces strings for all fields, and ensures they are not null. However, the database it's interacting with has some constraints to be wary of.
In this example, the backend database is using a VARCHAR(255)
field for all Strings. Providing a field greater than 255 characters will cause an error (duh!). Normally, validation logic to prevent this would reside in a resolver function. However, by doing this, validation rules aren't included in the SDL. This is meant to be self-documenting (because few like writing documentation) but consumers of this service will be unaware if all they have is the SDL.
It's possible to add this logic directly to the SDL using a module such as graphql-constraint-directive.
This module exposes a @constraint
directive to decorate an SDL with validation rules.
Thanks to graphql-tools and apollo-server, it's easy to integrate.
https://gist.github.com/0aac8c13b33a90e904b702157d6973c0
In just a few lines, it's now possible to decorate an SDL with @constraint
to attach validation rules. A full example can be found on the graphql-constraint-directive readme.
The SDL now looks a little different.
https://gist.github.com/924cd541b89271ef6792beae09d72807
This isn't a silver bullet solution. There will be more complex validation rules, such as those which rely on state (e.g. does this entity exist?) that will need to remain elsewhere.
Let's break this down...
https://gist.github.com/44852d830704256c818bb84a22a852d7
@constraint
has a number of available arguments (check the readme for a full list) depending on the field type. In this case, as it's a string, the option of defining a format
can be used. This particular constraint is validating the value is an email address and it's 255 characters at most.
You could split this into multiple
@constraint
directives. Note that directives are resolved right to left. In the following example, maxLength is resolved before format. E.g.email: String! @constraint(format: "email") @constraint(maxLength: 255)
https://gist.github.com/74bb9c8f8141dea339b03969b4057ed5
On a different field, it is confirming the last name is a maximum of 255 characters and also matches a regular expression. In this instance, alphanumeric only (sorry double barrelled surnames!).
You could remove
maxLength
and perform this check within the expression. However, regex and.length
return unexpected results when emojis are used... 💩
When a consumer sends a request which breaks these rules, the application will respond with an error message.
https://gist.github.com/444d74a2244a940d678816a739b0a424
This isn't user friendly. If this does not match your consumers' expected syntax, or you wish to provide your own message, you can handle this within formatError
Reviewing the error message, there's something out of place. Specifically Expected type ConstraintString
, even though the field is defined as String!
.
The constraint directive wraps each field with its own scalar types. graphql-tools
takes care of this implementation detail for the consumer, so the fields in question are still String!
when submitting the mutation. ConstraintString
& ConstraintNumber
will never appear within your SDL, and the client/consumer needn't be concerned by it.
When the value of each decorated field is parsed, the validation rules are executed. e.g. minLength
https://gist.github.com/bc7018e4fddab17836e4de4e0a931380
If all validation rules pass, ConstraintString
/ConstraintNumber
executes the original scalar's parseValue
and returns it. This allows other scalars to execute their own parseValue functions, enabling support for other custom scalars!
Using this approach, it's trivial to create your own custom directives to validate and manipulate field values.
To publish a custom directive as a module, ensure the custom directive class is exported as the main
module entry.
I hope this inspires you to develop and publish your own custom directives!