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
Routing API
A basic bit of routing might look like this:
get("/", async () => ResOf(200, "Home page"))
.withGet("/about", async () => ResOf(200, "About us."));
We can declare routing separately and combine at a later stage:
// root stays the same
const houseKeeping = get("/", async () => ResOf(200, "Root"))
.withGet("/about", async () => ResOf(200, "About us."));
// some other routes whose root is /hotels/{name}
const hotelRoutes = get("/hotels/{name}", async (req) => {
return ResOf(200, hotels[req.pathParams.name]);
}).withGet("/hotels/{name}/properties/{property}", async (req) => {
const body = hotels[req.pathParams.name].properties[req.pathParams.property];
return ResOf(200, body);
})
// combine them
houseKeeping.withRoutes(hotelRoutes)
.asServer()
.start();
// some data
const hotels = {
"tom-hotel": {
name: "Tom Hotel", numberOfProperties: 2, properties: {
"Cola Beach": {name: "Cola Beach", size: 20, location: "London"},
"Lilt Lookover": {name: "Lilt Lookover", size: 20, location: "New York"}
}
}
}
Matching handler path
Routes are matched left to right and deepest first. Declaring separate groups of routes and then combining them later helps make your code modular and more clearly testable. Eg. we might declare a few groups like so:
const houseKeeping = get("/home", async () => {
return ResOf(200, "home page");
}).withGet("/about", async () => {
return ResOf(200, "about us");
});
const userSignUp = get("/register", async () => {
return ResOf(200, "register");
}).withPost("/register", async (req: Req) => {
const {username, password} = req.bodyForm();
const saved = await userService.register(username, password);
return ResOf(saved ? 200 : 400, saved ? "Welcome!" : "Try again.");
});
const combinedRoutes = houseKeeping
.withRoutes(userSignUp)
In the above case, userSignUp
routes will be recursed through first.
The general case, “left to right and deepest first” looks like:
__A__
/ \ \
B D G
/ \
C E
\
F
Which we achieve by writing
A.withRoutes(
B.withRoutes(C)
).withRoutes(
D.withRoutes(
E.withRoutes(F)
)
).withRoutes(G)
And the result would be that we match on C
then B
then F
then
E
then D
then G
then A
Matching on a header
We can pass a header to a route and it will only match Req
s made with that header.
const requestAcceptText = ReqOf("GET", "/tom").withHeader(Headers.ACCEPT, HeaderValues.APPLICATION_JSON);
await route(requestAcceptText, async() => ResOf(200, "Accepted text"))
.serve(requestAcceptText)
Filters passed through
Top level filters apply to all routes. But only those filters within a group
of routes and only as deep as the matched handler will be applied.
E.g. matching handler E
in the following will only apply filters
attached to E
, D
and A
.
__A__
/ \ \
B D G
/ \
C E
\
F
Symmetry of routing and serving
We can also declare a route using a Req object:
const requestAcceptText = ReqOf("GET", "/tom").withHeader(Headers.ACCEPT, HeaderValues.APPLICATION_JSON);
const requestAcceptJson = ReqOf("GET", "/tom").withHeader(Headers.ACCEPT, HeaderValues.TEXT_HTML);
const response = await route(ReqOf("GET", "/"), async() => ResOf(200, "Hiyur"))
.withRoute(requestAcceptText, async() => ResOf(200, "Hiyur text")) //will match this route based on header
.withRoute(requestAcceptJson, async() => ResOf(200, "Hiyur json"))
.serve(requestAcceptText); //serve with same request used to declare routing
Path params
We’ve seen above how to specify path params:
get("/hotels/{name}/property/{property}", async (req) => {
return ResOf(200, req.pathParams)
}).serve(
ReqOf(Method.GET, "http://localhost:3000/hotels/Tom-Hotel/property/Cola-Beach")
);
//pathParams: { name: 'Tom-Hotel', property: 'Cola-Beach' }
Query params
Query params are available in a similar way.
get("/hotels", async (req) => {
const nameQuery = req.queries['name'];
const filteredHotels = hotels.filter(hotel => hotel.name === nameQuery);
return ResOf(200, filteredHotels);
}).serve(
ReqOf(Method.GET, "http://localhost:3000/hotels").withQuery("name", "Tom Hotel")
);
Form params
And form params are available in a similar way too.
post("/hotels", async (req) => {
const hotelName = req.bodyForm()['name'];
return ResOf(200, hotelName);
}).serve(
ReqOf(Method.POST, "http://localhost:3000/hotels").withFormField("name", "Tom Hotel")
);
We need to call bodyForm()
because by default, our Req
has a Readable
stream as its body, and therefore we cannot get at the form
without reading
the stream into memory.
Convenience methods
We have withGet
, withPost
, etc…
For terseness we can rewrite:
return get("/", async () => {
return ResOf(200, "root");
}).withHandler("GET", "/family/{name}", async () => { //withHandler
return ResOf(200, "least precise");
})
as the following
return get("/", async () => {
return ResOf(200, "root");
}).withGet("/family/{name}", async () => { //withGet
return ResOf(200, "least precise");
})
List of routes
We give you back the routes you’ve listed, in case you want to describe your API
get("/", async() => ResOf())
.withRoute(ReqOf("POST", "/tosh", "", {[Headers.CONTENT_TYPE]: HeaderValues.APPLICATION_JSON}),
async() => ResOf())
.withPut("/putsch", async() => ResOf())
.routes()
will return you
[
{method: "GET", path: "/", headers: {}},
{method: "POST", path: "/tosh", headers: {"Content-Type": "application/json"}},
{method: "PUT", path: "/putsch", headers: {}},
]
The full API is as follows:
class Routing {
server: Http4jsServer;
constructor(method: string,
path: string,
headers: HeadersType = {},
handler: HttpHandler)
withRoutes(routes: Routing): Routing
withRoute(request: Req, handler: HttpHandler): Routing
withFilter(filter: Filter): Routing
withHandler(method: string, path: string, handler: HttpHandler, headers: HeadersType = {}): Routing
asServer(server: Http4jsServer = HttpServer(3000)): Http4jsServer
serve(request: Req): Promise<Res>
match(request: Req): MountedHttpHandler
withGet(path: string, handler: HttpHandler): Routing
withPost(path: string, handler: HttpHandler): Routing
withPut(path: string, handler: HttpHandler): Routing
withPatch(path: string, handler: HttpHandler): Routing
withDelete(path: string, handler: HttpHandler): Routing
withOptions(path: string, handler: HttpHandler): Routing
withHead(path: string, handler: HttpHandler): Routing
}
routes(method: string, path: string, handler: HttpHandler, headers: HeadersType = {}): Routing
route(request: Req, handler: HttpHandler): Routing
get(path: string, handler: HttpHandler, headers: HeadersType = {}): Routing
post(path: string, handler: HttpHandler, headers: HeadersType = {}): Routing
put(path: string, handler: HttpHandler, headers: HeadersType = {}): Routing
patch(path: string, handler: HttpHandler, headers: HeadersType = {}): Routing
options(path: string, handler: HttpHandler, headers: HeadersType = {}): Routing
head(path: string, handler: HttpHandler, headers: HeadersType = {}): Routing
Prev: URI API
Next: In Memory Testing