Cloudflare Pages Functions are surprisingly not just for API endpoints, but for any dynamic server-side computation you need attached to your static site.
Let’s see this in action. Imagine you have a static site hosted on Cloudflare Pages, and you want to display a list of "featured products" that are actually managed in a separate database or API. You could pre-render this list at build time, but what if that list changes frequently?
Here’s a functions/featured-products.js file in your Pages project:
// functions/featured-products.js
export async function onRequest(context) {
// context.env contains bindings like KV namespaces, R2 buckets, etc.
// context.params contains route parameters if any.
// context.request is the incoming request object.
// For this example, let's simulate fetching from an external API
// In a real scenario, you might use fetch() to call another service
// or access bindings like KV or R2.
const featuredProducts = [
{ id: 1, name: "Quantum Widget", price: 199.99 },
{ id: 2, name: "Nebula Gadget", price: 299.50 },
{ id: 3, name: "Cosmic Gizmo", price: 99.00 },
];
// You can also access environment variables
const API_KEY = context.env.MY_EXTERNAL_API_KEY; // Assuming you've bound this
// Simulate some dynamic logic: maybe filter by availability or price threshold
const filteredProducts = featuredProducts.filter(product => product.price < 300);
return new Response(JSON.stringify(filteredProducts), {
headers: {
"Content-Type": "application/json",
// You can add custom headers, CORS headers, etc.
"Cache-Control": "public, max-age=60", // Cache for 60 seconds
},
});
}
When Cloudflare Pages deploys, it automatically creates a route for this function. If your site is at your-site.pages.dev, this function will be accessible at your-site.pages.dev/featured-products.
Your static HTML could then fetch this:
<!DOCTYPE html>
<html>
<head>
<title>Featured Products</title>
</head>
<body>
<h1>Our Featured Products</h1>
<ul id="products-list">
<li>Loading...</li>
</ul>
<script>
fetch('/featured-products')
.then(response => response.json())
.then(products => {
const list = document.getElementById('products-list');
list.innerHTML = ''; // Clear loading message
products.forEach(product => {
const item = document.createElement('li');
item.textContent = `${product.name} - $${product.price}`;
list.appendChild(item);
});
})
.catch(error => {
console.error('Error fetching products:', error);
document.getElementById('products-list').innerHTML = '<li>Could not load products.</li>';
});
</script>
</body>
</html>
This setup allows your static site to leverage dynamic data without needing a separate backend server. The "server-side logic" lives alongside your frontend assets, deployed as a single unit.
The Mental Model:
Cloudflare Pages Functions are essentially small, single-purpose serverless functions that are automatically routed based on their file path within your project’s functions directory. When a request hits a URL that matches a function’s path, Cloudflare’s edge network intercepts it and executes that function. The function receives the request, can interact with Cloudflare’s bindings (like KV, R2, D1, or external services via fetch), and returns a Response object. This Response is then sent back to the client, just as if it came from a traditional server.
The key is the onRequest export. This is the entry point for your function. It receives a context object containing everything you need: request for incoming details, env for your bound services and secrets, and params for any dynamic segments in the URL (e.g., if you had functions/products/[id].js and a request to /products/123, params.id would be 123). You construct and return a Response object, which can be JSON, HTML, text, or anything else.
You control function behavior through how you write the onRequest logic and how you configure bindings in your Cloudflare dashboard. For instance, to access a KV namespace named MY_KV, you’d add it to your Pages project’s build settings, and then in context.env.MY_KV, you could read and write data. Similarly, environment variables are exposed via context.env.
The Cache-Control header is crucial for performance. By setting public, max-age=60, you’re telling Cloudflare (and any intermediate caches) that the response for /featured-products can be cached for up to 60 seconds. This means subsequent requests within that minute won’t even hit your function; they’ll be served directly from Cloudflare’s cache, dramatically reducing latency and execution costs.
What most people miss is how deeply integrated these functions are with Cloudflare’s broader ecosystem. You’re not just running Node.js code; you’re running it within a distributed system that has direct, low-latency access to services like Workers KV for persistent key-value storage, R2 for object storage, D1 for SQL databases, and even other Workers. This allows you to build surprisingly complex applications where your static frontend is just the presentation layer for dynamic data and logic orchestrated entirely on Cloudflare’s edge.
The next step is often handling POST requests or more complex routing patterns within your functions.