Typically when writing a web-app we want to perform validation on both client side to offer immediate feedback and on server-side to ensure data integrity and security. However, client-side browser apps are typically written in JavaScript. Server-side can be written in Java, Php, Ruby, Python and a host of other languages. When server-side is backed by something like node.js, it is really easy to re-use the same validation code on both client and server, but if server-side is based on Rails or Django (or whatever other framework you can name), what's the best way to make sure the validation code are kept on sync? It seems a bit redundant to have to re-implement the same code in multiple languages.
If you keep the following persepective in mind, it may seem okay to duplicate certain validations.
Let's break validations into two parts. A) Business Validations e.g. "Amount in Field X should be greater than $500 if if checkbox Y is checked" B) Basic data validations e.g. datatype checks, null checks etc. (We may debate that every validation is business validation but that is purely context specific).
Category A: It is part of your business logic and should be kept only on server side.
Category B: Validations of this type are potential candidates to be placed on the client side. But keep in mind that browser side validation can be bypassed. This does not imply that you should not have validations on browser side at all but such validations should be considered merely a bonus to save network roundtrip from server. Server must re-perform these validations.
In nutshell, validations should not be considered as unit of reusable code across tiers. Their objective varies and should allow redundancy.
Hope this helps.
From projects I've seen there's three general strategies:
Fully duplicate client-side and server-side validation. This would require two different codebases in the case of javascript frontend and java/c#/ruby backend. You'd have to manually keep the logic of both in sync.
Do minimal client-side validation. Check for only very basic stuff. Have server side do the full validation. Have server side pass a validation-errors object of some kind back to the client and have client logic to translate that into UI messages (error messages, red borders, etc). Asp.net MVC framework is roughly an example of this model.
Use ajax to make validation calls into your server side when the user changes or leaves each control. This can allow all your validation on the server side, and will reduce the feedback wait time for the user, but can increase client to server side traffic immensely.
In my experience, option 1 is generally less of a pain point than maintaining the extra code and complexity required for option 2 and 3.
If you keep the following persepective in mind, it may seem okay to duplicate certain validations.
Let's break validations into two parts. A) Business Validations e.g. "Amount in Field X should be greater than $500 if if checkbox Y is checked" B) Basic data validations e.g. datatype checks, null checks etc. (We may debate that every validation is business validation but that is purely context specific).
Category A: It is part of your business logic and should be kept only on server side.
Category B: Validations of this type are potential candidates to be placed on the client side. But keep in mind that browser side validation can be bypassed. This does not imply that you should not have validations on browser side at all but such validations should be considered merely a bonus to save network roundtrip from server. Server must re-perform these validations.
In nutshell, validations should not be considered as unit of reusable code across tiers. Their objective varies and should allow redundancy.
Hope this helps.
Our architecture allows for validator code-sharing: Our back-end is JAVA. Front-End is JavaScript/TypeScript (Angular if that matters). The JAVA back-end did all business stuff. For reporting MySQL to user and some utility user preferences we also had a minimal back-end written in NodeJS/Express.
To re-use the validation, we changed some endpoints from JAVA to NodeJS, then the NodeJS did the validation and internally passed the request body to the JAVA. (by system-call or non-public API). Then we removed the validation from JAVA code. NodeJS/Express mostly acts as a proxy, it also checks for requests validity.
That way we could share TypeScript codes/classes between Front-End and our minimal Back-End.
The overheads added:
- need to run a NodeJS back-end separately from main back-end (one extra process to manage/watch)
- need to expose a way to call your methods/APIs internally in the system (and make sure they are not public as they'll no longer have validators)
- some extra Nginx routing
i build this serve side validation endpoint:
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.AllArgsConstructor;
import org.hibernate.validator.HibernateValidator;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.stream.Collectors;
import static java.util.stream.Collectors.groupingBy;
@RestController
@AllArgsConstructor
public class ValidationEndpoint {
static {
Locale.setDefault(Locale.GERMAN);
}
private final ObjectMapper objectMapper;
private final Validator validator;
@PostMapping("/")
public Map < String, List < String >> validate(@RequestBody String entity, @RequestParam String className) throws ClassNotFoundException, JsonProcessingException {
Class c = Class.forName(className);
Object jsonObject = objectMapper.readValue(entity, c);
return validator.validate(jsonObject).stream().map(constraintViolation -> {
Violation violation = new Violation();
violation.setMessage(constraintViolation.getMessage());
violation.setFieldName(constraintViolation.getPropertyPath().toString());
return violation;
}).collect(groupingBy(Violation::getFieldName, Collectors.mapping(Violation::getMessage, Collectors.toList())));
}
@PostMapping("/valida")
public List < String > validateField(@RequestBody String entity, @RequestParam String className, @RequestParam String propertyName) throws ClassNotFoundException, JsonProcessingException {
Class c = Class.forName(className);
Object jsonObject = objectMapper.readValue(entity, c);
ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
.configure()
.failFast(false)
.defaultLocale(Locale.GERMAN)
.buildValidatorFactory();
Validator validator = validatorFactory.getValidator();
return validator.validateProperty(jsonObject, propertyName).stream().map(constraintViolation -> {
Violation violation = new Violation();
violation.setMessage(constraintViolation.getMessage());
violation.setFieldName(constraintViolation.getPropertyPath().toString());
return violation;
}).map(Violation::getMessage).toList();
}
}
© 2022 - 2024 — McMap. All rights reserved.