Fast as fuck, easy to use http server written in typescript


Keywords
http, framework, rest, nodejs, typescript, server, json-apis
License
MIT
Install
npm install faf@3.0.1

Documentation

Fast as f**k http framework for building simple JSON APIs.

The idea behind faf is to provide a fetch-like API: A Request comes in, a Response goes out, but the other way around. Instead of calling response.json() you would do request.json(). Checkout the example project here.

Installation

$ npm install faf

// or
$ yarn add faf

Usage

import faf, { Response, HttpStatus } from "faf";

const app = faf();

app.post("/products", async (request) => {
  // You should validate this with your library of choice
  const body = await request.json();
  const product = await prisma.product.create(body);
  return Response.json(product, { status: HttpStatus.Created });
});

const { port } = await app.bind(4000);
console.log(`Listening on port ${port}`);

Dynamic and Query Params

Faf normalizes both query and dynamic params into a single URLSearchParams instance. note: dynamic params override query string params.

// incoming url: /products/5?limit=10
app.get("/products/:id", (request) => {
  const { url } = request;
  console.log(url.pathname); // /products/5
  console.log(url.searchParams); // URLSearchParams { 'id' => '5', 'limit' => '10' }
});

CORS

faf comes with cors support out of the box. It is a fair simple implementation (for now) taken mostly from @koa/cors and expressjs/cors.

const app = faf({ cors: true });

// or
const app = faf({
  cors: { origin: "https://example.com", allowedHeaders: ["x-token"] },
});

API

Application

You can create an application in two ways:

import faf from "faf";

const app = faf();

// or

import { Application } from "faf";

const app = new Application();
class Application {
  on(method: HttpMethod, path: string, requestHandler: RequestHandler): this;

  get(path: string, requestHandler: RequestHandler): this;

  post(path: string, requestHandler: RequestHandler): this;

  put(path: string, requestHandler: RequestHandler): this;

  patch(path: string, requestHandler: RequestHandler): this;

  del(path: string, requestHandler: RequestHandler): this;

  head(path: string, requestHandler: RequestHandler): this;

  all(path: string, requestHandler: RequestHandler): this;

  /**
   * Start the server
   *
   * @param port string | number | undefined
   * @param host string | undefined
   * @returns Promise<AddressInfo>
   */
  bind(port?: string | number, host?: string): Promise<AddressInfo>;

  /**
   * Stop the server
   *
   * @returns Promise<void>
   */
  unbind(): Promise<void>;
}

Request

Request is not intended to be instantiated by you. faf creates one for you once per request (forgive the redundancy).

class Request {
  readonly headers: IncomingHttpHeaders;
  readonly method: HttpMethod;
  readonly url: URL;

  get body(): Readable;

  get bodyUsed(): boolean;

  /**
   * Decode response as Buffer
   *
   * @returns Promise<Buffer>
   */
  buffer(): Promise<Buffer>;

  /**
   * Decode response as ArrayBuffer
   *
   * @returns Promise<ArrayBuffer>
   */
  arrayBuffer(): Promise<ArrayBuffer>;

  /**
   * Decode response as text
   *
   * @returns Promise<string>
   */
  text(): Promise<string>;

  /**
   * Decode response as JSON
   *
   * @returns Promise<unknown>
   */
  json(): Promise<unknown>;

  /**
   * Decode response as URLSearchParams
   *
   * @returns Promise<URLSearchParams>
   */
  form(): Promise<URLSearchParams>;
}

Response

Response is intended to be instantiated using its static methods, dependeing on what you want to respond with.

class Response {
  /**
   * Create a buffer response
   *
   * @param data Buffer
   * @param init ResponseInit
   * @returns Response
   */
  static buffer(data: Buffer, init?: HttpStatus | ResponseInit): Response;

  /**
   * Create a stream response
   *
   * @param readable Stream
   * @param init ResponseInit
   * @returns Response
   */
  static stream(readable: Readable, init?: HttpStatus | ResponseInit): Response;

  /**
   * Create a json response
   *
   * @param data unknown
   * @param init ResponseInit
   * @returns Response
   */
  static json(data: unknown, init?: HttpStatus | ResponseInit): Response;

  /**
   * Create a text response
   *
   * @param str string
   * @param init ResponseInit
   * @returns Response
   */
  static text(str: string, init?: HttpStatus | ResponseInit): Response;

  /**
   * Create an empty response
   *
   * @param status HttpStatus
   * @returns Response
   * @since 2.3.0
   */
  static empty(status?: HttpStatus): Response;
}

raise

raise is used when you want to terminate execution. It throws a special HttpError which faf will intercept and reply accordingly. raise takes an HttpStatus as its first argument, and an optional second argument which can be a JSON serializable object.

import { raise, HttpStatus } from "faf";

app.get("/products/:id", () => {
  raise(HttpStatus.NotFound);
});
function raise(httpStatus: HttpStatus, body?: unknown): never;

HttpStatus

HttpStatus is a type-safe map of all available status codes.

import { HttpStatus } from "faf";

HttpStatus.OK; // => 200
HttpStatus.NotFound; // => 404

// And so on...

Benchmarks

Command

bombardier -c 126 -n 1000000 http://localhost:4000/example

Code

import { Application, Response } from "faf";

const app = new Application();

app.get("/example", () => Response.text("Hello world"));

app.bind(4000);

Output

Statistics        Avg      Stdev        Max
  Reqs/sec     29834.31    3185.56   33827.70
  Latency        4.22ms   579.87us    47.48ms
  HTTP codes:
    1xx - 0, 2xx - 1000000, 3xx - 0, 4xx - 0, 5xx - 0
    others - 0
  Throughput:     6.52MB/s

faf is pretty minimalist, so it is fast. Here's the ouput for the express equivalent.

Statistics        Avg      Stdev        Max
  Reqs/sec      8880.65     839.30    9964.55
  Latency       14.19ms     0.93ms    69.20ms
  HTTP codes:
    1xx - 0, 2xx - 1000000, 3xx - 0, 4xx - 0, 5xx - 0
    others - 0
  Throughput:     2.60MB/s