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