Development proxy
Last modified on Thu 12 Jan 2023

TLDR

Follow instructions on @infinum/nextjs-api-dev-proxy. Everything else in this document is legacy and will no longer be updated.

Motivation

When developing applications, you sometimes want to test your local changes against data from a different environment. For example, creating a production hotfix requires you to locally connect to the production API. Maybe even testing the solution on a different device, e.g. a phone connected to your machine. That API could have http-only cookies that cannot be manipulated on the client and require a proxy that can intercept some requests and do some modifications to them.

The issue

NextJS already supports/recommends ways to proxy outgoing requests:

The problem with the former is that it runs on a different server. When testing local changes on e.g. a phone, we will get unresolvable CORS issues. The problem with the latter is that NextJS redirects don't provide a possibility to manipulate requests and/or responses. They just redirect.

The fix

Prerequisites:

// pages/api/[[...slug]].ts
import { IncomingMessage } from 'http';
import { createProxyMiddleware } from 'http-proxy-middleware';
import { NextApiRequest, NextApiResponse } from 'next';

let apiUrl: string;

switch (process.env.PROXY_ENV) {
  // pick API endpoint depending on the PROXY_ENV, assign to apiUrl
  case 'production':
    apiUrl = 'https://production.example.com';
    break;
  case 'uat':
    apiUrl = 'https://uat.example.com';
    break;
  default:
    apiUrl = 'https://staging.example.com';
}

const proxy = createProxyMiddleware({
  target: apiUrl,
  changeOrigin: true,
  logLevel: 'debug',
  cookieDomainRewrite: 'localhost',
  onProxyRes: (proxyRes: IncomingMessage) => {
    // You can manipulate the cookie here

    if (!proxyRes.headers['set-cookie']) {
      return;
    }

    // For example you can remove secure and SameSite security flags so browser can save the cookie in dev env
    const adaptCookiesForLocalhost = proxyRes.headers['set-cookie'].map((cookie) =>
      cookie.replace(/; secure/gi, '').replace(/; SameSite=None/gi, '')
    );

    proxyRes.headers['set-cookie'] = adaptCookiesForLocalhost;
  },
  onError: (err: Error) => console.error(err),
});

export default function handler(req: NextApiRequest, res: NextApiResponse<unknown>) {
  // Don't allow requests to hit the proxy when not in development mode
  // NextJS doesn't allow conditional API routes
  if (process.env.NODE_ENV !== 'development') {
    return res.status(404).json({ message: 'Not found' });
  }

  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
  // @ts-ignore
  return proxy(req, res);
}

export const config = {
  api: {
    bodyParser: false, // enable POST requests
    externalResolver: true, // hide warning message
  },
};

We can then run PROXY_ENV=<insert-env> next dev and then NextJS development instance will point to the appropriate API endpoint.

The implications

The only problem with this approach is that the defined NextJS route (pages/api/[[...slug]]) can be reached in production because NextJS doesn't support conditional removal of API routes. We can avoid the invocation of the proxy middleware by checking if the current environment is NOT "development" and return a 404 (can be changed to something more suitable).