Angular Tutorial - Sharing Validations Between Angular and NestJs
The beauty of developing with NestJs and Angular in a Nrwl NX monorepository reveals when code can be shared between both frameworks for full stack development. Many projects earlier in my career often lead to duplicate changes if backends were written in Java or C#.
For my use case, I required consistent validations between the frontend and backend, keeping it DRY.
In this tutorial, I'll cover how to create a validation framework that helps support both frontend and backend scenarios.
Free Your Developers
Nx Monorepo Starter alleviates developers from re-inventing deployments on popular Google Cloud Services, standardizes on libraries, and saves time for scaffolding projects.
Thanks for your interest in Nx GCP Starter. We'll be in touch shortly
GOALS
- Define the Validation Types
- Create Validation Service
- Add the Validation Service to a Reactive Angular Form
- Create a NestInterceptor validating the model
1. Define the Validation Types
Name | Returns | Description |
---|---|---|
ValidationErrors |
{ [validationName: string]: unknown } |
The validation type and the values associated with that validation e.g. { required: true } |
PropertyValidationErrors<T> |
Record<keyof T, unknown> |
Provides a key on property T of unknown type which will be the return validation. e.g. {firstName: { required: true, maxLength: { max: 50, actual: 51 } } |
ValidatorFn<T> |
ValidationErrors |
A function that returns the validations per the validator on a property |
ValidationField<T> |
(item: T) => unknown |
A delegate method that returns the value to be validated. e.g. (item: { name: string }) => item.name |
PropertyValidators<T> |
Record<keyof T, ValidatiorFn<T>>[] |
A list of validators of a particular property on T e.g. { firstName: [ required, max(10) ], email: { email, maxLength(50) } } |
The types above will allow mapping of the validations to a property on the model T
.
Love Web Development?
Angular, Google Cloud, C#, Node, NestJs?
2. Create the Validation Service
With the types defined, a validate
function requires the item and a list of validations. After having dealt with Angular validations on separate controls, centralizing the validations to one file felt less confusing.
The validate<T>
method accepts an object of property PropertyValidators<T>
. This creates a map of validators to a property on T
.
Each validation returns a ValidatorFn<T>
. The consumer of the validator defines the delegate value from the model T
, via (ValidationField<T>) => ValidatorFn<T>
. Delegating as ValidationField<T>
allows developers to extend beyond a property value.
For instance, if validation required the sum of item.fieldA + item.fieldB
must be less than 200, developers would be able to extend beyond a property value using the ValidationField<T>
.
3. Add Validation Service to a Reactive Form
While the Angular team develops generic forms, I decided to roll my own.
Having a generic form which the keys of the model / interface provided present interesting opportunities with the validator created above.
Inheriting from Angular's FormGroup
, the validation hook is invoked as the FormGroup
value changes. The keys from the validator align with the controls. This allows the ModeledFormGroup
to validate the control's value against our model validations supplied in the constructor. This ensures the control exists and can invoke the setErrors
method with or without the validations.
4. Create Validation NestInterceptor
In an effort to keep this as DRY as possible, the model validations can exist in a platform agnostic library.
At first, I thought utilizing middleware would be an option mapping routes and HTTP verbs associated with a particular validator. That felt like too much overhead, so I decided a NestInterceptor
decorated on routes with a validator would be easier to manage.
In order to determine the correct validator for the route, create a NestInterceptorFactory
which allows defining the validator to run for the model in the body of the request.
Initially, I thought the factory would exist as a service in the Nest module and the developer would be able to define a key mapping to a validator. At a developer experience level, that seemed over complicated. So I decided it'd be easier to inject the validator into the NestInterceptorFactory
, defining the validator for the route.
Above, the interceptor reads the body of the request and casts to the generic T
. If the model is empty, a bad request exception returns to the client. Otherwise, the validator checks the model, returning a keymap of the properties on the model. Within each of the model's properties returned in the response, the specific validators which failed return.
Below, one of the tests for an email addressed failed. The email
property of the model is required
and must be an email
.
Conclusion
Creating an agnostic validation library for Nest and Angular provides the ability to make a validator singly responsible for both client and back end applications.
The model will drive the controls necessary in an Angular form. With changes in the form, extending the FormGroup
to be generic allows control over the key / control mapping. This allows the validator to bind different validation errors to each control.
Adding a NestJs interceptor allows developers to validate the model prior to controller execution. With the validator defined in the NestJs Interceptor Factory, the interceptor retrieves the model from the body passing it to the validator. If it fails, the validator returns a BadRequestException
with a response of the key value / validation error mappings to the client.