Using Nest.js I would like to trim() all @body() input values

Viewed 3748

I would like to trim (empty white spaces at the beggining / end of a input field) of all body values. I don't want to have to loop all body elements for each API request to clean the fields up.

I was wondering if I can overwrite the @body() annotation, and put the code in there, or if there's a input formatter or pipe that does that.

At the moment, I'm doing this:

createAccount(@Body() body: any) {
  this.account.create(body.map(s => s.trim()))
}

Thanks

3 Answers

Thank you Uroš Anđelić for your advise. I created a PipeTransform to take care of this:

import { Injectable, PipeTransform, 
ArgumentMetadata, BadRequestException } from '@nestjs/common'

@Injectable()
export class TrimPipe implements PipeTransform {
  private isObj(obj: any): boolean {
    return typeof obj === 'object' && obj !== null
  }

  private trim(values) {
    Object.keys(values).forEach(key => {
      if (key !== 'password') {
        if (this.isObj(values[key])) {
          values[key] = this.trim(values[key])
        } else {
          if (typeof values[key] === 'string') {
            values[key] = values[key].trim()
          }
        }
      }
    })
    return values
  }

  transform(values: any, metadata: ArgumentMetadata) {
    const { type } = metadata
    if (this.isObj(values) && type === 'body') {
      return this.trim(values)
    }

    throw new BadRequestException('Validation failed')
  }
}

And this is how to use it

  @UsePipes(new TrimPipe())
  createAccount(@Body() body: any) {
    this.account.create(body)
  }

You can also set it up as a global pipe:

app.useGlobalPipes(new TrimPipe());

I made an interceptor just for that. I use it globally but you can use it wherever you want with @UseInterceptors decorator. Here is the base class that can be extended for other body transformations also:

import { CallHandler, ExecutionContext, NestInterceptor } from '@nestjs/common'
import { Observable } from 'rxjs'

export abstract class TransformRequest implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    this.cleanRequest(context.switchToHttp().getRequest())
    return next.handle()
  }

  cleanRequest(req: any): void {
    req.query = this.cleanObject(req.query)
    req.params = this.cleanObject(req.params)

    // If you use express adapter you will have
    // req.method
    // If you use fastify adapter you will have
    // req.raw.method

    if (req.raw.method !== 'GET') {
      req.body = this.cleanObject(req.body)
    }
  }

  cleanObject(obj: object | null | undefined) {
    if (!obj) {
      return obj
    }

    for (const key in obj) {
      // Prototype of obj is null
      // if (!obj.hasOwnProperty(key)) {
      //   continue
      // }

      const value = obj[key]

      // If the value is another nested object we need to recursively
      // clean it too. This will work for both array and object.
      if (value instanceof Object) {
        this.cleanObject(value)
      } else {
        // If the value is not an object then it's a scalar
        // so we just let it be transformed.
        obj[key] = this.transform(key, value)
      }
    }

    return obj
  }

  abstract transform(key: string | number, value: boolean | number | string | null | undefined): any
}

And here is the trim strings class:

import { Injectable } from '@nestjs/common'
import { TransformRequest } from './transform.request'

@Injectable()
export class TrimStrings extends TransformRequest {
  private except = ['password']

  transform(key: string | number, value: any) {
    if (this.isString(value) && this.isString(key) && !this.except.includes(key)) {
      return value.trim()
    }

    return value
  }

  isString(value: any): value is string {
    return typeof value === 'string' || value instanceof String
  }
}

You can also find it in this repository.

I actually think it's a better idea to trim the body in the front-end side if that is an option.

You can extend NestMiddleware class for that also:

import { NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';

export class TrimMiddleware implements NestMiddleware {
  use(req: Request, _res: Response, next: NextFunction) {
    const requestBody = req.body;
    if (this.isObj(requestBody)) {
      req.body = this.trim(requestBody);
    }
    next();
  }
  private isObj(obj: any): boolean {
    return typeof obj === 'object' && obj !== null;
  }

  private trim(value: unknown) {
    if (typeof value === 'string') {
      return value.trim();
    }

    if (Array.isArray(value)) {
      value.forEach((element, index) => {
        value[index] = this.trim(element);
      });
      return value;
    }

    if (this.isObj(value)) {
      Object.keys(value).forEach(key => {
        value[key] = this.trim(value[key]);
      });
      return value;
    }

    return value;
  }
}

And then in NestModule you can use apply and forRoutes methods to decide on which endpoint apply middleware.

Related