Extending functionality
What and why extend
An httpc server can be extended with the following major components:
Custom context properties can be added to define better the information surrounding each request.
Custom parsers allow the server to handle any http request, in addition to the builtin httpc call requests. Custom parsers are very useful to handle requests from clients you don’t control or which follow different request conventions.
You can rewrite the
HttpCall
before the pipeline executes. Common scenarios are call-path rewriting to call a different function or to run checks before the pipeline executes.A Middleware encapsulates common logic you can reuse among different functions. Middleware logic wraps the function execution and can run pre-checks, parameter inspection, result processing. Common scenarios are authentication guards and parameters validation.
In addition to json outputs, custom renderers allow the server to respond with a different media-type. A custom render can also process the function result and apply arbitrary transformations.
Custom Context
The server context can be easily extended with custom properties.
The @httpc/server provides a basic context with the follow builtin properties:
property | type | description |
---|---|---|
requestId | string | random uuid auto generated at the beginning |
request | http.IncomingMessage | node native http request |
startedAt | number | unix timestamp as returned by Date.now() |
You can access the context with the useContext
hook:
const { requestId } = useContext();
To enrich the context with custom properties, just create a file called env.d.ts
in your project:
/// <reference types="@httpc/server/env" />
global {
interface IHttpCContext {
// example custom property
environment: string
// other custom properties here
// ...
}
}
Now you can access the property from the context as usual with the useContext
hook:
const { environment } = useContext(); // read the property
Or you can use the useContextProperty
hook to write or read the property with an alternative syntax:
// write the property
useContextProperty("environment", process.env.NODE_ENV);
// read the property
const environment = useContextProperty("environment");
The context extension allows keeping your code safe, as the typescript compiler will type check your code and emit errors in case of type mismatch.
Custom Parser
A parser is typed by HttpCServerCallParser
. It transforms an http request into an HttpCall
. A parser can skip the processing if it doesn’t support the request. The server will try the next parser in the list.
Common scenarios
Usually, a custom parser is added to:
Handle non-json body
When the http request includes a different format than json, like yaml, binary, or a different media-type like browser form submission or file uploads.
Map a path to a call path
When the server must handle callbacks or webhook calls and you want to map the http request path to a specific function. Or, when the request path contains a parameter you want to extract.
Handle query string
Sometimes extra parameters can be added via query-string by other clients. You want to extract them and use them as function parameters or inject them into the context.
Parsers are defined by a factory with the following structure:
import { HttpCServerCallParser } from "@httpc/server";
export function CustomParser() : HttpCServerCallParser {
return async req => {
if (!canIParseThis(req)) {
return; // skip to the next parser
}
const call: HttpCall = /* extract the httpc call */
return call;
};
}
To use the parser, just pass it in the server options:
import { createHttpCServer } from "@httpc/server";
import { CustomParser } from "./parsers";
const server = createHttpCServer({
parsers: [
CustomParser()
]
});
server.start();
A Parser can accept options to define its behavior.
To define a parser with options, just add the options
argument with the relevant typings:
import { HttpCServerCallParser } from "@httpc/server";
export type CustomParserOptions = {
// option properties
}
export function CustomParser(options: CustomParserOptions) : HttpCServerCallParser {
return async req => {
const { } = options; // <-- read the options
// parser code
};
}
And, finally, to use the parser with the options:
import { createHttpCServer } from "@httpc/server";
import { CustomParser } from "./parsers";
const server = createHttpCServer({
parsers: [
CustomParser({
// set parser options here
})
]
});
server.start();
Parser helper
To assist writing custom parsers, the @httpc/server provides the Parser
helper. The Parser
helper contains utilities for common scenarios like reading the request body or preprocessing the request headers.
import { Parser } from "@httpc/server";
const body = await Parser.readBodyAsString(req);
Example: YAML parser
In a scenario where a client can upload yaml documents, you can easily add a parser that handle the yaml format.
import { HttpCServerCallParser, Parser } from "@httpc/server";
import yaml from "yaml";
export type YamlParserOptions = {
maxBodySize?: number
}
export function YamlParser(options?: YamlParserOptions): HttpCServerCallParser {
return async req => {
if (req.method !== "POST") return;
if (req.headers["content-type"] !== "application/yaml") return;
const url = new URL(req.url, `http://${req.headers.host}`);
const yamlString = await Parser.readBodyAsString(req, options?.maxBodySize);
return {
access: "write",
path: url.pathname,
params: [
yaml(yamlString)
]
};
};
}
And add it to the server:
import { createHttpCServer } from "@httpc/server";
import { YamlParser } from "./parsers";
const server = createHttpCServer({
parsers: [
YamlParser({ maxBodySize: 5 * 1024 }) // max 5Mb body
]
});
server.start();
Additional examples are available in the tutorials.
Custom Processor
A call processor is typed by HttpCServerCallRewriter
.
Usually a call rewriter is defined by a factory:
import { HttpCServerCallRewriter } from "@httpc/server";
export function CustomRewriter(): HttpCServerCallRewriter {
return async call => {
// rewrite call...
// ...and return it
return call;
};
}
And add it to the server:
import { createHttpCServer } from "@httpc/server";
import { CustomRewriter } from "./rewriters";
const server = createHttpCServer({
rewriters: [
CustomRewriter()
]
});
server.start();
Custom Middleware
A middleware is typed by HttpCServerMiddleware
.
Usually a middleware is defined by a factory:
import { HttpCServerMiddleware } from "@httpc/server";
export function CustomMiddleware(): HttpCServerMiddleware {
return async (call, next) => {
// middleware logic
// with a call to the next middleware somewhere
return await next(call);
};
}
And add it to the server:
import { createHttpCServer } from "@httpc/server";
import { CustomMiddleware } from "./middlewares";
const server = createHttpCServer({
middlewares: [
CustomMiddleware()
]
});
server.start();
Custom Renderer
A renderer is typed by HttpCServerRenderer
.
Usually a call rewriter is defined by a factory:
import { HttpCServerRenderer } from "@httpc/server";
export function CustomRender(): HttpCServerRenderer {
return async result => {
if (!canIRenderThis(result)) {
return; // skip to the next renderer
}
// render to a `HttpCServerResponse`
const response: HttpCServerResponse = ...
return response;
};
}
And add it to the server:
import { createHttpCServer } from "@httpc/server";
import { CustomRender } from "./renderers";
const server = createHttpCServer({
rewriters: [
CustomRender()
]
});
server.start();