Hatsy

Hatsy

Asynchronous TypeScript-friendly HTTP server for Node.js

NPM Build Status Code Quality Coverage GitHub Project API Documentation

Example

import { escapeHTML } from 'httongue';
import { httpListener, Rendering } from '@hatsy/hatsy';
import { dispatchByName, Routing } from '@hatsy/router';
import { createServer } from 'http';

const server = createServer(
httpListener(
Routing.and(Rendering).for(
dispatchByName({
hello({
route: {
url: { searchParams },
},
renderHtml,
}) {
renderHtml(
`<!DOCTYPE html>
<html lang="en">
<body>Hello, ${escapeHTML(searchParams.get('name') || 'World')}!</body>
</html>
`,
);
},
}),
),
),
);

server.listen(8080);

The server above responds with Hello, your-name-here! HTML at http://localhost:8080/hello?name=your-name-here

Goals

Hatsy is developed with the following goals in mind:

  • Simple API.

    Hatsy is a thin layer atop of Node.js HTTP listener. Everything in Hatsy implemented as a RequestHandler function. There is no API like application, middleware, etc. They all can be boiled down to handlers.

    More than that, Hatsy is not strictly bound to Node.js API. The core functionality works with any type of requests.

  • First-class TypeScript support.

    No need in declaration merging. The request context can be extended in a type-safe manner when needed.

  • Asynchronous processing.

    A RequestHandler can be either synchronous or asynchronous.

Good to have:

  • Existing middleware support. At least at some level.

    Connect-style middleware supported. Not ExpressJS one.

Non-goals:

  • HTTP/2, TLS support.

    The typical place of Node.js-driven HTTP server is behind the forwarding proxy. So, there is no need in these technologies supported at Node.js application level.

    Such support is still possible however. Everything that works with standard HTTP API will work with TLS or HTTP/2 compatibility API. Specific functionality can be added by extending a request context.

HTTP listener

httpListener([config,] handler) function creates a Node.js HTTP listener. It accepts an optional configuration and a RequestHandler to process HTTP requests by.

HTTP processing configuration has the following options:

  • defaultHandler - A request handler to call if other handlers did not respond.

    Issues 404 (Not Found) error by default.

  • errorHandler - A request handler to call when error occurred.

    By default, handles HttpError with corresponding status code and renders error page (either HTML or JSON).

  • logError - Whether to log HTTP processing error.

    true by default.

  • handleBy - Creates actual HTTP request handler.

    This can be used e.g. to set up additional request processing capabilities, such as Logging.

Request Handlers

Everything in Hatsy is implemented as a request handler, which is a function accepting a RequestContext as its only parameter. The latter contains all means necessary for request processing.

The handler can do the following:

  • Respond synchronously. E.g. by utilizing HTTP response from request context.
  • Respond asynchronously by returning a promise.
  • Delegate request processing to another handler by calling next() function from request context.
  • Modify or even extend with additional properties the request context for the next handler.

Request Context

By default, HTTP request processing context contains the following properties:

  • request - Node.js HTTP request.
  • response - Node.js HTTP response.
  • requestAddresses object containing request url and remote ip.
  • next() method the handler can use to delegate to another one.

However, the request handler may require more properties to operate. This is where context extension comes into play:

import { HttpMeans, RequestContext } from '@hatsy/hatsy';

/**
* This is an extended request processing means containing user info.
*
* They are applied by [contentExtender] handler and passed to [greeter] one.
*/
interface UserMeans {
readonly name: string;
}

/**
* Accepts context containing HTTP request processing means (`HttpMeans`).
*
* Delegates to [greeter] handler and extends its context with the necessary means.
*/
async function contextExtender(
// Every context property, including methods, is suitable for destructuring.
{ requestAddresses, next }: RequestContext<HttpMeans>,
): Promise<void> {
// The second parameter contains request context properties that will be added or updated.
// The rest remain unchanged.
await next(greeter, { name: requestAddresses.url.searchParams.get('name') || 'anonymous' });
}

/**
* Accepts extended request processing means. I.e. both HTTP and user info.
*/
function greeter({ name, response }: RequestContext<HttpMeans & UserMeans>): void {
// Respond with HTTP response.
// Hatsy would detect the handler actually responded, because of the `.end()` method call.
// Otherwise, Hatsy may try other handlers to process the same request.
response.end(`Hello, ${name}`);
}

Capabilities

Request handlers are everything needed to process the requests. However, it is quite typical to add more request processing means. The request processing capabilities is a conventional API for the task. They also can be combined to add multiple processing means at a time.

Some capabilities are:

  • Rendering.

    Extends request context with RenderMeans containing renderHtml() and renderJson() methods.

  • FormDecoding.

    Decodes application/x-www-form-urlecoded request. Extends request context with RequestBodyMeans containing body property with request body decoded as URLSearchParams or converted to some other representation.

  • JsonParsing

    Parses application/json request. Extends request context with RequestBodyMeans containing body property with request body optionally converted to other representation.

  • Logging

    Request logging capability. Extends request context with LoggerMeans containing a logger instance for request logging.

  • Routing from @hatsy/router module.

    Initiates routing. Extends request context with RouterMeans containing request route used to dispatch to handler(s) matching it.

See the very first example containing capabilities usage. Here is the explanation:

Routing.and(Rendering) // Combine two capabilities. `Rendering` will be applied after `Routing`.
// More capabilities can be combined by chaining `.and()` calls.
.for(handler); // Apply capabilities to the handler.
// The handler receives a request context extended by both of them.

Dispatchers

Dispatchers are handlers that delegate processing to other handlers depending on request.

The following dispatcher implemented:

Middleware

Connect-style middleware can be utilized by middleware function.

Generated using TypeDoc