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