Forwarding requests to API endpoints in SvelteKit without a reverse proxy
Skip to the bottom if you just want to see the code.
APIs will usually give you a secret key or a client ID to be able to use their endpoints. They'll also tell you not to give it to the client for obvious reasons.
If you're forward-thinking, you probably thought of having your server talk to the API first, then sending data down to the client afterwards. This is great because any sensitive data stays between your server and the API's.
The client doesn't need to talk to the API. Rather, the server talks to the API on the client's behalf. This is what is known as reverse proxying.
Traditionally, web apps would use reverse proxy software like nginx (or more modern alternatives like Caddy or Traefik) to do this. If you had a separate frontend and backend, your reverse proxy might serve your frontend at https://mysite.com
and backend at https://mysite.com/api
.
In this case, we want to access our SvelteKit frontend normally, but use some route like /api/external/...
to actually access the API.
Creating endpoints
Let's say we have an API at https://external.com
. Their documentation says that you can get the list of all blog posts at https://external.com/api/v1/posts
and authors at https://external.com/api/v1/authors
.
Let's create a SvelteKit endpoint at /api/external/posts
to get the list of posts:
// src/routes/api/external/posts/+server.ts
import { API_KEY } from '$env/static/private';
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async ({ fetch }) => {
const url = 'https://example.com/api/v1/posts';
const response = await fetch(url, {
headers: {
authorization: API_KEY; // The client doesn't need to provide the key!
}
});
const posts = processResponse(response); // Parse the JSON response, transform it, etc.
return {
posts
};
};
Now to get the list of authors, we can create src/
and simply copy-paste. But most times, APIs have lots of different endpoints, and it would be tedious and error-prone to create new +server.ts
files for each endpoint.
We can greatly improve the ergonomics of this by simply catching the ...
in /api/external/...
and throwing it in https://external.com/api/v1/...
. We wouldn't have to keep creating new endpoint files, and it would make it easier to follow along the API's documentation!
In SvelteKit, we can achieve this with rest parameters.
Rest parameters
We know we want to use /api/external/...
to access the API. So let's create a file at src/routes/api/external/[...path]/+server.ts
:
// src/routes/api/external/[...path]/+server.ts
import type { RequestHandler } from './$types';
import { API_KEY } from '$env/static/private';
export const GET: RequestHandler = async ({ params, fetch }) => {
// `path` comes from the folder name '[...path]'.
// If you tried to fetch from `/api/external/foo/bar`, `path` would be 'foo/bar'.
const { path } = params;
const baseURL = 'https://example.com/api/v1';
const url = `${baseURL}/${path}`; // We've moved the path over to the actual API now!
return await fetch(url, {
headers: {
authorization: API_KEY;
}
});
};
If you're just sending GET
requests, this is everything you need. If you need to handle other request methods like POST
or DELETE
, you can actually just export the same function multiple times:
// src/routes/api/external/[...path]/+server.ts
import type { RequestHandler } from './$types';
import { API_KEY } from '$env/static/private';
const handle: RequestHandler = async ({ params, fetch }) => {
const { path } = params;
const baseURL = 'https://example.com/api/v1';
const url = `${baseURL}/${path}`;
return await fetch(url, {
headers: {
authorization: API_KEY;
}
});
};
export {
handle as GET,
handle as POST,
handle as DELETE,
handle as PUT,
handle as PATCH,
handle as OPTIONS
};
One more tweak we can make is we can use all the headers, cookies, and everything else from the original request and pass that along to the API without having to reconstruct them:
// src/routes/api/external/[...path]/+server.ts
import type { RequestHandler } from "./$types";
import { API_KEY } from "$env/static/private";
const handle: RequestHandler = async ({ params, request, fetch }) => {
const { path } = params;
const baseURL = "https://example.com/api/v1";
const url = `${baseURL}/${path}`;
// Keep the exact same request with a different URL.
const newRequest = new Request(url, request);
// Important!
// The original request's `Host` value is *this* server. If this header
// does not match the the server that it's intended for, then `fetch` will
// 404 and throw an `UNABLE_TO_VERIFY_LEAF_SIGNATURE` error. To avoid this,
// we simply remove the `Host` header.
newRequest.headers.delete('host');
// Add the auth token!
newRequest.headers.set("authorization", API_KEY);
return await fetch(newRequest);
};
export {
handle as GET,
handle as POST,
handle as DELETE,
handle as PUT,
handle as PATCH,
handle as OPTIONS,
};
Now you can go ahead and /api/
!
I'm including another version below where I've actually needed to handle some auth-related headers and cookies for a real project. It might help if your API isn't using common auth practices.
TL;DR
Here you go. Thanks for skipping reading! ✻
// src/routes/api/externalservice/[...path]/+server.ts
import { SECRET_HEADER_VALUE } from "$env/static/private";
import type { RequestHandler } from "./$types";
const handler: RequestHandler = async ({ params, request, fetch }) => {
const { path } = params;
// `fetch('/api/externalservice/foo/bar')` will resolve to
// `https://externalservice.com/api/v1/foo/bar`.
const apiBaseURL = "https://externalservice.com/api/v1";
const url = `${apiBaseURL}/${path}`;
// Use the same request headers, cookies, etc.,
// but pointed at the new URL.
const newRequest = new Request(url, request);
// Avoid confusing the external API.
newRequest.headers.delete("host");
// Add custom headers.
newRequest.headers.set("secret-header", SECRET_HEADER_VALUE);
// Consume headers and transform them for the API request.
const accessToken = newRequest.headers.get("authorization");
newRequest.headers.delete("authorization");
if (accessToken) {
newRequest.headers.set("cookie", `token=${accessToken}`);
}
// Get exactly what the API returns!
return await fetch(newRequest);
};
// Use the same handler for all endpoint methods.
export {
handler as DELETE,
handler as GET,
handler as OPTIONS,
handler as PATCH,
handler as POST,
handler as PUT,
};