It's easy to build a big ball of mud when working with technology we do not understand. Two years into a project, we found ourselves converting among typed arrays, streams, and buffers, without really knowing our stuff. Today, about an hour of research help cut through the Gordion Knot and produce a simpler, maintainable solution.

Of note is that events.EventEmitter has a pipe method with the following signature.

pipe<T extends NodeJS.WritableStream>(
  destination: T, 
  options?: { end?: boolean; }
): T;

As a result, so long we have an object that extends events.EventEmitter, we can pipe its contents into any object that implements NodeJS.WritableStream. Result: we can pipe any stream.Stream into an express.Response!

My conclusion is that, when dealing with streams, unless there is a pressing need, use the smallest possible interface. That usually means using stream.Stream.

                        events.EventEmitter
                                ^
                                |
                                |
NodeJS.ReadableStream    stream.Stream          NodeJS.WritableStream
       ^                 ^           ^              ^
       |                /             \             |
       |               /               \            |
stream.Readable ______/                 \______ stream.Writable
       ^                                            ^                        
       |                                            |
       |                                            |
fs.ReadStream                                http.OutgoingMessage
                                                    ^
                                                    |
                                                    |
                                             http.ServerResponse
                                                    ^
                                                    |
                                                    |
                                             express.Response