Today I'd like to introduce you to a new project I've recently developed - Farrow. a type-friendly functional style Node.js web framework.
Motivation
In the current Node.js open source ecosystem, there are already expressjs, koajs, hapi, restify, fastify, nestjs, and perhaps countless other web services frameworks, so do we need another one?
The answer may vary from person to person, so I'd like to share my personal opinion here.
Most of the popular web service frameworks in Node.js were developed with a JavaScript perspective before TypeScript became really popular.
They take full advantage of the expressive power of JavaScript's dynamic typing, no doubt about it.
If we take into account the ability of the Static Type-System to capture as many potential problems as possible in Compile-Time, then redeveloping a Web services framework in TypeScript might be a worthwhile endeavor.
Farrow is one of my outputs in this direction.
Middleware design from a TypeScript perspective
Rich Harris, the author of Rollup and Svelte, recently shared his thoughts on Next-gen Node HTTP APIs, and I was inspired.
It started with a poll tweeted by Wes.
Close to 70% of developers, opted for expressjs style middleware function design. An overwhelming choice.
Rich Harris' choice, with only 14.5% support.
In that Gist, Rich Harris explains why he doesn't like the first option. Roughly, it goes as follows.
- Always need to ugly pass
res
parameters - When combining middleware, you often have to do monkey-patching on
res
He gave what he felt was a better alternative design.
Simply put, the res
parameter is eliminated, only the req
parameter is retained, the response result is expressed by return response, and the next middleware next()
is called by return void/undefined.
Another developer, Oliver Ash, tweeted about one of the shortcomings of expressjs' middleware design - it does not take full advantage of Compile-Time's troubleshooting capabilities.
In brief, when the response is the return value of middleware, TypeScript can type-check that each request must have a return value without fear of omission.
Giulio Canti, the author of fp-ts, also has his own attempt - hyper-ts. inspired by purescript's hyper project, hyper-ts uses TypeScript's Type- System to circumvent some common errors, such as:
These clues all point to the conclusion that it may be possible to design the HTTP middleware API in a functional style (immutable way).
Farrow-Pipeline: Type-friendly middleware function design
Farrow's middleware functions are inspired by Koa middleware, but are different.
From the above figure, we can learn the following information.
Response
is not in the parameters of the middleware function, but from the plain function exported by farrow-http module.response
is the return value of the middleware function, which can be checked in Compile-Time.
If there is no return value, it will look like the following.
If an incorrect value is returned, it will look like the following.
The response to the client must be made by means of Response.{method}()
.
Response's API is designed to support Method Chaining, which can be called as follows.
As above, setting response status, setting response headers, response cookies, and response content can all be written together elegantly.
So, how does multiple middlewares collaborate with each other in Farrow?
For example, in the upstream middleware, pass a new request to the downstream middleware, like the following.
The second parameter of the Farrow middleware function is the next
function. Unlike expressjs/koajs middleware functions, the Farrow middleware function has both parameters and return values.
Its parameter is the optional request
and its return value is response
.
When the next()
call is made without passing parameters, the downstream middleware gets the same request
as the upstream middleware.
If a new request
is passed when next
is called, the downstream middleware will get the new request
object.
With this very natural parameter passing mechanism, we don't need to modify the current request. Even, Farrow sets the request type to read-only.
Farrow encourages keeping the request/response immutable.
Similarly, we can filter or manipulate the response returned by the downstream middleware in the upstream middleware, as follows.
The Response object provides a merge method to easily merge the status, headers, cookies, content, and other components of multiple responses.
Farrow also provides a fractal-enabled Router design that helps us to fine-grained segment business logic into different modules and organically combines them.
Farrow-Schema: Type-Safe Routing Design
Farrow implements a powerful and flexible Schema-Based Validation that can match specific request objects in a type-safe manner.
The basic usage is shown below.
The http.match
method accepts parameters as { pathname, method, query, params, headers, cookies }
objects to form a Request Schema
.
schema.pathname
adopts expressjs-like style via path-to-regexp
.
Farrow will extract the exact type of the matching request object through type infer according to the Request Schema, and validate it in the runtime to ensure the type safety of the request object.
In addition, Farrow also implements type-safe route matching based on the Template Literal Types feature of TypeScript V4.1.
With the format in the path, we can combine { pathname, params, query }, write only one path, and extract the corresponding type from the path by type infer.
A more complex case is shown below.
When
<key:type>
appears before?
is treated as part ofparams
. The order is sensitive.When
<key:type>
appears after?
appears after it, it is treated as part of thequery
, where the order is not sensitive.
To learn more about Farrow's Router-Url-Schema, you can check out its documentation.
Farrow-Hooks mechanism
Another noteworthy feature in Farrow is that we take a cue from React-Hooks and provide a Farrow-Hooks mechanism to integrate with other parts of the server, such as logger, database-connection, etc.
In contrast to koajs, which mounts extended methods with mutable ctx
arguments, in Farrow, the context is not an argument, but a Hooks.
Like React-Hooks useState
, it can be seen as a fine-grained slicing of the this.state
shared in the class-component.
Context.use in Farrow cuts the shared ctx in the same way. This is shown below.
We define a User type, create a Farrow Context in a similar way to React.createContext, and provide the default value (in this case, null).
UserContext.use() is a built-in hook that provides access to the corresponding user context, and all Contexts are new and independent during each request -> response.
Instead of having one big ctx, we have multiple small Context units.
We can wrap custom hooks based on Context.use()
, such as useUser
in the above image.
To dynamically update the value of a Context, do something like the following.
Implement a Provider Middleware that dynamically and asynchronously updates the context value for consumption by the downstream middleware. The relationship is similar to that of Provider and Consumer in React Context. The upstream middleware is the Context Provider and the downstream middleware is the Context Consumer.
With the Context Hooks mechanism, our middleware function type is always simple and stable, it only focuses on request -> response processing, other additional things can be provided through Hooks on demand.
Farrow-React: A built-in component-based SSR
Farrow provides an official SSR library, farrow-react, but you can also build your own SSR library based on methods like Response.html
or Response.stream
.
As you can see above, farrow-react
provides a Farrow-Hooks and through useReactView
we get a ReactView object that renders JSX into HTML and sends it to the browser through farrow-http
.
farrow-react
provides a Link
component to help us handle prefix-related auto-completion. To learn more, check out the official farrow documentation.
Summary
At this point, we have broadly described a few of Farrow's core features.
Farrow's goal doesn't stop there, we will build more farrow ecosystem in the future. For example.
farrow-restapi
andfarrow-restapi-client
support reusing the schema/type of the server project in the client project to achieve type-safe functionality on the server/client side of the data transfer.farrow-graphql
andfarrow-graphql-client
, similar tofarrow-restapi
but with support for implementation via graphql.farrow-server-component
, supports React Server Component.
There is still a lot of work to be done, so if you are also interested, feel free to contribute to the Farrow.
Top comments (1)
Forrow is cool~