codamit.dev

Introducing Convoyr, the reactive HTTP extensions for Angular

May 20, 2020

Convoyr began with some discussions (and beers) with @yjaaidi about the Angular community. At this time I was bored at my job and I wanted to push myself more in the open-source. Then we created the JScutlery organization for authoring open-source work and we started to code Convoyr.

Convoyr has been built with one goal in mind: helping you to focus on your apps’ features instead of the transport layer’s boilerplate without any trade-off.

Authentication with Interceptors

Let’s start by a concrete example, if I want to authenticate HTTP requests in Angular I should use Interceptors. Here is an example grabbed from internet :

@Injectable()
export class TokenInterceptor implements HttpInterceptor {
  constructor(private auth: AuthService) {}

  intercept(
    request: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {
    request = request.clone({
      setHeaders: {
        Authorization: `Bearer ${this.auth.getToken()}`,
      },
    });
    return next.handle(request);
  }
}

Then I should provide the TokenInterceptor in the AppModule :

@NgModule({
  providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: TokenInterceptor,
      multi: true,
    },
  ],
})
export class AppModule {}

I wrote this exact same thing many times, what’s wrong with it ?

  • Require verbose boilerplate just for setting a request header.
  • May send API token to malicious remote origins.
  • It’s highly repetitive between apps.

Authentication with Convoyr

Let’s see the same authentication feature using Convoyr :

@NgModule({
  imports: [
    ConvoyrModule.forRoot({
      deps: [AuthService],
      config: (auth: AuthService) => ({
        plugins: [
          createAuthPlugin({
            shouldHandleRequest: matchOrigin('https://my-secure-api.com'),
            token: auth.getToken(),
          }),
        ],
      }),
    }),
  ],
})
export class AppModule {}

There is no provider configuration required and security is improved with the matchOrigin function as the token will never be sent to any other origin except https://my-secure-api.com.

How Convoyr was cooked

TDD and TCR

Convoyr has been built with TDD and TCR in dual programming. I recommend to checking out extreme programming which is a software development methodology without the Agile commercial bullshit. This lightweight methodology can help your team to resolve problems faster, better and stronger.

Monorepos style

Technically Convoyr uses both Nx and Lerna to manage the monorepos. The codebase is splitted in different libraries :

  • The Core which runs declared plugins and only depends on TypeScript and RxJS.
  • The Angular module which makes the glue with the framework.
  • The plugins collection which enhance HTTP capabilities.

Convoyr Schema

Making the Core package agnostic from Angular give us a the possibility to be compatible with other HTTP clients, platforms and environments (e.g. server side).

Plugins

The main building block is the plugin. A plugin is a simple object that lets you intercept network communications and control or transform them easily.

Convoyr comes with its built-in plugins collection to handle some complex and repetitive tasks for you. These plugins extends HTTP capabilities in three different axes : security, performance and resilience.

Package Description
@convoyr/plugin-cache Respond with cache-then-network strategy.
@convoyr/plugin-retry Retry failed requests with exponential backoff.
@convoyr/plugin-auth Handle authentication.

Custom plugin examples

It’s easy to implement its own custom plugin, here are some examples.

Response Logging

The handler is the object where the plugin logic is put.

export const loggerPlugin: ConvoyrPlugin = {
  handler: {
    handle({ request, next }) {
      return next
        .handle({ request })
        .pipe(
          tap((response) =>
            console.log(`${request.method} ${request.url}`, response.body)
          )
        );
    },
  },
};

The next.handle({ request }) function lets you access the response stream and transform it before passing it to the next plugin.

Response notifier using Promises

Convoyr allows you to play with Observables, or Promises, or even synchronous calls.

export function createErrorNotifierPlugin(
  snackbar: MatSnackBar
): ConvoyrPlugin {
  return {
    handler: {
      async handle({ request, next }) {
        try {
          const response = await next.handle({ request }).toPromise();
          return response;
        } catch (errorResponse) {
          snackbar.open(
            `Something wrong happened: ${errorResponse.body.error.message}`
          );
          throw errorResponse;
        }
      },
    },
  };
}

I can use Angular services in plugins :

@NgModule({
  imports: [
    ConvoyrModule.forRoot({
      deps: [MatSnackBar],
      config: (snackbar: MatSnackBar) => ({
        plugins: [createErrorNotifierPlugin(snackbar)],
      }),
    }),
  ],
})
export class AppModule {}

However I recommend you to opt for the Observable approach since it provides a nicer API to manage asynchronous tasks.

Setting custom headers

A plugin can manipulate the request and the response stream as well.

export const addCustomHeaderPlugin: ConvoyrPlugin = {
  shouldHandleRequest: matchOrigin('https://www.codamit.dev'),
  handler: {
    handle({ request, next }) {
      return next.handle({
        request: {
          ...request,
          headers: {
            ...request.headers,
            'x-custom-header': '🚀',
          },
        },
      });
    },
  },
};

Rejecting requests to unknown origins

The shouldHandleRequest function lets you conditionally handle requests :

export const rejectUnknownOriginsPlugin: ConvoyrPlugin = {
  shouldHandleRequest: not(matchOrigin('https://www.codamit.dev')),
  handler: {
    handle({ request, next }) {
      return throwError(`🛑 Requesting invalid origin, url: ${request.url}`);
    },
  },
};

Conditional handling is a part of the core library. It’s really useful to select which request the plugin should handle.

Camelize JSON Body keys

Recursively convert JSON body keys to camelcase using the camelcase-keys library.

export const camelCaseJsonKeysPlugin: ConvoyrPlugin = {
  shouldHandleRequest: and(matchMethod('GET'), matchResponseType('json')),
  handler: {
    handle({ request, next }) {
      return next.handle({ request }).pipe(
        map((response) => ({
          ...response,
          body: camelcaseKeys(response.body),
        }))
      );
    },
  },
};

Securing URLs

It should be nice to use HTTPS if the server doesn’t redirect automatically.

export const secureUrlPlugin: ConvoyrPlugin = {
  shouldHandleRequest: ({ request }) => request.url.startsWith('http:'),
  handler: {
    handle({ request, next }) {
      return next.handle({
        request: {
          ...request,
          url: request.url.replace('http:', 'https:'),
        },
      });
    },
  },
};

Network Profiling

Here is a profiler example for measuring network performance.

export const profilerPlugin: ConvoyrPlugin = {
  handler: {
    handle({ request, next }) {
      const started = Date.now();
      let status: 'ok' | 'failed' | null = null;
      return next.handle({ request }).pipe(
        tap({
          next: (response) => (status = 'ok'),
          error: (error) => (status = 'failed'),
        }),
        finalize(() => {
          const elapsed = Date.now() - started;
          const msg = `${request.method} ${request.url} ${status} in ${elapsed}ms`;
          console.log(msg);
        })
      );
    },
  },
};

Instead of rawly logging the result in the console I can imagine sending those performance measures to a remote server to create rich reports.

Mocking back-end response

Sometimes I don’t have any back-end route implemented yet, it can be useful to mock the response.

const usersMock = [
  { id: 0, fullName: 'Michel Paccard' },
  { id: 1, fullName: 'Jacques Balmat' },
  { id: 2, fullName: 'Edward Whymper' },
];

export const mockUserApiPlugin: ConvoyrPlugin = {
  shouldHandleRequest: and(
    matchOrigin('https://www.codamit.dev'),
    matchPath('/api/users'),
    matchMethod('GET')
  ),
  handler: {
    handle({ request, next }) {
      return next
        .handle({ request })
        .pipe(mapTo(createResponse({ body: usersMock })));
    },
  },
};

Further with Convoyr

There is a demo app workspace in our monorepos which can be used as example to start with Convoyr.

We have many other things to do like adding installation schematics or creating other plugins. Any ideas or contributions are welcome.

Hope you will find it useful! 👋🏻

I'm Edouard Bozon, I play almost everyday with web technologies. I focus my work on building better JavaScript apps and contributing to open source. I'm freelancer, currently working on awesome projects.