Home > Posts Dev frontend with prod backend

Max Heinritz

Dev frontend with prod backend

During frontend development, I like being able to connect directly to the production backend from development frontend. For example:

# dev frontend
https://app.dev.foo.com:4000

# use a button in the dev frontend UI
# to toggle between either:

# (1) dev backend
https://api.dev.foo.com:3000

# or (2) prod backend
https://api.foo.com

Use cases

Using cookies

If you are logged in with a cookie-based session on the prod frontend, then a cookie set there can be sent by the browser even when requests are made from the dev frontend at a different URL – so long as SameSite is set to None.

Security

The permissions and security of the production backend still apply when using the dev frontend. The developer is logged in as their production user and has the same permissions with the dev frontend as they would have with prod frontend.

Implementation

Frontend

The frontend implementation involves React Context:

import { noop } from "lodash";
import { createContext } from "react";

export const BackendUrlContext = createContext<{
  backendUrl: string | undefined;
  setBackendUrl: (backendUrl: string) => void;
  checkBackendUrl: () => void;
  // True if dev frontend talking to prod backend
  // or prod frontend talking to dev backend.
  isOtherEnvBackendUrl: boolean;
}>({
  backendUrl: undefined,
  setBackendUrl: noop,
  checkBackendUrl: noop,
  isOtherEnvBackendUrl: false,
});

And a React provider.

import { ReactNode, useEffect, useState } from "react";

import {
  DEFAULT_BACKEND_URL_FOR_FRONTEND_ENV,
  PROD_BACKEND_URL,
} from "src/common/constants/backend-url";
import { readCookie, writeCookie } from "src/common/cookie/cookie";
import { AppCookieName } from "src/common/cookie/cookie.registry";
import { isProdFrontend } from "src/common/util/env.util";

import { BackendUrlContext } from "./BackendUrlContext";

const BackendUrlProvider = ({ children }: { children: ReactNode }) => {
  const [backendUrl, setBackendUrlImpl] = useState<string | undefined>(
    isProdFrontend() ? PROD_BACKEND_URL : undefined
  );

  const setBackendUrl = async (backendUrl: string) => {
    await writeCookie(AppCookieName.BACKEND_URL, { backendUrl });
    setBackendUrlImpl(backendUrl);
  };

  const checkBackendUrl = async () => {
    const wrapper = await readCookie(AppCookieName.BACKEND_URL);
    const backendUrl =
      wrapper?.backendUrl || DEFAULT_BACKEND_URL_FOR_FRONTEND_ENV;
    setBackendUrlImpl(backendUrl);
  };

  // Check the backend URL immediately upon page load.
  useEffect(() => {
    checkBackendUrl();
  }, []);

  if (!backendUrl) {
    // Wait until the backend URL is set before loading the app.
    return null;
  }

  const isOtherEnvBackendUrl =
    backendUrl !== DEFAULT_BACKEND_URL_FOR_FRONTEND_ENV;

  return (
    <BackendUrlContext.Provider
      value=
    >
      {children}
    </BackendUrlContext.Provider>
  );
};

export default BackendUrlProvider;

Then when building the Relay or Apollo environment, the context can be used to initialize the network to use that particular backend.

// ...
const { backendUrl } = useContext(BackendUrlContext);
// ...use the backendUrl to configure the GraphQL client.

Backend

The thing to watch out for on the backend is that cookies need to be HTTPS and have sameSite: 'none'.

{
  // We need "none" because the frontends make requests to api.* from
  // different domains.
  sameSite: 'none',
}