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 Req
s 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