http4js
Table of Contents
- Overview
- Handlers and Filters
- Request and Response API
- URI API
- Routing API
- In Memory Testing
- End to End Testing
- Approval testing with fakes
- Zipkin tracing
- Https Server
- Proxy
- Use in Javascript
- Example App
Request and Response API
There are two ways to build a Req or Res:
- you can new up the class
- you can use the builder function
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