In the case where validation rules are not static, validation cannot be specified at the model level. Hence, validation can be added in the controller layer.
Take an example of a promo code in an order, it is usually a defined value that is only valid for a certain period of time. And in the CoffeeShop example, the area code of a phone number usually depends on the geolocation.
Add validation function in the Controller method
The simplest way is to apply the validation function in the controller method. For example:
// create a validatePhoneNum function and call it here
if (!this.validatePhoneNum(coffeeShop.phoneNum, coffeeShop.city))
throw new Error('Area code in phone number and city do not match.');
return this.coffeeShopRepository.create(coffeeShop);
Add interceptor for validation
Another way is to use interceptors.
Interceptors are reusable functions to provide aspect-oriented logic around method invocations.
Interceptors have access to the invocation context, including parameter values for the method call. It can perform more specific validation, for example, calling a service to check if an address is valid. There are three types of interceptors for different scopes: global, class-level and method-level interceptors.
Interceptors can be created using the
interceptor generator
lb4 interceptor
command. In the CoffeeShop example, the phoneNum
in the
CoffeeShop
request body will be validated for the POST
and PUT
calls
whether the area code in the phone number matches the specified city. Since this
is only applicable to the CoffeeShop endpoints, a non-global interceptor is
created, i.e. specify No
in the Is it a global interceptor
prompt.
$ lb4 interceptor
? Interceptor name: validatePhoneNum
? Is it a global interceptor? No
create src/interceptors/validate-phone-num.interceptor.ts
update src/interceptors/index.ts
Interceptor ValidatePhoneNum was created in src/interceptors/
In the newly created interceptor ValidatePhoneNumInterceptor
, the function
intercept
is the place where the pre-invocation and post-invocation logic can
be added.
async intercept(
invocationCtx: InvocationContext,
next: () => ValueOrPromise<InvocationResult>,
) {
// Add pre-invocation logic here
// ------ VALIDATE PHONE NUMBER ----------
let coffeeShop: CoffeeShop | undefined;
if (invocationCtx.methodName === 'create')
coffeeShop = invocationCtx.args[0];
else if (invocationCtx.methodName === 'updateById')
coffeeShop = invocationCtx.args[1];
if (
coffeeShop &&
!this.isAreaCodeValid(coffeeShop.phoneNum, coffeeShop.city)
) {
const err: ValidationError = new ValidationError(
'Area code and city do not match',
);
err.statusCode = 422;
throw err;
}
// ----------------------------------------
const result = await next();
// Add post-invocation logic here
return result;
} catch (err) {
// Add error handling logic here
throw err;
}
}
isAreaCodeValid(phoneNum: string, city: string): Boolean {
// add some dummy logic
const areaCode: string = phoneNum.slice(0, 3);
if (
!(
city.toLowerCase() === 'toronto' &&
(areaCode === '416' || areaCode === '647')
)
)
return false;
// otherwise it always return true
return true;
}
Now that the interceptor is created, we are going to apply this to the
controller with the CoffeeShop
endpoints, CoffeeShopController
.
// Add these imports for interceptors
import {inject, intercept} from '@loopback/core';
import {ValidatePhoneNumInterceptor} from '../interceptors';
// Add this line to apply interceptor to this class
@intercept(ValidatePhoneNumInterceptor.BINDING_KEY)
export class CoffeeShopController {
// ....
}
Reference
To find out more about interceptors, check out the blog posts below: