Angular Tutorial - Sharing Validations Between Angular and NestJs

Validating models across NestJs and Angular could lead to many differences. This explains how we can leverage the power of Typescript to create a cross platform validation library.

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.

View on Gumroad
@ngserveio/you-good
This library was generated with [Nx](https://nx.dev).

GOALS

  1. Define the Validation Types
  2. Create Validation Service
  3. Add the Validation Service to a Reactive Angular Form
  4. 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.