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, the plugged-in middlewares are also based on a 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 a dummy HTTP request logging middleware look.

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

In the example above we get a stream of requests, then tap the console.log side effect and return the same stream as a response from our middleware pipeline. The middleware, compared to the basic effect, must return the request at the end.

If you prefer shorter type definitions, you can use the HttpMiddlewareEffect function interface.

const logger$: HttpMiddlewareEffect = (req$, res) =>
req$.pipe(
// ...
);

In order to use our custom middleware, we need to attach the defined middleware to the httpListener config.

const middlewares = [
// ๐Ÿ‘‡ our custom middleware
logger$,
];
โ€‹
const app = httpListener({ middlewares, effects });

Parameterized middleware

There are some cases when our custom middleware needs to be parameterized - for example, the dummy logger$ middleware should console.log request URL's conditionally. To achieve this behavior we can make our middleware function curried, where the last returned function should conform toHttpMiddlewareEffect interface.

interface LoggerOpts {
showUrl?: boolean;
}
โ€‹
const logger$ = (opts: LoggerOpts = {}): HttpMiddlewareEffect => req$ =>
req$.pipe(
tap(req => console.log(`${req.method} ${opts.showUrl ? req.url : ''}`)),
);

The improved logging middleware, can be composed like in the following example:

const middlewares = [
// ๐Ÿ‘‡ our custom middleware
logger$({ showUrl: true }),
];

Sending a response earlier

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

const middleware$: HttpMiddlewareEffect = (req$, res) =>
req$.pipe(
switchMap(() => res.send({ body: ๐Ÿ’ฉ, status: 304, headers: /* ... */ }),
);

If the HTTP response is sent ealier than inside the target Effect, the execution of all following middlewares and Effects will be skipped.

Middlewares composition

In Marble.js you can compose middlewares in four 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 scenarios where 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 us to compose them using dedicated use operator, 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 { use, r } from '@marblejs/core';
import { authorize$ } from './auth.middleware';
โ€‹
const getUsers$ = r.pipe(
r.matchPath('/'),
r.matchType('GET'),
// ๐Ÿ‘‡ here...
r.use(authorize$),
r.useEffect(req$ => req$.pipe(
// ๐Ÿ‘‡ or here...
use(authorize$),
// ...
)));

Using r.pipe operators, the middlwares can be composed in two ways. The first one doesn't infer the returned HttpRequest type of chained middlewares.

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

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

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

via combineRoutes

There are some cases where you have to compose a bunch of middlewares before grouped routes, e.g. to authorize only a selected group of endpoints. Instead of composing middlewares for each route separately, using use operator, you can also compose them via the extended second parameter incombineRoutes() function.

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

via httpListener

If your middleware should operate globally, e.g. in case of request logging, the best place is to compose it inside httpListener. In this case the middleware will operate on each request that goes through your HTTP server.

const middlewares = [
logger$(),
bodyParser$(),
];
โ€‹
const effects = [
// ...
];
โ€‹
export default httpListener({ middlewares, effects });

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