Webroute

Middleware

Middleware helps with code reuse and orchestrating more sophisticated request pipelines. Webroute offers a simple, powerful and novel approach to middleware to avoid some of the pitfalls of previous approaches.

Overview

Webroute's approach to middleware is highly functional. Instead of middleware imperatively orchestrating the app, it is a pure function which merely accepts inputs and returns outputs. Consequently, many of the common bugs are avoided.

This approach means middleware are always strongly type-safe and connected in-code. Our routes will alway know what middleware runs before it and what state middleware may or may not provide.

Getting Started

To register a middleware function, we use the .use() method. The handler we provide takes the same parameters as a request handler.

route().use((req, c) => {
  console.log("URL:", req.url);
});

Middleware works incredibly well with the composition pattern.

const routeWithMiddleware = route().use((req) => {
  console.log("URL:", req.url);
});
 
const oneRoute = routeWithMiddleware.handle(() => {});
const anotherRoute = routeWithMiddleware.handle(() => {});

Middleware Effects

Most middleware wants to change or orchestrate our app somehow. Because web-standard Requests are immutable, we can't directly modify the Request object. Moreover, webroute doesn't provide any function like next() to orchestrate the app flow. Instead, webroute uses return values to enable these actions.

Updating Request 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. We can return a plain JavaScript object to indicate a state update is in order. Webroute will read the type of the object, which will be reflected in future access of the state property.

()
  .((, {  }) => {
    return { : 3 };
  })
  .((, { 
state: {
    elvenBreadCount: number;
}
state
}) => {
});

State updates from additional middleware will be shallow merged with the request state.

Early Response

Middleware is commonly used to guard subsequent handlers by returning a response early under some condition. For example authentication and authorisation middleware.

If we return a Response from middleware, this will trigger an early response.

()
  .((, {  }) => {
    if (..("x-is-balrog")) {
      return new ("You shall not pass.", { : 401 });
    }
    return { : true };
  })
  .((, {  }) => {
    return "You passed.";
  });

Response Handlers

Another use for middleware is to listen to and potentially alter the Response on its way out. Such response handlers are the third and final variant a middleware can return. Response handlers are functions which accept a Response as the only parameter. They may optionally return an updated Response.

().((, {  }) => {
  const  = .();
 
  return () => {
    .("Took", .() - );
 
    return new (., { : 203 });
  };
});

Request middleware run in order of registration. Response handlers run in the reverse order.

Escape Hatch: Mutating State

Most of the time, middleware will only need to do one of these three things at once. They are almost mutually exclusive options. However, in rare cases we may want to update request state and return early or handle responses.

In this case, we can use the escape hatch of mutating state directly, i.e. without returning. Not only can we mutate state, but we can do so without sacrificing type-safety.

()
  .<{ : boolean }>((, ) => {
    const  = . as any;
    .hasRing = false;
 
    return new ("OK");
  })
  .((, ) => {
    const { 
const hasRing: boolean
hasRing
} = .;
});

Here we provide a generic type parameter indicating how our states type will change.

Defining Middleware

While creating middleware inline is a valid approach, it makes reusing middleware more difficult. Instead, we can define middleware elsewhere.

As we've seen, middleware is inherently basic, so we can define middleware like so.

export const myMiddleware = (req: Request) => {
  // Implement middleware
};

Instead of making our middleware reliant on the context param, we encourage specifically defining only what is needed. This helps with testing and will make your code less fragile.

A Realistic Example

So far the examples have been relatively contrived. Here is a more realistic glimpse at what working with middleware might look like.

const  = (: Request) => {
  return {
    : ..("Authorization")?.("Bearer", "").(),
  };
};
 
const  = (: Request, ?: string) => {
  try {
    const {  } = ();
    return { :  };
  } catch () {
    return new ("Unauthorized", { : 401 });
  }
};
 
const  = ()
  .()
  // Pass data as needed
  .((, ) => (, ..));
 
const  = .("/foo").((, ) => {
  const { 
const userId: string
userId
} = .;
});

On this page