SvelteKit - Setting up an API reverse proxy


Introduction

I am currently working on a SvelteKit app that needs to communicate with a separate backend API written in Kotlin (say, my-website.com and api.my-website.com). To work around various CORS issues, I need to setup a reverse proxy for my API.

What is a Reverse Proxy?

Let’s say I setup a reverse proxy at my-website.com/api-proxy. This means that everytime my web app wants to fetch data from my API, it would first make the request to an endpoint within the webapp’s server at my-website.com/api-proxy, which would then pass that request along to the API, and forward the response it receives back to the webapp.

Solution

I tried using Sveltekit’s Actions but ran into a bunch of issues. Too much “magic” was happening. I realized that I’d rather skip any intervention on Sveltekit’s behalf here: all I want is an escape hatch that will forward my requests (& subsequent responses) as-is.


To do that, I ended up intercepting the API proxy requests in the handle hook, like so:


import type { Handle } from "@sveltejs/kit";

const MY_API_BASE_URL = "https://api.my-website.com";
const PROXY_PATH = "/api-proxy";

const handleApiProxy: Handle = async ({ event }) => {
  const origin = event.request.headers.get("Origin");

  // reject requests that don't come from the webapp, to avoid your proxy being abused.
  if (!origin || new URL(origin).origin !== event.url.origin) {
    throw error(403, "Request Forbidden.");
  }

  // strip `/api-proxy` from the request path
  const strippedPath = event.url.pathname.substring(PROXY_PATH.length);

  // build the new URL path with your API base URL, the stripped path and the query string
  const urlPath = `${MY_API_BASE_URL}${strippedPath}${event.url.search}`;
  const proxiedUrl = new URL(urlPath);

  // Strip off header added by SvelteKit yet forbidden by underlying HTTP request
  // library `undici`.
  // https://github.com/nodejs/undici/issues/1470
  event.request.headers.delete("connection");

  return fetch(proxiedUrl.toString(), {
    // propagate the request method and body
    body: event.request.body,
    method: event.request.method,
    headers: event.request.headers,
  }).catch((err) => {
    console.log("Could not proxy API request: ", err);
    throw err;
  });
};

export const handle: Handle = async ({ event, resolve }) => {
  // intercept requests to `/api-proxy` and handle them with `handleApiProxy`
  if (event.url.pathname.startsWith(PROXY_PATH)) {
    return await handleApiProxy({ event, resolve });
  }

  // ...the rest of your `handle` logic goes here
};

And that’s it! This snippet will grab any request made to /api-proxy, even if it’s a nested path like /api-proxy/users/123, and forward it to api.my-website.com/users/123. It will also forward & respect methods (POST, UPDATE, DELETE, etc.) and query parameters, so POST /api-proxy/users?name=John will be forwarded to POST api.my-website.com/users?name=John.