Page Contents

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:

/src/controllers/coffee-shop.controller.ts

// 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.

/src/interceptors/validate-phone-num-interceptor.ts

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.

/src/controllers/coffee-shop.controller.ts

// 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: