Skip to main content

farrow-http

A Type-Friendly Web Framework.

Installation

yarn add farrow-http

Usage

import { Http, Response } from "farrow-http";

const http = Http();

// add http middleware
http.use(() => {
// returning response in middleware
return Response.text(`Hello Farrow`);
});

http.listen(3000);

API

Router

Router factory for farrow-http. It extends from Pipeline in farrow-pipeline. So there are all api in Pipeline.

Type Signature:

type RouterPipeline = Pipeline<RequestInfo, MaybeAsyncResponse> & {
capture: <T extends keyof BodyMap>(
type: T,
callback: (body: BodyMap[T]) => MaybeAsyncResponse
) => void;
route: (name: string) => Pipeline<RequestInfo, MaybeAsyncResponse>;
serve: (name: string, dirname: string) => void;
match: <T extends RouterRequestSchema>(
schema: T,
options?: MatchOptions
) => Pipeline<TypeOfRequestSchema<T>, MaybeAsyncResponse>;
};

Example Usage:

import { Router } from 'farrow-http`

const router = Router()

router.match

Match specific request via router-request-schema and return a schema-pipeline which can handle the matched request info.

Type Signature:

match: <T extends RouterRequestSchema>(
schema: T,
options?: MatchOptions,
) => Pipeline<TypeOfRequestSchema<T>, MaybeAsyncResponse>
}

type RouterRequestSchema = {
// match pathname of req via https://github.com/pillarjs/path-to-regexp
pathname: Pathname
// match method of req.method, default is GET, supports multiple methods as array
method?: string | string[]
// match the params parsed by path-to-regexp
params?: RouterSchemaDescriptor
// match the req query
query?: RouterSchemaDescriptor
// match the req body
body?: Schema.FieldDescriptor | Schema.FieldDescriptors
// match the req headers
headers?: RouterSchemaDescriptor
// match the req cookies
cookies?: RouterSchemaDescriptor
}

type MatchOptions = {
// if true, it will throw error when there are no middlewares handle the request, or it will calling next()
block?: boolean
// if given, it will be called when Router-Request-Schema was failed, if it returned Response in sync or async way, that would be the final response of middleware
onSchemaError?(error: ValidationError): Response | void | Promise<Response | void>
}
info

learn more about Schema Builder from farrow-schema.

Example Usage:

router
.match({
pathname: "/product/:id",
method: "POST",
params: {
id: Number,
},
query: {
a: Number,
b: String,
c: Boolean,
},
body: {
a: Number,
b: String,
c: Boolean,
},
headers: {
a: Number,
b: String,
c: Boolean,
},
cookies: {
a: Number,
b: String,
c: Boolean,
},
})
.use(async (request) => {
console.log("request", request);
});
Dynamic parameter

A dynamic parameter has the form <key:type>.

  • If it was placed in pathname(before ? in a url), it will regard as params[key] = type. the order is matter
  • If it was placed in querystring(after ? in a url), it will regard as query[key] = type. the order is't matter

Dynamic parameter support modifier(learn more from here), has the form:

  • <key?:type> means optional, the corresponding type is { key?: type }, the corresponding pattern is /:key?
  • <key*:type> means zero or more, the corresponding type is { key?: type[] }, the corresponding pattern is /:key*
  • <key+:type> means one or more, the corresponding type is { key: type[] }, the corresponding pattern is /:key+
Static parameter

A static parameter can only be placed in querystring, it will regard as literal string type.

For example: /?<a:int>&b=2 has the type { pathname: string, query: { a: number, b: '2' } }

Current supported types in router-url-schema

The supported types in <key:type> are list below:

  • string -> ts string
  • number -> ts number
  • boolean -> ts boolean
  • id -> ts string, but farrow-schema will ensure it's not empty
  • int -> ts number, but farrow-schema will ensure it's integer
  • float -> ts number
  • {*+} -> use the string wrapped by {} as string literal type. eg. {abc} has type "abc", only string literal type is supported
  • | -> ts union types. eg. <a:int|boolean|string> has ts type number|boolean|string

RESTful Method

router[get|post|put|patch|head|delte|options], routing methods.

Type Signature:

type RoutingMethod = <U extends string, T extends RouterSharedSchema>(
path: U,
schema?: T,
options?: MatchOptions
) => Pipeline<
MarkReadOnlyDeep<
TypeOfUrlSchema<
{
url: U;
method: string;
} & (RouterSharedSchema extends T ? {} : T)
>
>,
MaybeAsyncResponse
>;

Example Usage:

http.get("/get0/<arg0:int>?<arg1:int>").use((request) => {
return Response.json({
type: "get",
request,
});
});

// With Schema
http
.post("/get0", {
body: {
arg0: Int,
arg1: Int,
},
})
.use((request) => {
return Response.json({
type: "post",
request,
});
});

// With options
http
.post(
"/get0",
{
body: {
arg0: Int,
arg1: Int,
},
},
{ block: true }
)
.use((request) => {
return Response.json({
type: "post",
request,
});
});

Options:

  • block?: boolean

    If block the request and throw Unhandled error when the request does not match any middleware.

  • onSchemaError

    Calling when a request does not match the schema.

Router-Url-Schema

Since farrow v1.2.0, a new feature router-url-schema is supported. it combines { pathname, params, query } into { url }, and use Template literal types to extract the type info.

router.capture

Capture the response body if the specific type is matched, should returning response in callback function.

Type Signature:

capture: <T extends keyof BodyMap>(type: T, callback: (body: BodyMap[T]) => MaybeAsyncResponse) => void

Example Usage:

router.route

Add sub route and return a route-pipeline which can handle the matched request info.

Type Signature:

route: (name: string) => Pipeline<RequestInfo, MaybeAsyncResponse>

Example Usage:

const foo = Router();
const bar = Router();

foo.route("bar").use(bar);

router.serve

Serve static assets.

Type Signature:

serve: (name: string, dirname: string) => void

Example Usage:

router.serve("/static", dirname);

Response

Response can be used to describe the shape of the real server response, farrow-http will perform it later.

Type Signature:

type ResponseInfo = {
status?: Status;
headers?: Headers;
cookies?: Cookies;
body?: Body;
vary?: string[];
};
type Response = {
info: ResponseInfo;
merge: (...responsers: Response[]) => Response;
is: (...types: string[]) => string | false;
string: ToResponse<typeof string>;
json: ToResponse<typeof json>;
html: ToResponse<typeof html>;
text: ToResponse<typeof text>;
redirect: ToResponse<typeof redirect>;
stream: ToResponse<typeof stream>;
file: ToResponse<typeof file>;
vary: ToResponse<typeof vary>;
cookie: ToResponse<typeof cookie>;
cookies: ToResponse<typeof cookies>;
header: ToResponse<typeof header>;
headers: ToResponse<typeof headers>;
status: ToResponse<typeof status>;
buffer: ToResponse<typeof buffer>;
empty: ToResponse<typeof empty>;
attachment: ToResponse<typeof attachment>;
custom: ToResponse<typeof custom>;
type: ToResponse<typeof type>;
};

Example Usage:

Response.text(`Hello Farrow`);

// Use in http
http.use(() => {
// returning response in middleware
return Response.text(`Hello Farrow`);
});

Response.info

Response info.

Type Signature:

info: ResponseInfo;

Example Usage:

const headers = Response.info.headers;

Response.merge

Merge all responses.

Type Signature:

merge: (...responses: Response[]) => Response;

Example Usage:


Response.is

Check response content type. response.is('json') => 'json' | false.

Implement by jshttp/type-is.

Type Signature:

is: (...types: string[]) => string | false;

Example Usage:

const response = Response.string("farrow");

response.is("string"); // 'string'
response.is("json"); // false

Response.string

Set string response body.

Type Signature:

string: (value: string) => Response;

Example Usage:

Response.string("farrow");

Response.json

Set json response body.

Type Signature:

json: (value: JsonType) => Response;

Example Usage:

Response.json({ name: "farrow" });

Response.html

Set html response body.

Type Signature:

html: (value: string) => Response;

Example Usage:

Response.html("<html><head><title>Farrow</title></head></html>");

Response.text

Set text response body.

Type Signature:

text: (value: string) => Response;

Example Usage:

Response.text("farrow");

Response.redirect

Redirect response.

Type Signature:

redirect: (url: string, options?: { usePrefix?: boolean }) => Response;

Example Usage:

Response.redirect("/toFoo");

// With options
// input url is `/basename/tofoo`, will redirect to `/basename/tobar`
Response.redirect("/tobar", { usePrefix: true });

Options

  • usePrefix?: boolean If rediect with the prefix of url of current request

Response.stream

Set stream response body.

Type Signature:

stream: (stream: Stream) => Response;

Example Usage:

import { Writable } from "stream";

const myStream = new Writable();

Response.stream(myStream);

myStream.write("some data");

Response.file

Set file response body.

Type Signature:

file: (filename: string) => Response;

Example Usage:

Response.file("/pathtofile");

Response.vary

Set vary header fields.

Implement by jshttp/vary.

Type Signature:

vary: (...fileds: string[]) => Response;

Example Usage:

Response.vary("Origin", "User-Agent");

Response.cookie

Set response cookie.

Implement by pillarjs/cookies.

Type Signature:

cookie: (
name: string,
value: string | number | null,
options?: Cookies.SetOption
) => Response;

Example Usage:

Response.cookie("sessionid", "pimyqmcka_f4e");

Response.cookies

Set response cookies.

Implement by pillarjs/cookies.

Type Signature:

cookies: (
cookies: { [key: string]: string | number | null },
options?: Cookies.SetOption
) => Response;

Example Usage:

Response.cookie({ sessionid: "pimyqmcka_f4e" });

Response.header

Set response header.

Implement by [res.setHeader](https://nodejs.org/dist/latest-v17.x/docs/api/http.html#responsesetheadername-value).

Type Signature:

header: (name: string, value: Value) => Response;

Example Usage:

Response.header("Content-Type", "text/html");

Response.headers

Set response headers.

Type Signature:

headers: (headers: Headers) => Response;

Example Usage:

Response.header({ "Content-Type", "text/html" });

Response.status

Set response status.

Type Signature:

status: (code: number, message?: string) => Response;

Example Usage:

Response.header(200);
Response.header(404, "Not found");

Response.buffer

Set buffer response body.

Buffer

Type Signature:

buffer: (buffer: Buffer) => Response;

Example Usage:

import { Buffer } from "buffer";

const buffer = Buffer.from([1, 2, 3]);

Response.buffer(buffer);

Response.empty

Set empty content response body.

Type Signature:

empty: () => Response;

Example Usage:

Response.empty();

Response.attachment

Set attachment response header. It is different from Response.file.

Content-Disposition

Type Signature:

attachment: (filename?: string) => Response;

Example Usage:

Response.file("/attachment");

Response.custom

Do nothing when reture this response object but you did in custom handler.

Type Signature:

custom: (handler?: CustomBodyHandler) => Response;

Example Usage:

Response.custom(({ res }) => {
res.end("farrow");
});

Response.type

Set content-type via mime-type/extname.

Type Signature:

type: (type: string) => Response;

Example Usage:

Response.type("json");

Http

Create a http server.

It extends from Router. So there are all api in [Router](#router).

Type Signature:

createHttpPipeline: (options?: HttpPipelineOptions | undefined) => HttpPipeline;

type HttpPipeline = RouterPipeline & {
handle: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
listen: (...args: Parameters<Server["listen"]>) => Server;
server: () => Server;
};

Example Usage:

import { Http, Response } from "farrow-http";

const http = Http();

// add http middleware
http.use(() => {
// returning response in middleware
return Response.text(`Hello Farrow`);
});

http.listen(3000);

Options

type HttpPipelineOptions = {
//
basenames?: string[];
// options for parsing req body, learn more: https://github.com/cojs/co-body#options
body?: BodyOptions;
// options for parsing req cookies, learn more: https://github.com/jshttp/cookie#options
cookie?: CookieOptions;
// options for parsing req query, learn more: https://github.com/ljharb/qs
query?: QueryOptions;
// injecting contexts per request
contexts?: (params: {
req: IncomingMessage;
requestInfo: RequestInfo;
basename: string;
}) => ContextStorage | Promise<ContextStorage>;
// enable log or not
logger?: boolean | HttpLoggerOptions;
};

type LoggerOptions = {
// handle logger result string
transporter?: (str: string) => void;
};
  • basenames?: string[]

    Basename list, farrow-http will cut the basename from request.pathname

  • body?: BodyOptions

    Options for parsing req body, learn more: https://github.com/cojs/co-body#options

  • cookie?: CookieOptions

    Options for parsing req cookies, learn more: https://github.com/jshttp/cookie#options

  • query?: QueryOptions

    options for parsing req query, learn more: https://github.com/ljharb/qs

  • contexts?: (params: { req: IncomingMessage, requestInfo: RequestInfo, basename: string}) => ContextStorage | Promise<ContextStorage>

    Injecting contexts per request.

  • logger?: boolean | HttpLoggerOptions

    Enable log or not

http.handle

Handle request and respond to user, you can use this function to make farrow-http work in expressjs/koajs or other web framework in Node.js.

Type Signature:

handle: (req: IncomingMessage, res: ServerResponse) => Promise<void>

Example Usage:

import { createServer } from "http";
import { Http } from "farrow-http";
const http = Http();

const server = createServer(http.handle);

http.listen

The same args of http.createServer().listen(...), create a server and listen to port.

Type Signature:

listen: (...args: Parameters<Server["listen"]>) => Server;

Example Usage:

http.listen(3000, () => {
console.log("Server started at 3000.");
});

http.server

Just create a server with http.handle(req, res), don't listen to any port.

Type Signature:

server: () => Server;

Example Usage:

const server = http.server();

Https

Create a https server.

It extends from Http. So there are all api in [Http](#http).

Type Signature:

Https: (options?: HttpsPipelineOptions | undefined) => HttpsPipeline;

Example Usage:

import { Https, Response } from "farrow-http";

const CERT = fs.readFileSync(path.join(__dirname, "./keys/https-cert.pem"));
const KEY = fs.readFileSync(path.join(__dirname, "./keys/https-key.pem"));
const CA = fs.readFileSync(path.join(__dirname, "keys/https-csr.pem"));

const https = Https({
tls: {
cert: CERT,
ca: CA,
key: KEY,
},
});

// add http middleware
https.use(() => {
// returning response in middleware
return Response.text(`Hello Farrow`);
});

https.listen(3000);

Options

type HttpsPipelineOptions = HttpPipelineOptions & {
tls?: HttpsOptions;
};

hooks

useReq

Type Signature:

useReq(): IncomingMessage

Example Usage:

http.use(() => {
// original request
let req = useReq();
});

useRes

Type Signature:

useRes(): ServerResponse

Example Usage:

http.use(() => {
// original response
let res = useRes();
});

useRequestInfo

Type Signature:

useRequestInfo(): RequestInfo

Example Usage:

http.use((request0) => {
// request1 in here is equal to request0, but we can calling useRequestInfo in any custom hooks
let request1 = useRequestInfo();
});

useBasenames

Type Signature:

useBasenames(): string[]

Example Usage:

const http = Http({
basenames: ["/base0"],
});
http.route("/base1").use(() => {
// basenames will be ['/base0', '/base1'] if user accessed /base0/base1
let basenames = useBasenames().value;
return Response.json({ basenames });
});

usePrefix

Type Signature:

usePrefix(): string

Example Usage:

const http = Http({
basenames: ["/base0"],
});

http.route("/base1").use(() => {
// prefix will be '/base0/base1' if user accessed /base0/base1
let prefix = usePrefix();
return Response.json({ prefix });
});

Learn more

Relative Module