This small post is intended to show an alternative method for handling HTTP connections.

It demonstrates a pattern that is known from the alternative JavaScript framework Deno, which is actually more of a TypeScript framework.

Background

Deno is an alternative framework to Node.js and is compatible in many aspects. Two additional main goals are to ensure high security in the execution of scripts (sandbox mode and explicit permissions) and also to implement the well-known browser APIs, like Blobs, FormData or web storage.

It is written in Rust and supports TypeScript natively.

Now the idea is to handle HTTP connections in Node through a for loop, just like it can be done in Deno, instead of using a legacy callback or event pattern:

const server = Deno.listen({
    port: 8080
});

for await (const connection of server) {
    // handle `connection`
}

Implementation

The demo project on GitHub demonstrates how to make use of so called AsyncIterator protocol.

First we have to do is to define an interface for our HTTP server:

// ...

/**
 * A HTTP server.
 */
export interface IHttpServer extends AsyncIterable<IHttpConnection>, NodeJS.EventEmitter {
}

// ...

On the one hand, the interface describes, that it can be used in an asynchronious use of a for loop by returning IHttpConnection objects in each loop.

Such an object will hold the request and response contextes of the underlying HTTP connection:

// ...

import {
    IncomingMessage,
    ServerResponse
} from 'node:http';

// ...

/**
 * A connection between a HTTP server and a remote client.
 */
export interface IHttpConnection {
    /**
     * The request context.
     */
    request: IncomingMessage;
    /**
     * The response context.
     */
    response: ServerResponse;
}

// ...

On the other hand NodeJS.EventEmitter interface ensures, that we will implement a server class that is able to handle events. For this we can use shipped-in EventEmitter from node:events module as base class.

Create and start a new server

To create and start a new HTTP server instance, I decided to implement a factory function, called listenHttp():

// ...

export function listenHttp(port: number | string = 8080): IHttpServer {
    // ...
}

In the very first step, we create a new Node.js server:

// ...

import {
    createServer,
    IncomingMessage,
    Server,
    ServerResponse
} from 'node:http';

// ...

export function listenHttp(port: number | string = 8080): IHttpServer {
    const instance = createServer();

    // ...
}

Next, we implement the AsyncIterator, I already mentioned, by only implementing the next() method.

On every call, this will return a Promise that listens to error and request events of the server instance, we created in the previous step.

And calling this method is what an asynchronious use of for loop does:

// ...

type HttpIterator = AsyncIterator<IHttpConnection, any, undefined>;

type HttpIteratorResult = IteratorResult<IHttpConnection, any>;

// ...

export function listenHttp(port: number | string = 8080): IHttpServer {
    // ...

    const iterator: HttpIterator = {
        next: () => {
            return new Promise<HttpIteratorResult>((resolve, reject) => {
                instance.once('error', (error) => {
                    reject(error);
                });

                instance.once('request', (request, response) => {
                    const newConnection: IHttpConnection = {
                        request,
                        response
                    };

                    resolve({
                        done: !instance.listening,  // we do not continue when we are not listening anymore
                        value: newConnection
                    });
                });
            });
        }
    };

    // ...
}
// ...

Both instances we now give to the constructor of our internal HttpServer class, which is an implementation of IHttpServer interface but not accessible from outside:

// ...

export function listenHttp(port: number | string = 8080): IHttpServer {
    // ...

    const server = new HttpServer(instance, iterator);

    // ...
}

Before we start listening on the submitted port, we forward all error events of the Node.js server to our own HttpServer instance:

// ...

export function listenHttp(port: number | string = 8080): IHttpServer {
    // ...

    // proxy from Node server instance
    // to own `HttpServer` instance
    server.instance.on('error', (instanceError) => {
        server.emit('error', instanceError);
    });

    // now start listening
    server.instance.listen(Number(port), '0.0.0.0');

    return server;
}

Usage

That’s all we need to do.

You can now start a HTTP server instance and handle connections in a for loop:

// ...

async function handleConnection(connection: IHttpConnection) {
    const {
        request,
        response
    } = connection;

    try {
        // ...
    } catch (error) {
        const errorMessage = Buffer.from(
            String(error), 'utf8'
        );

        if (!response.headersSent) {
            response.writeHead(500, {
                'Content-Type': 'text/plain; charset=UTF-8',
                'Content-Length': String(errorMessage.length)
            });
        }

        response.write(errorMessage);
    } finally {
        response.end();  // ensure connection is closed
    }
}

const server = listenHttp(8080);

for await (const connection of server) {
    // we do not need an await here
    //
    // this here ensures that actions are handled
    // as asynchronious as possible
    handleConnection(connection);
}

// ...

Have fun while implementing and trying it out! 🎉