Extending functionality

status: working in progress

What and why extend

An httpc server can be extended with the following major components:

Custom Context

The server context can be easily extended with custom properties.


The @httpc/server provides a basic context with the follow builtin properties:

propertytypedescription
requestIdstringrandom uuid auto generated at the beginning
requesthttp.IncomingMessagenode native http request
startedAtnumberunix 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:


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

working in progress

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

working in progress

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

working in progress

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();