1
\$\begingroup\$

I have built a Express.js proxy server using TypeScript to be able to hide my front-end API keys.

I designed it to be able to include multiple API services and to reject connections that are not from allowed domains.

It is working as expected, but I'm new to TypeScript and wanted to get some feedback, also on the overall design of the project.

This is app.ts:

import { Request, Response, NextFunction } from 'express';
import Debug from 'debug';
import dotenv = require('dotenv');
import proxies from './proxies';

dotenv.config();

import express = require('express');

const debug = Debug('express:server');
const app = express();

function notFound(req: Request, res: Response, next: NextFunction) {
  res.status(404);
  const error = new Error('Not Found');
  next(error);
}

function errorHandler(error: Error, req: Request, res: Response) {
  res.status(res.statusCode || 500);
  res.json({
    message: error.message,
  });
}

proxies.forEach((proxy) => app.use(proxy));
app.use(notFound);
app.use(errorHandler);

const port = process.env.PORT || 5000;
app.listen(port, () => debug(`Listening on port ${port}`));

This is proxies.ts which is responsible for creating the proxies using the http-proxy-middleware and the proxies defined on the config.ts file:

import querystring, { ParsedUrlQueryInput } from 'querystring';
import { Request } from 'express';
import { createProxyMiddleware, Options, Filter } from 'http-proxy-middleware';
import config from './config';

const { allowedDomains: globalAllowedDomains = [], proxies } = config;

export default proxies.map(
  ({
    route,
    target,
    allowedDomains = [],
    allowedMethods = ['GET'],
    queryparams = {},
    headers = {},
    auth,
  }) => {
    const filter: Filter = (pathname: string, req: Request) => {
      if (typeof req.headers.origin === 'string') {
        return (
          pathname.startsWith(route) &&
          allowedMethods.includes(req.method) &&
          [...globalAllowedDomains, ...allowedDomains].includes(
            req.headers.origin
          )
        );
      }
      return false;
    };
    const options: Options = {
      target,
      changeOrigin: true,
      headers,
      auth,
      pathRewrite(path: string, req: Request) {
        const qp = querystring.stringify({
          ...req.query,
          ...queryparams,
        } as ParsedUrlQueryInput);
        const newPath = `${path.split('?')[0].replace(route, '')}?${qp}`;
        return newPath;
      },
      logLevel: process.env.NODE_ENV === 'development' ? 'debug' : 'info',
    };

    return createProxyMiddleware(filter, options);
  }
);

And this is config.ts which is a configuration file where I can enable multiple API services:

import { Options } from 'http-proxy-middleware';
import { ParsedUrlQueryInput } from 'querystring';

export interface Proxy extends Options {
  route: string;
  allowedMethods: string[];
  queryparams?: ParsedUrlQueryInput;
  allowedDomains?: string[];
}

export interface Config {
  allowedDomains: string[];
  proxies: Proxy[];
}

const config: Config = {
  allowedDomains:
    process.env.NODE_ENV === 'production'
      ? ['https://www.mauriciorobayo.com']
      : ['http://localhost:8080'],
  proxies: [
    {
      route: '/weather',
      allowedMethods: ['GET'],
      target: 'https://api.openweathermap.org/data/2.5/weather',
      queryparams: {
        appid: process.env.WEATHER_API_KEY,
      },
    },
    {
      route: '/ipinfo',
      allowedMethods: ['GET'],
      target: 'https://ipinfo.io/',
      queryparams: {
        token: process.env.IPINFO_TOKEN,
      },
    },
    {
      route: '/github',
      allowedMethods: ['GET'],
      target: 'https://api.github.com',
      headers: {
        Accept: 'application/vnd.github.v3+json',
        Authorization: `Token ${process.env.GITHUB_TOKEN}`,
      },
    },
  ],
};

export default config;
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.