Middlewares

It is a common requirement to encounter the necessity of having various operations needed around incoming HTTP requests to your server. In Marble.js middlewares are streams of side-effects that can be composed and plugged-in to our request lifecycle to perform certain actions before reaching the designated Effect.

Building your own middleware

Because everything here is a stream, also plugged-in middlewares are based on similar Effect interface. By default framework comes with composable middlewares like: logging, request body parsing or request validator. Below you can see how simple can look the dummy HTTP request logging middleware.

const dummyLogger$: Effect<HttpRequest> = (req$, res) =>
  req$.pipe(
    tap(req => console.log(`${req.method} ${req.url}`)),
  );

There are two important differences compared to API Effects:

  • stream handler can take a response object as a second argument,

  • middleware must return a stream of requests at the end of the pipeline.

If you prefer shorter type definitions, you can use a type alias which is a shorthand for Effect<HttpRequest> generic type:

const dummyLogger$: Middleware = (req$, res) =>
  req$.pipe(
    // ...
  );

In the example above we get the stream of requests, tap console.log side effect and return the same stream as a response of our middleware pipeline. At the end, all we need to do is to attach the defined middleware to httpListener config.

const middlewares = [
  // 👇 our custom middleware 
  dummyLogger$,
];

const app = httpListener({ middlewares, effects });

Parametrized middleware

There are some cases when our custom middleware needs to be parametrized - for example dummy logger$ middleware should console.log request URL's conditionally. To achieve this we need to curry our middleware by creating some kind of middleware factory, where the last returned function should conform toEffect<HttpRequst> generic interface.

interface LoggerOptions {
  showUrl?: boolean;
}

const dummyLogger$ = (opts: LoggerOptions = {}): Middleware => req$ =>
  req$.pipe(
    tap(req => console.log(`${req.method} ${opts.showUrl ? req.url : ''}`)),
  );

Which can be composed like in the following example:

const middlewares = [
  // 👇 our custom middleware
  dummyLogger$({ showUrl: true }),
];

Sending a response earlier

Some types of middlewares need to send a HTTP response earlier. In this case Marble.js exposes a dedicated req.send method which allows to send a HTTP response using the same common interface that we use for sending a response inside API Effects. The method returns an empty Observable (Observable that immediately completes) as a result, so it can be composed easily inside middleware pipeline.

const middleware$: Middleware = (req$, res) =>
  req$.pipe(
    switchMap(() => res.send({ body: 💩, status: 304, headers: /* ... */ }),
  );

Middlewares composition

In Marble.js you can compose middlewares in three ways:

  • globally (inside httpListener configuration object),

  • inside grouped effects (via combineRoutes function),

  • or by composing it directly inside Effect request pipeline.

via Effect

There are many case why we would like to apply middlewares inside our API Effects. One of them is to authorize only specific endpoints. Going to meet the requirements Marble.js allows to do this using dedicated use operator responsible for composing middlewares directly inside request stream pipeline.

Lets say we have an endpoint for getting list of all users registered in the system, but we would like to make it secure, and available only for authorized users. All we need to do is to compose authorization middleware using dedicated for this case use operator which takes as an argument our middleware.

getUsers.effect.ts
import { authorize$ } from 'auth.middleware`;

const getUsers$: EffectFactory
  .matchPath('/')
  .matchType('GET')
  .use(req$ => req$.pipe(
    // 👇 middleware composition 
    use(authorize$),
    // ...
  );

Where example implementation of authorize$ middleware can look like in the following snippet:

auth.middleware.ts
const authorize$: Middleware = req$ =>
  req$.pipe(
    switchMap(req => iif(
      () => !isAuthorized(req),
      throwError(new HttpError('Unauthorized', HttpStatus.UNAUTHORIZED)),
      of(req),
    )),
  );

As you probably noticed auth.middleware introduces a sample use case of error handling. You can read more about it in dedicated Error handling chapter.

via combineRoutes

There are some cases when there is a need to compose a bunch of middlewares before grouped routes, eg. to authorize only a selecteted group of endpoints. Instead of composing middlewares for each route separately, using use operator, you can also compose them via extended second parameter incombineRoutes() function:

const api$ = combineRoutes('api/v1', {
  middlewares: [ authorize$ ],
  effects: [ user$ ],
});

via httpListener

If your the middleware should operate globally, eg. in case of request logging, then the best place to compose it inside httpListener. In this case the middleware will operate for each request that will go through your HTTP server.

const middlewares = [
  logger$,
  bodyParser$,
];

const effects = [...];

export const app = httpListener({ middlewares, effects });

The order of placing middlewares inside httpListener and combineRoutes matters, because the middlewares are run sequentially (one after another).

Available middlewares

You can check all available middlewares here:

Last updated