DEV Community

Cover image for Farrow: A new web framework in the new year
Jade Gu
Jade Gu

Posted on

Farrow: A new web framework in the new year

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.

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.

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.

a tweet of Oliver Ash

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:

some common errors

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.

Alt Text

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.

Alt Text

If an incorrect value is returned, it will look like the following.

Alt Text

The response to the client must be made by means of Response.{method}().

Alt Text

Response's API is designed to support Method Chaining, which can be called as follows.

Alt Text

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.

Alt Text

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.

Alt Text

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.

Alt Text

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.

Alt Text

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.

Alt Text

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.

Alt Text

  • When <key:type> appears before ? is treated as part of params. The order is sensitive.

  • When <key:type> appears after ? appears after it, it is treated as part of the query, 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.

Alt Text

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.

Alt Text

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.

Alt Text

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 and farrow-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 and farrow-graphql-client, similar to farrow-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)

Collapse
 
forsigner profile image
forsigner

Forrow is cool~