Route

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.

()
  .((, {  }) => {
    if () {
      // Return state updates
      return { : true };
    }
 
    // Or, Return a response, completing the request early
    return .({ : "UNAUTHORIZED" }, { : 401 });
  })
  .((, {  }) => {
    (.
isAuthed: boolean
isAuthed
=== true);
});

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:

Data | Response | ((response, ctx) => Response);
Result
DataA piece of data, in the form of an object, which will overwrite the existing state
ResponseA Response, indicating to early-return from the current request, given that response.
Response handlerA 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.

()
	.((, {  }) => {
		return { : { : "webroute" }}
	})
	.((, {}) => {
		..
name: string
name
})

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.

()
	.((, {  }) => {
		const  = ();
		if() return {  }
 
		return .("Unauthorized", { : 401 })
	})
	.(() => {
		.("This will not run, unless I'm logged in")
	})
	.((, {  }) => {
		.("Neither will this.")
 
		// State shape is still preserved
		.
user: {
id: number;
}
user
})

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.

()
	.((, {  }) => {
		const  = .()
 
		return () => {
			const  = new (.)
			.("elapsed", (.() - ).())
 
			return new (., {
				
			})
		}
	})
	.((, {  }) => {
		//...
	})

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 Responses 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.

.use((req, { state }) => {
	state.foo = "bar"
})

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.

On this page