Middleware
Middleware was invented to avoid repeating the same functionality. However, typically an app or router will handle middleware, upstream from request handlers. This means it's not always clear what information the route will have access to, and tightly couples our code to the framework.
For example, traditionally middleware will use something like app.use(...)
and in our route handler we might access a user like req.user
. However it's never clear if req.user
exists or if other middleware ends the request before the handler is even reached.
webroute
takes a different approach. Routes build an explicit, type-safe, traceable chain of middleware by relying on standard code instead of app-level orchestration.
Middleware Rules
Notably, webroute
middleware lacks any next()
function which is an otherwise common approach to coordinating middleware. Instead, return values are relied on to determine what side-effects the middleware might have. One of three things can be returned:
Result | |
---|---|
Data | A piece of data, in the form of an object, which will overwrite the existing state |
Response | A Response , indicating to early-return from the current request, given that response. |
Response handler | A response handler which will be called after the handler has executed. |
Returning a primitive or empty value will have no effect - it will be ignored. |
Response handlers are executed in reverse order. This is common behaviour you're likely accustomed to.
Data and State
A common use-case of middleware is adding some information to our request. In other words, we are storing some state about the current request.
Because webroute
prohibits modifying a Request
object, this is impossible. Instead we can just return state updates as an object. This will be accessible with full-type safety in successive middleware and handlers.
Response
Returning a response early is very common flow used for authentication or validation, for example. We can "exit early" by returning a Response
. This will cause webroute
to skip the remaining middleware and handlers.
Any response middleware already initialised will be run. The next section elaborates on this idea.
Response Handler
Our middleware can finally also return a response handler. As the name suggests, this can be used to handle the response after it's been handled by our main handler. Instead of having access to the Request
, our response handlers have access to the Response
as the first parameter.
By using this nested function approach, we can use closures to retain access to any incoming request information or internal state we might want to access on the Response
s journey out.
Doing Multiple at Once
Since we can only return a single value, our three behaviours are mutually exclusive. One cannot update state and add a request handler by returning a value.
While it's not a bad idea to separate the concerns of middleware like this, there is a way to update state without strictly returning it.
Chaining
As we've seen here, middleware is very easy to chain. When combined with composition patterns, this becomes incredibly powerful. Learn more about Composition.
Reasoning
You might be wondering why this strange approach to middleware. On top of being strange, it is also restrictive.
The middleware design here is very intentional. The return-value approach is used as an alternative to passing framework-specific functions to our middleware. Consequently, our middleware here could actually be used wherever we want: it merely returns data or a web-standard Respone
objects or handlers.
To learn more about this philosophy, visit Middleware package docs, for which this approach is derived.