Adding a reverse proxy to an Astro site hosted on Cloudflare Pages

Hint: You don't need a worker, just use a 'Pages Function'.

I have a fairly common use case on my projects. There’s a client side hosted inside a static Astro website and that client side wants to call /api on the domain it was fetched from (to avoid CORS and similar issues). But I deploy my Astro sites as pure static pages.

I use Cloudflare Pages for hosting these websites. On the one hand I already use Cloudflare’s DNS to protect the website and Pages is just a natural extension of that. It comes with the same continuous deployment from Git that other providers like Vercel and Netlify offer.

Adding Pages Functions

Cloudflare Pages is built on Cloudflare Workers, the serverless functions they provide. You can run Workers in front of Pages, but then you’re running two services. The easier approach is to use Cloudflare Pages Function.

Basically you add a functions directory to the root of your Astro project (not the public, src or dist directory, but directly in the root if the repository). Then add a Javascript file with the name of the path you would like to serve.

/functions/foo.js will response to requests for /foo.
/functions/api/bar.js will response to requests for /api/bar.

The remainder of the site will be served as usual, except for any paths defined in functions. Pages Functions take precendent over the static assets.

Proxying to another service

In my case I want to forward POST requests on a specific path to another server. The script in /functions/api.js I use is the following:

export async function onRequest(context) {
	// Only allow POST requests
	if (context.request.method !== 'POST') {
		return new Response('Method not allowed', {
			status: 405,
			headers: {
				Allow: 'POST',
				'Content-Type': 'application/json'
			}
		})
	}

	try {
		// Get the JSON payload from the request
		const payload = await context.request.json()

		// Forward the request to the licensing server
		const response = await fetch('https://target.example.com/api', {
			method: 'POST',
			headers: {
				'Content-Type': 'application/json'
			},
			body: JSON.stringify(payload)
		})

		// Get the response data
		const data = await response.json()

		// Return the response with appropriate headers
		return new Response(JSON.stringify(data), {
			status: response.status,
			headers: {
				'Content-Type': 'application/json',
				'Access-Control-Allow-Origin': '*' // Adjust this based on your CORS requirements
			}
		})
	} catch (error) {
		// Handle any errors that occur during the request
		return new Response(
			JSON.stringify({
				error: 'Internal Server Error',
				message: error.message
			}),
			{
				status: 500,
				headers: {
					'Content-Type': 'application/json'
				}
			}
		)
	}
}

Limits

A quick note on the limits. If you are using the Free plan you are limited to 100K executions of that worker. That may be plenty if you’re justing forwarding a few API calls to a backend server, but if you’re communicating heavily with an API then you’ll hit that limit on no time.

The good news is, the paid plan at currently 5 USD per month, increases that limit to 10 million per day. Over that you’ll pay

$0.30 per additional million + $0.02 per additional million CPU milliseconds

You may want to add monitoring of those cost if you’re worried about it:

To prevent accidental runaway bills or denial-of-wallet attacks, configure the maximum amount of CPU time that can be used per invocation by defining limits in your Worker's wrangler.toml file, or via the Cloudflare dashboard (Workers & Pages > Select your Worker > Settings > CPU Limits).

See details here

Conclusion

Hosting static Astro websites on Cloudflare Pages is a no-brainer. A built-in CDN and the general Cloudflare service set are very appealing.

With Pages Functions you can easily reverse proxy an API path to a server or lamda function as part of that static website and it is easy to setup and deploys as part of your static website seamlessly.