DEV Community

Cover image for Asynchronous Iterable HTTP Server in Node.js
Amin
Amin

Posted on • Updated on

Asynchronous Iterable HTTP Server in Node.js

Since I played a little with Deno, I noticed that their server was created using an asynchronous iterable, which means that we can use the for...await...of loop and simplify a lot the writing of web servers.

Since then I kept on digging and I finally found a way to make our beloved http server in Node.js an asyncronous iterable.

import http from "http"

const getBody = async request => {
    let body = ""

    for await (const chunk of request) {
        body += chunk.toString()
    }

    return body
}

const createServer = () => {
    const server = http.createServer()

    server[Symbol.asyncIterator] = function() {
        return {
            next() {
                return new Promise((resolve, reject) => {
                    const onRequest = (request, response) => {
                        resolve({
                            done: false,
                            value: { request, response }
                        })

                        server.off("request", onRequest)
                    }

                    server.on("request", onRequest)
                })
            }
        }
    }

    return server
}

const server = createServer()

server.listen(8000, "0.0.0.0", () => {
    console.log("Server listening")
})

for await (const {request, response} of server) {
    if (request.method === "POST") {
        const body = await getBody(request)
        console.log(body)
    }

    response.end("OK")
}
Enter fullscreen mode Exit fullscreen mode

Iterator

An iterator is a construct that allows things like for...of loops and the spread operator [...iterable] to operate on iterable objects.

This is also why we can't loop over an object: objects do not defin a Symbol.iterator method that is checked by the for...of loop at runtime, hence why we get the error something is not iterable if we try to loop over a plain object.

Strings, arrays and more objects in JavaScript already define a Symbol.iterator method that returns the Iterator Protocol: an object that define a method next that is called each time we need to send a new iteration.

Asynchronous Iterator

An asynchronous iterator is simply an iterator that gets its value from the result of a promise. This means that we can also send back values that have been awaited and that enables a whole new world of possibilities for our iterators.

Object can be made iterable

Any object can be made an iterable version of itself, even numbers (since almost everything is an object in JavaScript).

This means that things like [...100] could be possible in a near future because the Number constructor can contain a method Symbol.iterator in its prototype.

Of course, it is highly discouraged to augment the prototyes of native objects in JavaScript but this is a fun exercise to do if you want to experiment with iterators.

Number.prototype[Symbol.iterator] = function* () {
    for (let number = 0; number < this; number++) {
        yield number
    }
}

console.log([...10])
// [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
Enter fullscreen mode Exit fullscreen mode

Again, do not reproduce in a production application because it is very dangerous to augment the prototype of native objects and can cause severe damages and conflicts in libraries that might use these prototypes or even if the language is being updated with the a similar construct.

Node requests are asynchronous iterables

For a long time, I used to create little helpers to help me get the body of my request, which can be then translated from a Stream (which is at the heart of the Node platform but a concept that is hard to grasp) to a Promise.

import http from "http"

const getBody = request => {
    return new Promise((resolve, reject) => {
        let body = ""

        request.on("data", chunk => {
            body += chunk.toString()
        })

        request.on("end", () => {
            resolve(body)
        })

        request.on("error", error => {
            reject(error)
        })
    })
}

http.createServer(async (request, response) => {
    if (request.method === "POST") {
        const body = await getBody()
        console.log(body)
    }

    response.end("OK")
}).listen(8000, "0.0.0.0", () => {
    console.log("Listening")
})
Enter fullscreen mode Exit fullscreen mode

request here is a Stream, and streams implement the EventEmitter class, which enables us to listen to events when a chunk of data is decoded from the HTTP stream, or when an error occurred for instance.

This is not a trivial task since manipulating events can be hard at first, especially for beginners or even confirmed developers.

We could also have done it another way, by using a for...await...of loop.

import http from "http"

const getBody = async request => {
    let body = ""

    for await (const chunk of request) {
        body += chunk.toString()
    }

    return body
}

http.createServer(async (request, response) => {
    if (request.method === "POST") {
        const body = await getBody()
        console.log(body)
    }

    response.end("OK")
}).listen(8000, "0.0.0.0", () => {
    console.log("Listening")
})
Enter fullscreen mode Exit fullscreen mode

This is because request implements the Iterator protocol and returns an asynchronous generator (which is another kind of iterator).

This allows us to use the for...await...of loop, which is almost like the for...of loop and is a popular way to loop over things in JavaScript, thus making it very clear to the eyes of the reader of what is happening here.

Unfortunately, when creating a new HTTP server, the server object itself does not implement the asynchronous iterator protocol, thus why we need to implement it ourselves if we really need to push this deeper.

import http from "http"

const getBody = async request => {
    let body = ""

    for await (const chunk of request) {
        body += chunk.toString()
    }

    return body
}

const createServer = () => {
    const server = http.createServer()

    server[Symbol.asyncIterator] = function() {
        return {
            next() {
                return new Promise((resolve, reject) => {
                    const onRequest = (request, response) => {
                        resolve({
                            done: false,
                            value: { request, response }
                        })

                        server.off("request", onRequest)
                    }

                    server.on("request", onRequest)
                })
            }
        }
    }

    return server
}

const server = createServer()

server.listen(8000, "0.0.0.0", () => {
    console.log("Server listening")
})

for await (const {request, response} of server) {
    if (request.method === "POST") {
        const body = await getBody(request)
        console.log(body)
    }

    response.end("OK")
}
Enter fullscreen mode Exit fullscreen mode

In my opinion, I think that it could be great to implement it in Node.js and would make the writing of web server super easy for people wanting to use the built-in HTTP server.

Of course this is not a replacement for other routers like Express of Fastify that bring more things to the table but this would be a great way to get inspiration from other platforms like Deno that thrives to make the life of its users easier.

Top comments (1)

Collapse
 
dev4ever profile image
dev4ever

Hello,

There is an error in your script, would you know how to fix it?

for await (const {request, response} of server) {
SyntaxError: Unexpected reserved word

It refers to the await