NestJS / Intermediate: policies, adapters and sources / Lesson 9
Interceptors, observables and safe serialization
Wrap result execution deliberately while preserving stream behavior, public shape and exception meaning.
On this page
Lesson goals
- Explain next.handle and inbound/outbound interceptor order.
- Separate output projection from access policy and cache correctness.
An interceptor surrounds an execution boundary
A Nest interceptor can run logic before the handler and transform the observable result path afterward. next.handle represents the continuation; returning without invoking it can short-circuit the handler intentionally or accidentally. When several interceptors wrap a request, outbound work unwinds in reverse nesting order. Keep concerns such as timing, safe projection and error adaptation explicit. An interceptor does not create a storage transaction merely by wrapping a controller, and a response envelope does not prove object eligibility. Review whether the same component is valid for HTTP, RPC or streaming output before applying it globally.
Transform one ordinary result without hiding failures
The illustrative interceptor wraps successful emissions under data using an RxJS map operator. It assumes imports, interface definitions and registration. It does not catch failures or claim that every endpoint should use this envelope. Applying the same mapping to an already enveloped result can double-wrap it, while applying it to native response handling may not have the intended framework-managed effect. Test the exact result shape and error channel. For streams, one emission is not necessarily one complete HTTP response, and materializing a stream to build a summary can destroy bounded-memory behavior.
@Injectable()
export class EnvelopeInterceptor implements NestInterceptor {
intercept(context: ExecutionContext,next: CallHandler) {
return next.handle().pipe(map(value => ({data:value})));
}
}
RxJS map and Nest imports are assumed. This transforms emissions conceptually and was not registered or executed.
Class metadata only works through its actual integration
Class-based serialization can exclude or expose properties under the selected interceptor and class instance setup. A plain object that merely resembles a class is not automatically evidence that exclusion metadata ran. Return an intentional public model and verify exactly which keys leave the boundary. Nest 12 also documents schema-based output alternatives; choose and configure one approach rather than assuming declarations enforce themselves. Serialization should not reveal a forbidden object and then try to hide the sensitive field alone. Apply domain eligibility first, and keep internal persistence fields out of the public contract.
Test shape and nonmutation locally
The local function builds a fresh envelope around a narrow card projection. It leaves the source record untouched and omits a synthetic secret. This verifies a plain mapping decision, not class-transformer metadata, RxJS completion or interceptor binding. A real output test should also cover arrays, null, expected error payloads and already wrapped values according to the chosen contract. Do not add a universal mapper that silently changes every representation. A download, a 204 response and a live event stream have different body semantics from an ordinary JSON card.
function publicEnvelope(source) {return {data:{id:source.id,title:source.title}};}
const source={id:"c1",title:"Interceptors",secret:"lab-only"};
const result=publicEnvelope(source);
console.assert(JSON.stringify(result) === '{"data":{"id":"c1","title":"Interceptors"}}');
console.assert(source.secret === "lab-only");
console.assert(!Object.hasOwn(result.data,"secret"));
The result has only the selected keys and the original remains unchanged. No observable or serializer was executed.
Measure and adapt without swallowing the contract
Use finalization behavior carefully to observe success, failure or cancellation without converting them all into success. A catch-and-return fallback can hide an unavailable permission source or turn a failed write into a misleading 2xx body. Timeouts need source cancellation support; stopping an observable subscription does not universally undo external work. Keep metrics labels bounded and secrets out of logs. Verify registration through container-aware bindings when dependencies are needed. This course executes the local projection only, leaving observable subscription, cancellation, platform response and serialization integrations untested. An honest report preserves those distinct boundaries.
Common errors and fixes
A controller never runs
- Why it happens
- An interceptor returned without the intended continuation.
- How to fix it
- Call next.handle where execution should proceed.
- Prevent it next time
- Assert handler calls and deliberate short-circuit cases.
The response has data.data unexpectedly
- Why it happens
- Several layers applied the same envelope.
- How to fix it
- Assign one output-shape owner.
- Prevent it next time
- Test exact nested keys and special representations.
A failure becomes an apparently successful fallback
- Why it happens
- Error adaptation erased domain meaning or authority requirements.
- How to fix it
- Preserve failure categories and safe disclosure.
- Prevent it next time
- Test unavailable permission and uncertain-write cases.
Check your understanding
1. Why is a plain object test not proof of class-based serialization?
Show worked solution
It does not instantiate the class or invoke the configured serializer and metadata. The output may coincidentally match the expected shape while the real interceptor is missing or receives another type. Keep the pure projection test, then separately verify class instances, binding and exact public keys through the intended Nest integration.
2. Can a timeout interceptor guarantee the source stopped?
Show worked solution
No. It can end the observed result path, but the external task may continue unless cancellation reaches a supporting adapter. Observe actual cleanup in an isolated source test and preserve uncertain write outcomes. Do not infer rollback from an observable timeout or promise rejection alone.
Key takeaways
- Interceptors wrap a specific result lifecycle, not every business guarantee.
- Serialization needs an actual configured runtime boundary.