A mostly static site should send only genuinely dynamic requests through a server-side function. Keep articles, images, stylesheets, scripts, downloads, and public directory pages on the static path; reserve Functions for narrow jobs such as accepting a report, validating form input, or returning authenticated user data. On Cloudflare Pages, a deliberate _routes.json configuration file makes that boundary visible, predictable, and testable.
Without an explicit routing boundary, adding a single Function can unintentionally broaden the dynamic surface area of the entire project. That expansion creates avoidable failure modes, makes edge request limits harder to estimate, and complicates debugging. The solution is not to avoid Functions altogether, but rather to give each Function a narrow route contract and allow everything else to remain an ordinary static asset served directly from the edge cache.
List the site’s public paths before writing server-side code. Classify each route by the exact behavior required when a visitor requests it.
| Route group | Example | Preferred delivery | Reason |
| Entry pages | / and /about/ |
Static | Delivers identical content to every visitor |
| Articles | /guides/* |
Static | Prebuilt, cacheable editorial content |
| Assets | /assets/* |
Static | Versioned files requiring no dynamic processing |
| Directory data | /data/resources.json |
Static | Public, reviewed structured dataset |
| Report submission | /api/report |
Function | Validates, sanitizes, and accepts user input |
| Personal view | /api/account/* |
Function | Requires authenticated user context |
Do not route a file through a Function merely because it contains structured data. If the exact same JSON payload can be downloaded by every visitor, it belongs on the static delivery path. Dynamic execution is justified only when the response depends on unique request parameters, requires private secrets, mutates persistent state, or enforces runtime access rules.
Define the operational requirement for every dynamic route in a single sentence: "This Function exists because the response cannot be safely or correctly produced at build time." If the team cannot complete that sentence with a clear technical justification, keep the route static until a genuine requirement emerges.
Cloudflare Pages Functions use file-based routing by default. Placing a JavaScript or TypeScript file inside the project-level functions directory automatically provisions a matching route.
/
functions/
api/
report.js
account/
[user].js
public/
index.html
guides/
assets/
In this layout, /functions/api/report.js maps directly to /api/report, while the bracketed [user].js file handles variable path parameters beneath /api/account/.
The functions directory must reside at the project root, not inside the static asset folder. Conversely, the _routes.json file belongs in the final build output directory alongside the published HTML files, because it instructs the Cloudflare edge runtime which requests should invoke Functions. This distinction is easy to overlook when a build framework compiles source files from src or public into a separate dist directory. Always inspect the generated deployment folder directly to ensure the configuration file survived the build process.
When a site features only a small dynamic surface, configure _routes.json to include only that explicit prefix:
{
"version": 1,
"include": ["/api/*"],
"exclude": []
}
This configuration ensures that requests beneath /api/ can invoke Functions, while requests for articles, images, stylesheets, and public data files remain on the static path.
If an individual path beneath a dynamic prefix is intentionally static, exclude it explicitly:
{
"version": 1,
"include": ["/api/*"],
"exclude": ["/api/status.json"]
}
Exclusion rules take priority over inclusion rules. Use exclusions for genuine exceptions rather than patching an overly broad include pattern. A configuration file containing dozens of disparate exclusions usually indicates that the include pattern is too broad. Avoid starting with /* unless the entire site requires server-side execution. A static publication with a single contact endpoint does not need every page request crossing the compute layer.
Route scope defines where code executes, while method scope defines which HTTP methods the endpoint accepts. An input endpoint might accept POST requests while explicitly rejecting GET, PUT, and DELETE.
export async function onRequest(context) {
if (context.request.method !== "POST") {
return new Response("Method not allowed", {
status: 405,
headers: { Allow: "POST" }
});
}
const type = context.request.headers.get("content-type") || "";
if (!type.includes("application/json")) {
return new Response("Expected JSON", { status: 415 });
}
const body = await context.request.json();
if (typeof body.path !== "string" || body.path.length > 300) {
return new Response("Invalid report", { status: 400 });
}
return new Response("Report accepted", { status: 202 });
}
This handler represents the initial boundary check of a workflow. A production implementation still requires durable storage integrations, rate limiting, retention rules, and structured error logging.
Perform validation as close to the route boundary as possible. Reject unsupported methods, invalid content types, oversized payloads, and unverified parameters before calling downstream storage or third-party APIs. Return concise, descriptive error responses that help the client correct the request without exposing stack traces, internal variables, or architectural details.
A curated resource directory is fundamentally a static publication, even when updated on a regular schedule. Compile approved records into static HTML or a public JSON file during the build, deploy the updated artifact, and let the CDN serve requests directly from the edge cache.
For a fixture that represents an external public collection, one Korean example such as 링크모음 주소업 can be included in the reviewed data. It should behave like any other static record. Do not create a Function that fetches the destination on every visit, copies its contents, or decides in real time whether it is safe.
If editors require a submission form to propose updates, make the submission endpoint dynamic while keeping the published directory completely static. A maintainer can review pending suggestions, update the source data files, and trigger a fresh deployment. This maintains a clean separation between untrusted public input and production output.
Static and dynamic routes fail differently and require distinct recovery paths. A missing static asset returns a standard 404 response, whereas a failed form submission should preserve the user's input and provide clear guidance on whether to retry.
| Failure | Visitor result | Operator action |
| Static article missing | Site error page and navigation | Repair the broken link or restore the source file |
| Function rejects input | Field-level correction message | Review validation rules only if reports are valid |
| Function dependency unavailable | Temporary failure with retry guidance | Check backend service health and runtime logs |
| Function allowance exhausted | Defined open or closed behavior | Review routing rules and evaluate quota usage |
| Public JSON missing | HTML directory remains fully usable | Restore or re-generate the build artifact |
The choice between failing open or failing closed is an architectural decision. If a Function enforces access controls or authenticates requests, falling back to a static file may expose restricted content. If the Function merely provides a non-essential convenience, serving a static fallback is often appropriate. Document this failure behavior for every dynamic route before incidents occur.
Audit the routes capable of executing code and track the specific user actions that trigger them. A concise review matrix helps keep this surface under control:
| Metric | Desired interpretation |
| Included route patterns | Small and tied strictly to named features |
| Excluded patterns | Few, with a documented reason for each |
| Functions per feature | One clear owner and single purpose |
| Static requests invoking code | Zero unless intentionally designed |
| Dynamic requests without method tests | Zero |
Do not evaluate the routing boundary solely by looking at the functions directory. Inspect the deployed _routes.json file, framework-generated routes, edge middleware, and custom worker scripts. Modern static site generators sometimes emit routing rules automatically, so compare build output against your route inventory after major framework updates.
Use a Cloudflare Pages Preview deployment to verify both content delivery and routing rules. The test must confirm that dynamic routes invoke serverless code while static routes bypass the compute layer entirely.
Ensure the test plan covers the following checks:
The homepage and primary editorial articles load normally through static hosting.
Versioned static assets return correct cache headers without function execution.
The public resource data file remains directly downloadable.
A valid POST request to the report endpoint receives the expected success response.
A GET request to the same endpoint returns a 405 Method Not Allowed status with a valid Allow header.
An unknown path under /api/ returns a clean error without exposing runtime debug details.
Paths outside /api/ never acquire Function-only behavior.
A signed-out visitor can access all intended public pages without permission errors.
Test against deployment-specific permalink URLs rather than mutable branch aliases, as branch aliases change whenever new commits are pushed. Record the specific deploy ID, routing configuration, and test outcomes in the release log. Note that Cloudflare Pages Functions require deployment via connected Git repositories or the Wrangler CLI; Direct Upload via the web dashboard only supports purely static deployments.
Promote only the exact build artifact whose output directory and routing rules were inspected in Preview. Following production deployment, repeat a brief signed-out verification on the live domain.
Monitor dynamic function errors separately from static 404 events. A missing article indicates a content maintenance task, whereas a spike in validation errors on /api/report points to a broken client integration or abusive traffic. Combining both signals into a single alert channel obscures real operational issues. When decommissioning a Function, remove its routing rule and frontend client calls within the same release to prevent orphan routes and unexpected edge behavior.
Every dynamic route has an explicit, documented reason to exist.
The functions/ directory resides at the project root.
_routes.json is present in the final build output directory.
The include list is scoped as narrowly as possible.
Static assets and public data files never invoke Functions.
Exclusions are documented and take precedence as expected.
Each endpoint rejects unsupported HTTP methods with appropriate headers.
Input size, data types, and payload shapes are strictly validated.
Failure modes and recovery behaviors are defined per route.
The exact Preview deployment build was tested.
The deployment method supports Functions (Git integration or Wrangler).
Production was verified while signed out in a clean browser session.
A static site becomes far easier to operate when its dynamic boundary is small enough to explain in a single sitting. Let the platform serve public assets directly, route only the requests that require server-side computation, and verify that separation on every release. This approach keeps baseline content fast and resilient while maintaining a secure environment for features that genuinely need a backend.