Simulating the Pipeline

The Whole Lifecycle in Plain TypeScript

Example

This program runs three requests through a tiny pipeline with the same stages and order as NestJS: one succeeds, one is stopped by the guard, one fails validation in the pipe. Every thrown error goes to the exception filter.

class HttpError extends Error {
  constructor(readonly status: number, message: string) { super(message); }
}
type Req = { method: string; url: string; headers: Record<string, string>; body: Record<string, unknown> };
type Handler = (body: { productId: number; qty: number }) => Promise<unknown>;

const middleware = (req: Req) => console.log('  [middleware] ' + req.method + ' ' + req.url);
const authGuard = (req: Req) => {
  const ok = req.headers.authorization === 'Bearer good-token';
  console.log('  [guard] auth ' + (ok ? 'passed' : 'failed'));
  if (!ok) throw new HttpError(401, 'Unauthorized');
};
const validationPipe = (body: Record<string, unknown>) => {
  const qty = Number(body.qty);
  console.log('  [pipe] validating body');
  if (!Number.isInteger(qty) || qty < 1) throw new HttpError(400, 'qty must be a positive integer');
  return { productId: Number(body.productId), qty };
};
const handler: Handler = async (dto) => {
  console.log('  [handler] creating order for product ' + dto.productId);
  return { orderId: 501, ...dto };
};
const exceptionFilter = (e: unknown) => {
  const err = e instanceof HttpError ? e : new HttpError(500, 'Internal server error');
  console.log('  [filter] ' + err.status);
  return { status: err.status, body: { statusCode: err.status, message: err.message } };
};

async function handle(req: Req) {
  console.log(req.method + ' ' + req.url);
  try {
    middleware(req);
    authGuard(req);
    console.log('  [interceptor] before');
    const dto = validationPipe(req.body);
    const result = await handler(dto);
    console.log('  [interceptor] after: wrapping result');
    return { status: 201, body: { data: result } };
  } catch (e) {
    return exceptionFilter(e);
  }
}

async function main() {
  const auth = { authorization: 'Bearer good-token' };
  console.log(await handle({ method: 'POST', url: '/orders', headers: auth, body: { productId: 7, qty: 2 } }));
  console.log(await handle({ method: 'POST', url: '/orders', headers: {}, body: { productId: 7, qty: 2 } }));
  console.log(await handle({ method: 'POST', url: '/orders', headers: auth, body: { productId: 7, qty: 0 } }));
}
main();

Output

POST /orders
  [middleware] POST /orders
  [guard] auth passed
  [interceptor] before
  [pipe] validating body
  [handler] creating order for product 7
  [interceptor] after: wrapping result
{ status: 201, body: { data: { orderId: 501, productId: 7, qty: 2 } } }
POST /orders
  [middleware] POST /orders
  [guard] auth failed
  [filter] 401
{ status: 401, body: { statusCode: 401, message: 'Unauthorized' } }
POST /orders
  [middleware] POST /orders
  [guard] auth passed
  [interceptor] before
  [pipe] validating body
  [filter] 400
{
  status: 400,
  body: { statusCode: 400, message: 'qty must be a positive integer' }
}

Exercise

Exercise: In the program above, where would a ThrottlerGuard (rate limit) go, and what status should it produce? Where would a cache check go?

Show answer
// Rate limit: a guard, run with (or before) the auth guard -> throws 429 Too Many Requests.
// Cache: an interceptor "before" step. If the key is cached, return the cached
// value and skip pipes + handler entirely; otherwise run them and store the result
// in the "after" step.