RxJS Basics for Interceptors

Observables in One Minute

NestJS wraps every handler result in an RxJS Observable, a stream of values. In interceptors you mostly need three operators:

OperatorDoesInterceptor use
map(fn)Replace each value with fn(value)Wrap the response { data }
tap(fn)Run a side effect, keep the valueLogging, metrics
catchError(fn)Handle an error, return a new streamConvert errors
timeout(ms)Error if nothing arrives in timeRequest timeouts

A Tiny Observable

Example

rxjs is not available here, so this is a mini Observable with pipe, map and tap. It behaves like the real thing for these operators:

type Observer<T> = (value: T) => void;
type Operator<A, B> = (source: Observable<A>) => Observable<B>;

class Observable<T> {
  constructor(private producer: (next: Observer<T>) => void) {}
  subscribe(next: Observer<T>) { this.producer(next); }
  pipe<A>(op1: Operator<T, A>): Observable<A>;
  pipe<A, B>(op1: Operator<T, A>, op2: Operator<A, B>): Observable<B>;
  pipe(...ops: Operator<any, any>[]): Observable<any> {
    return ops.reduce((src, op) => op(src), this as Observable<any>);
  }
}
const of = <T>(value: T) => new Observable<T>((next) => next(value));
const map = <A, B>(fn: (a: A) => B): Operator<A, B> => (src) => new Observable((next) => src.subscribe((v) => next(fn(v))));
const tap = <A>(fn: (a: A) => void): Operator<A, A> => (src) => new Observable((next) => src.subscribe((v) => { fn(v); next(v); }));

// what next.handle() would return for a controller that returns a product
const handlerResult = of({ id: 7, name: 'boAt Airdopes 141', price: 1299 });

handlerResult
  .pipe(
    tap((p) => console.log('[log] handler returned product', p.id)),
    map((p) => ({ success: true, data: p })),
  )
  .subscribe((body) => console.log('response body:', JSON.stringify(body)));

Output

[log] handler returned product 7
response body: {"success":true,"data":{"id":7,"name":"boAt Airdopes 141","price":1299}}