October 26, 2017
Angular Serverside Validation with Hibernate and Redux
What is this article about?
If you develop a web application with Angular, sooner or later you will run into the following problem: Where will the data generated by the client be validated? Client side validation is included in Angular, and gives the user a fast feedback about the correctness of his data. This is a standard in today’s web application.
As most Angular Apps are running against a server side REST Interface the data has to be validated again on the server to guarantee the correctness of the data. Otherwise, incorrect data could be inserted into the database.
The problem is, how to avoid duplicating the validation logic in the client and the server. I want to show you an approach we chose, and which works fine in an intranet application we recently deployed.
Our simple approach to serverside validation with Angular
Since the application runs on an intranet – and thus it is ensured that an additional network roundtrip does not have a negative influence on the client’s reactivity – we decided to carry out the validation logic completely to server. Our ORM framework (Hibernate) has a validation feature that we were able to utilize for this. The advantage is obvious: the validation logic only needs to be implemented once.
However, this creates the challenge of transferring the validation results from the server back to the client. As the user types we call the validation endpoint frequently and update the forms error state in order to inform both, the user and the Angular application.
Deciding how to call the Validation API
Our first approach was to listen to the respective Angular Input Controls and send the data to server.The code for this would more or less like this:
1 2 3 |
nameControl.valueChanges.forEach( (value: string) => this.nameChangeLog.push(value) ); |
But this approach had several drawbacks:
- Since our entity is spread over several forms, we would have to collect the different subsets of the entity and send them back to the server as a complete entity.
- It is hard to decide how often and when to send the data to the server. When the users exits a control (onBlur) or after each keystroke?
The Redux way
As we already have a Redux store backing all our forms we decided to trigger the validation from a Redux Effect. The code is shown below. Basically it sends the full data obejct every 300ms (but only when changes are made) to the server.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
@Effect() validate: Observable<Action> = this.actions$ //Register the types to run the effect on .ofType(UPDATE_FORM) //debounce for 300ms, so we do not send on every keystroke .debounceTime(300) .map(toPayload) // retrieve our full data object from the store .switchMap((something) => { return this.store.select(getSaveableObject).first(); }) .switchMap(val => { const data = val.data; //call the serverside validation service return this.service.validate(data).map(errors => { //take all the errors and go on return new UpdateErrorsAction(errors); }); }).catch(() => Observable.of(new UpdateErrorsAction(null))); |
On the Server
On the server we are validating our data object by using JavaEE Bean Validation and some custom code. JavaEE Bean Validation has some nice Annotations to check fields. Further information can be found here. The code displayed here is in Kotlin, but this could be easily done with Java too.
1 2 |
import javax.validation.Validation import javax.validation.Validator |
1 2 3 4 |
// run the validation val constraintViolations = Validation.buildDefaultValidatorFactory().validator!!.validate(entity) // map the validations to key value pairs, in order to send them to the client val controlErrors = constraintViolations.associateBy({ it.propertyPath.toString() }, { it.message }) |
Note: We also have some custom validation, which runs logic checks against the given data object and writes the errors into a Simple Java Map.
Displaying Errors in the Client
In the client, we perform the following operations each time the server answers a validation request.
- Set the error state via Angular
- Disable the save button
Setting errors
The first snippet demonstrates how to set an error to an Angular control. It is very important (at least for Angular Material) to mark the control as touched. Otherwise the error will not be displayed.
1 2 3 |
const control = form.controls[key]; control.setErrors({"serverValidationErr": errorText}); control.markAsTouched(); |
To display the error text, the following snippet is also needed for every input control.
1 2 3 |
<md-error *ngIf="form.controls.fieldName.hasError('serverValidationErr')"> {{form.controls.auftragNummer.errors.serverValidationErr}} </md-error> |
To summarize what we achieved: A validation that only had to be implemented once and still applies to the server and client. This allows us an agile further development.
Conclusion
The approach really worked very well for our application. We only have to write validation logic once on the server. The data is validated every 300ms and also on saving. Of course there is a lot more to do, then displayed here. Matching the error Map which is sent from the server to the client can be quit difficult, especially when there is data in collections involeved. This snippet is some code which matches errors to forms which are in a collection.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
addErrorToFormFromCollection(form: FormGroup, key: string, selectedIndex: number, errorText: string) { let errorIndexTest = key.match(/\[(.*?)\]/); if (errorIndexTest) { let errorIndex = errorIndexTest[1]; // this.logger.info(errorIndex); if (selectedIndex.toString() == errorIndex) { // this.logger.info("matches"); const posOfDot = key.indexOf('.'); const fieldName = key.slice(posOfDot + 1, key.length); // this.logger.info(fieldName); this.addErrorToForm(form, fieldName, errorText); } } }; |
Intranet-Only
This approach suits only intranet applications, where there is a quite fast bandwidth. Depending on the size of the dataobject there is quite a lot of data sent over the network.
We never experienced any problems in our environment.
Greetings! Very helpful advice within this article!
It’s the little changes that make the most important
changes. Many thanks for sharing!