http4js

Table of Contents

Request and Response API

There are two ways to build a Req or Res:

new Res(200, 'body'); //or
ResOf(200, 'body');

new Req("GET", "/"); //or
ReqOf("GET", "/")

They are named Req and Res in this short-hand manner so that they don’t conflict with other objects named Request and Response.

Streaming

Incoming Reqs are streamed (as is default in Node). We expose a handle on this stream with the function bodyStream().

A simple Req streamed in and out to a streamed Res would look like this:

get("/body-stream", async (req: Req) => {
    return ResOf(200, req.bodyStream()!);
})

In order to respond with a stream, we can return a ResOf(200, readable) where readable is a Readable stream.

Streaming a large file for example, is as simple as:

get('/bigfile', async() => ResOf(200, fs.createReadStream('./bigfile.txt')))
    .asServer()
    .start();

Or proxy streaming is as simple as:

get('/bigfile', async() => ResOf(200, fs.createReadStream('./bigfile.txt')))
    .asServer(HttpServer(3006))
    .start();

get('/proxy/bigfile', async() => {
    // proxy this response 
    const response = await HttpClient(ReqOf('GET', 'http://localhost:3006/bigfile'));
    // streamed file from http://localhost:3006/bigfile and out to http://localhost:3007/proxy/bigfile
    return ResOf(200, response.bodyStream()); // streamed! :)
})
    .asServer(HttpServer(3007))
    .start();

Large bodies

As node presents bodies as a stream (and streams are asynchronous), we have an async convenience method for large bodies (>65kb) that wraps streaming the chunks.

await response.fullBodyString()

will return the full body.

Building a form

We can build up the form on a Req easily.

const req = ReqOf('POST', '/form')
    .withForm({name: 'tom'})
    .withFormField('name', 'tosh')
    .withForm({name: 'ben', age: '31'})

req.formBodyString()
/*
    name=tom&name=tosh&name=ben&age=31
 */

withForm and withFormField will accumulate values as expected in a form and set the Content-Type header to application/x-www-form-urlencoded.

How it works

Our NativeHttpServer sees that the Res has a bodyStream and streams the outgoing response.

const bodyStream = response.bodyStream();
if (bodyStream){
    bodyStream.pipe(res);
} else {
    res.write(response.bodyString());
    res.end();
}

Our HttpClient works in the same way, streaming in and out.

Convenience methods for starting a server

We provide HttpServer(3000) and HttpsServer(3000, certs) as quick easy ways to provide a server.

Immutability - why?

Both Req and Res are immutable, so every method on them returns a new object. For example we might want to set a header on a Req but then replace it:

const request = ReqOf(Method.GET, "/")
                    .withHeader(Headers.EXPIRES, "max-age=60")
                    
const noMaxAgeRequest = request.replaceHeader(Headers.EXPIRES, "max-age=0");

request and noMaxAgeRequest are different objects. This stops us from passing around state all over our codebase and finding it hard to know where our Req or Res is mutated. For example, it stops the following:

get("/" , async (req: Req) => {
    doSomethingOverThere(req)
    return ResOf(200, req.bodyString());
})

function doSomethingOverThere(req: Req): number {
    req.withHeader(Headers.CACHE_CONTROL, "no-cache, no-store, must-revalidate")
    return Math.random();
}

where our function doSomethingOverThere takes a Req and tries to set a cache control directive but is actually doing something else - returning a number. Because doSomethingOverThere doesn’t return the Req explicitly and because Req is immutable, it actually has no effect on the Req used in new Res(200, req.body) because Req is immutable.

API

We provide builder functions Req and Res because their full names Request and Response conflict with other libraries using the same names. So you might write:

get("/", async (req) => ResOf(200, "Hello, world!"))
    .asServer()
    .start();

HttpClient(ReqOf("GET", "http://localhost:3000/")).then(res=>console.log(res));

Alternatively you can use the HttpClient interface using ReqOptions:

const response = await HttpClient({method: 'GET', uri: 'http://localhost:3013/'});

where ReqOptions is just an object representing a Req:

interface ReqOptions {
    method: string;
    uri: Uri | string;
    body?: Body | BodyContent;
    headers?: Headers | HeadersJson;
}

We also have Res helpers to be more descriptive

Res.OK
Res.Created
Res.NoContent
Res.Redirect
Res.MovedPermanently
Res.Found
Res.SeeOther
Res.NotModified
Res.TemporaryRedirect
Res.BadRequest
Res.Unauthorized
Res.Forbidden
Res.NotFound
Res.InternalServerError
Res.BadGateway
Res.ServiceUnavailable
Res.GatewayTimeout

which is just sugar, for eg. Res.SeeOther(location) is sugar for ResOf(302).withHeader("Location", "/somewhere/else")

The full api is as follows:


class Req {
    withUri(uri: Uri | string): Req
    
    header(name: string): string 
    
    withHeader(name: string, value: string): Req 
    
    replaceHeader(name: string, value: string): Req 
    
    removeHeader(name: string): Req
    
    withBody(body: string): Req
    
    withFormField(name: string, value: string | string[]): Req 
    
    withForm(form: object): Req 
    
    formField(name: string): string | string[]
    
    bodyString(): string
    
    bodyForm(): {}
    
    withQuery(name: string, value: string): Req
    
    withQueries(queries: {}): Req
    
    query(name: string): string
}

class Res {
    header(name: string): string

    withHeader(name: string, value: string): Res 

    withHeaders(headers: HeadersType): Res 

    replaceAllHeaders(headers: HeadersType): Res 

    replaceHeader(name: string, value: string): Res 

    removeHeader(name: string): Res 

    withBody(body: string): Res 

    bodyString(): string 
}

Prev: Handlers and Filters

Next: URI API