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.
- 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
||The validation type and the values associated with that validation e.g.
||Provides a key on property
||A function that returns the validations per the validator on a property|
||A delegate method that returns the value to be validated. e.g.
||A list of validators of a particular property on
The types above will allow mapping of the validations to a property on the model
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.
validate<T> method accepts an object of property
PropertyValidators<T>. This creates a map of validators to a property on
Each validation returns a
ValidatorFn<T>. The consumer of the validator defines the delegate value from the model
(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
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
required and must be an
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.