farrow-http
A Type-Friendly Web Framework.
Installation
- npm
- Yarn
npm install farrow-http
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 asparams[key] = type
. the order is matter - If it was placed in
querystring
(after?
in a url), it will regard asquery[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
-> tsstring
number
-> tsnumber
boolean
-> tsboolean
id
-> tsstring
, butfarrow-schema
will ensure it's not emptyint
-> tsnumber
, butfarrow-schema
will ensure it's integerfloat
-> tsnumber
{*+}
-> use the string wrapped by{}
asstring literal type
. eg.{abc}
has type"abc"
, onlystring literal type
is supported|
-> tsunion types
. eg.<a:int|boolean|string>
has ts typenumber|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.
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
.
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;
};
tls?: HttpsOptions
Intersection between options from tls.createServer() and tls.createSecureContext() in Node.js.
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
- farrow-pipeline: Type-Friendly middleware library.
- farrow-cors: Cross-Origin Resource Sharing(CORS) for farrow-http.
- farrow-express: Adapter for
farrow-http
in Express app. - farrow-koa: Adapter for
farrow-http
in Koa app. - farrow-react: React adapter for farrow-http.
- farrow-vite: Vite adapter for farrow-http.