In any API development project, ensuring that sensitive or unnecessary data is not exposed in the responses is crucial for security and efficiency. In NestJS, interceptors provide a powerful way to manipulate the flow of data. In this blog post, we’ll explore how to use an interceptor to sanitize API responses by removing specific properties. We’ll also look at how to write tests to ensure the interceptor behaves as expected.

Interceptors can be used to manipulate API responses in other ways as well. For instance, for creating pagination headers or similar purposes.

Background

Before diving into the implementation, let’s briefly understand the purpose of the interceptor and the scenario it addresses. The ResponsePayloadSanitizationInterceptor is designed to remove specific properties (e.g. 'my_super_secret_secret') from the API responses. These properties are often used internally by databases and might not be relevant or safe to expose to the clients consuming the API.

Implementation

The interceptor class ResponsePayloadSanitizationInterceptor implements the NestInterceptor interface. It intercepts the response data and uses a recursive approach to remove the specified properties from objects and arrays. The interceptor is applied globally to the application using app.useGlobalInterceptors(new ResponsePayloadSanitizationInterceptor()).

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

@Injectable()
export class ResponsePayloadSanitizationInterceptor<T> implements NestInterceptor<T> {
  readonly omittedProperties = ['my_super_secret_secret']

  intercept(context: ExecutionContext, next: CallHandler): Observable<T> {
    return next.handle().pipe(map((data) => this.sanitizeProperties(data)))
  }

  private sanitizeProperties(data: any): any {
    if (Array.isArray(data)) {
      return data.map((item) => this.sanitizeProperties(item))
    }
    if (typeof data === 'object' && data !== null) {
      const sanitizedData = {}
      for (const key in data) {
        if (!this.omittedProperties.includes(key)) {
          sanitizedData[key] = this.sanitizeProperties(data[key])
        }
      }
      return sanitizedData
    }
    return data
  }
}

Writing Tests

Writing tests is a crucial step to ensure that the interceptor behaves as expected. In this case, the test suite focuses on the TestController and verifies that the interceptor removes the specified property ('my_super_secret_secret') from the API response.

import { Controller, Get, HttpStatus, INestApplication } from '@nestjs/common'
import { Test } from '@nestjs/testing'
import { Public } from 'nest-keycloak-connect'
import * as request from 'supertest'

import { ResponsePayloadSanitizationInterceptor } from './response-payload-sanitization.interceptor'
import { AppModule } from '../../app.module'

@Public()
@Controller()
class TestController {
  // eslint-disable-next-line class-methods-use-this
  @Get('/api/test')
  test() {
    return {
      results: [
        {
          my_super_secret_secret: '1',
          value: 'hey',
          foo: {
            my_super_secret_secret: '2',
            value: 'hoy',
          },
        },
      ],
    }
  }
}

describe('ResponsePayloadSanitizationInterceptor', () => {
  let app: INestApplication

  beforeEach(async () => {
    app = (
      await Test.createTestingModule({
        imports: [AppModule],
        controllers: [TestController],
      }).compile()
    ).createNestApplication()
    app.useGlobalInterceptors(new ResponsePayloadSanitizationInterceptor())
  })

  it('should remove all secret properties from response', async () => {
    await app.init()
    const response = await request(app.getHttpServer()).get('/api/test')
    expect(response.status).toBe(HttpStatus.OK)
    expect(JSON.stringify(response.body)).toBe(
      JSON.stringify({
        results: [
          {
            value: 'hey',
            foo: {
              value: 'hoy',
            },
          },
        ],
      })
    )
  })

  afterEach(async () => {
    await app.close()
  })
})

The test case should remove all secret properties from response initializes the NestJS application, sends a request to the /api/test endpoint, and checks if the interceptor has successfully removed the specified properties from the response. The supertest library is used to make HTTP requests and assert the expected behavior.

Conclusion

Using interceptors in NestJS provides a clean and reusable way to modify the behavior of your API. The ResponsePayloadSanitizationInterceptor showcased here is a practical example of how interceptors can be used to sanitize API responses by removing specific properties. Writing tests ensures that the interceptor functions correctly and provides a safety net for future changes.

By incorporating this interceptor into your NestJS application, you can enhance the security and cleanliness of your API responses, making them more suitable for consumption by client applications.