@prostory/mountain

Yet another HTTP/2 server and client.


Keywords
Server, Client, HTTP/2, nodejs, http2
License
MIT
Install
npm install @prostory/mountain@1.0.0-alpha.3

Documentation

Mountain ⛰️ - HTTP/2-ready server and client

This library is written and designed as set of ES modules.

Intentions

Why not? 🙃

Prerequisites

HTTP/2 was introduced in NodeJS 8.5.0.

In order to use this library, you should have Node version at least 12.17.0 and above.

Get started

It is wrapper under HTTP/2 module of NodeJS.

HTTP/2 (originally named HTTP/2.0) is a major revision of the HTTP network protocol used by the World Wide Web. It was derived from the earlier experimental SPDY protocol, originally developed by Google. HTTP/2 was developed by the Hypertext Transfer Protocol working group httpbis (where bis means "second") of the Internet Engineering Task Force. The HTTP/2 specification was published as RFC 7540 in May 2015.

The standardization effort was supported by Chrome, Opera, Firefox, Internet Explorer 11, Safari, Amazon Silk, and Edge browsers. Most major browsers had added HTTP/2 support by the end of 2015. And NodeJS did.

API

Server

As browsers support only encrypted HTTP/2 connection and this is desirable for all clients, so only secure server can be created. For this you must provide key and certificate.

import { readFileSync } from 'fs';

import { server } from '@prostory/mountain';

const serverInstance = server({
  key: readFileSync('path/to/key.pem'),
  cert: readFileSync('path/to/cert.pem'),
});

Server instance has such public interface:

interface Server {
  /** Adds listeners to stream's events. */
  on: <T extends keyof Http2ServerEventMap>(
    event: T,
    listener: Http2ServerEventMap[T]
  ) => Server;
  /** Adds routes to server.  */
  use: (...routes: ReadonlyArray<Route>) => Server;
  /** Stops the server from establishing new sessions */
  close: (callback?: (error?: Error) => void) => Server;
  /** Starts the server listening to requests. */
  listen: (port?: number, host?: string, listerner?: VoidFunction) => Server;
}

To let server instance handle requests you should provide routes with use method.

serverInstance.use(
  route1,
  route2,
  route3
  // and so on`
);

Route has such interface:

interface Route {
  readonly path: string;
  readonly method: string;
  handle: RequestHandler;
}

where RequestHandler is a function 👇

type RequestHandler = (request: Request) => void;

Where Request is an object with a few properties:

interface Context {
  readonly flags: number;
  readonly stream: Http2Stream;
  readonly headers: IncomingHttpHeaders & IncomingHttpStatusHeader;
}

interface Request extends Context {
  readonly stream: ServerHttp2Stream;
  /** Holds values of capturing groups of path. */
  readonly parameters: ReadonlyArray<string>;
}

To create a route use a route function.

import { constants } from 'http2';

import { route } from '@prostory/mountain';

const mainRoute = route(constants.HTTP2_METHOD_GET, '/', (request) => {
  /* ... */
});

There is a bunch of predefined route functions for most popular methods: get, put, post, head, del(delete) and options.

const updateRoute = put('/put', (request) => {
  /* ... */
});

path parameter is converted to RegExp to match against path of incoming requests, so for declaring variable parts of URL use valid RegExp syntax.

get('/article/\\d+', (request) => {
  /* ... */
});

And if you want to receive some values from path, then declare capturing groups and its value will be in parameters property of Request object.

get('/article/(\\d+)', (request) => {
  // accessRequest is a helper that simplify getting headers and body from
  // request through its methods.
  const [id] = accessRequest(request).parameters; // or just request.parameters
  /* ... */
});

accessRequest function returns RequestAccessor:

interface Accessor {
  body: GetBodyFunction;
  header: GetHeaderFunction;
}

interface RequestAccessor extends Accessor {
  readonly url: URL;
  readonly method: string;
  readonly parameters: ReadonlyArray<string>;
}

To start listening to incoming requests call listen function.

// By default it will start server on localhost:3333
serverInstance.listen();

If you have many routes with same prefix, you can use group function to gather such routes.

import { group } from '@prostory/mountain';

const testGroup = group('/test');

// It will attach prefix `/test` to each route.
const routes = testGroup(
  route1,
  route2,
  route3
  // and so on
);

There is a predefined route creator for handling static assets - files function. It takes name of the directory relative to current working directory into which server should search for files.

// Now all requests to static assets will be catched and handled.
serverInstance.use(files());

To respond to incoming request use responseFor function:

function responseFor(request: Request): ResponseBuilder;

It creates ResponseBuilder that allows you simply creating a response.

interface ResponseBuilder {
  /** Sends response to client. */
  end: () => void;
  /** Define chunks of body to be sent to client. */
  body: (chunk: string) => ResponseBuilder;
  /** Sends JSON to client. */
  json: (payload: object) => void;
  /**
   * Sends file to client. _path_ should be an
   * absolute path to file.
   */
  file: (path: string) => void;
  /** Define a header for response. */
  header: (name: string, value: string) => ResponseBuilder;
}

To send response to client you should always call end method at the end. Otherwise, client will not receive any response. json and file automatically close response.

import { constants } from 'http2';

// ...

responseFor(request)
  // By default, status is **200**, if you do not provide any.
  .header(constants.HTTP2_HEADER_STATUS, String(200))
  // Body will be sent to client as stream of chunks,
  // so you can divide you data as much as you want.
  .body('Hello')
  .body('world!')
  .end();

Cookies

For easier creating of cookies there is a cookies object. It has two methods: parse and create:

function parse(data: string): Cookies;

// Can create only one key/value pair per one method application.
function create(key: string, value: string, attributes: Cookies = {}): string;

Cookies is an object with a key/value pairs and cookie attributes.

To add multiple cookies to response just call header method many times with new created cookie.

responseFor(request)
  .header('set-cookie', create(pid, 'asldkfjlsjdflaskjdflkajfd'))
  .header('set-cookie', create(name, 'Ben'))
  .end();

Client

client function is use to create Client:

function client(
  authority: string | URL,
  options?: SecureClientSessionOptions
): Client;

Where Client is:

interface Client {
  readonly closed: boolean;

  body: (chunk: string) => Client;
  header: (name: string, value: string) => Client;

  /** Removes headers and body from previous request. */
  fresh: () => Client;
  /**
   * Makes a request to remote peer on opened connection.
   * By default it performs **GET** request to _path_ URL.
   */
  request: (
    path: string,
    options?: ClientSessionRequestOptions
  ) => Promise<Response>;
  /** Closes connection with remote peer. */
  close: (callback?: VoidFunction) => void;
  on: <T extends keyof ClientHttp2SessionEventMap>(
    event: T,
    listener: ClientHttp2SessionEventMap[T]
  ) => Client;
}

Client establishes connection with remote peer (usually server). The same client can make multiple requests to remote peer. In order to do that after every request you should refresh client.

import { constants } from 'http2';
import { client } from '@prostory/mountain';

const articleClient = client('...');

const result1: Promise<Response> = articleClient
  .header(constants.HTTP2_HEADER_METHOD, constants.HTTP2_METHOD_GET)
  .request('/article/1');

const result2: Promise<Response> = articleClient
  .fresh()
  .header(constants.HTTP2_HEADER_METHOD, constants.HTTP2_METHOD_PUT)
  .body('some text')
  .request('/put/article');

Client receives response from remote peer as an object:

interface Response extends Context {
  readonly stream: ClientHttp2Stream;
}

The same as accessRequest there is accessResponse function that helps to get information from response.

interface Accessor {
  body: GetBodyFunction;
  header: GetHeaderFunction;
}

interface ResponseAccessor extends Accessor {}

function accessResponse(context: Response): ResponseAccessor;

Do not forget to close client at the end of work.

Word from author

Have fun ✌️