Handling HTTP Requests
Cloudflare Workers utilize standard Web APIs — specifically the Fetch API. There are no heavy frameworks or proprietary Cloudflare routing objects; you receive a standard Request object and must return a standard Response object.
By the end of this module, you will understand the ES Module syntax structure, how to read incoming Request data (headers, URLs, JSON bodies), and how to manipulate Responses before sending them to the client.
The ES Module Syntax (ESM)
The modern standard for writing Workers is using Exported Modules (ESM). A Worker file exports an object containing event handlers (like fetch, scheduled, or email).
export interface Env {
// We'll cover this in Module 6
}
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
return new Response("Hello World!", {
status: 200,
headers: {
"Content-Type": "text/plain"
}
});
}
};
The Three fetch Parameters
Every fetch event handler receives three crucial arguments:
request: The standard HTTPRequestobject containing the URL, headers, method, and body.env: An object containing your environment variables, Secrets, and structural bindings (like KV namespaces or D1 databases).ctx: The Execution Context. This is used to control the lifecycle of the Worker (primarily usingctx.waitUntil()for background tasks).
Reading the Request
Because Workers use the standard Request object, parsing data is straightforward.
Analyzing the URL
To read paths or query parameters, pass the request.url string into a standard URL object.
const url = new URL(request.url);
if (url.pathname === "/api/users") {
// Respond to /api/users
}
// Extracting a query parameter like ?userId=123
const userId = url.searchParams.get("userId");
Reading Headers and Methods
const method = request.method; // "GET", "POST", etc.
const authHeader = request.headers.get("Authorization");
const userAgent = request.headers.get("User-Agent");
Parsing JSON Bodies
Standard Fetch API handles streams natively. To parse a JSON body (for a POST request):
if (request.method === "POST") {
try {
const data = await request.json();
console.log(data.username);
} catch (error) {
return new Response("Invalid JSON", { status: 400 });
}
}
Methods like .json(), .text(), or .formData() consume the request body stream. You cannot read the body twice. If you need to read it multiple times, you must clone the request first: const clonedReq = request.clone();.
Constructing the Response
A Response object requires the body data, and an optional init object containing status codes and headers.
Returning JSON
Returning a JSON object requires setting the Content-Type. Cloudflare explicitly provides a helper Response.json() shortcut for this.
// Traditional way:
return new Response(JSON.stringify({ status: "success" }), {
headers: { "Content-Type": "application/json" }
});
// Modern shortcut:
return Response.json({ status: "success" });
HTML Responses
const html = `<h1>Welcome to my Worker!</h1>`;
return new Response(html, {
headers: { "Content-Type": "text/html;charset=UTF-8" }
});
Intermediate Pattern: The Middleware Proxy
Often, a Worker doesn't generate the content itself; it sits in front of another server (the "origin") and intercepts the traffic.
Pass-through (Fetch)
To pass the request along to your origin server, you use the global fetch() method.
export default {
async fetch(request, env, ctx) {
// 1. Intercept the request
const url = new URL(request.url);
// 2. Pass it to the origin
let response = await fetch(request);
// 3. Return the origin's response back to the user
return response;
}
};
Modifying the Request Before Sending
Assume you want to force all requests to use your secure backend API, but the user is hitting the Worker domain.
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
// Change the hostname to point to your real backend
url.hostname = "internal-api.example.com";
// Create a NEW request based on the old one, but with the new URL
const modifiedRequest = new Request(url.toString(), request);
// Add a secret header the backend requires
modifiedRequest.headers.set("X-Internal-Secret", env.SHARED_SECRET);
return fetch(modifiedRequest);
}
};
Modifying the Response Before Returning
If you want to add security headers to the output before the user sees it:
export default {
async fetch(request, env, ctx) {
const response = await fetch(request);
// Responses from `fetch()` are immutable. You must construct a new one.
const newResponse = new Response(response.body, response);
// Inject security headers
newResponse.headers.set("X-Content-Type-Options", "nosniff");
newResponse.headers.set("X-Frame-Options", "DENY");
return newResponse;
}
};
Background Tasks: ctx.waitUntil()
If your Worker needs to log data to an analytics server, you usually don't want the user to wait for that network request to finish before receiving their response.
ctx.waitUntil() allows you to tell the Worker to keep running after it has returned the Response to the user, ensuring the task completes without penalizing user latency.
export default {
async fetch(request, env, ctx) {
// The background task function
async function logRequestToAnalytics() {
await fetch("https://analytics.example.com/log", {
method: "POST",
body: JSON.stringify({ path: request.url })
});
}
// Tell the Worker: Don't kill the isolate until this promise resolves!
ctx.waitUntil(logRequestToAnalytics());
// Respond immediately to the user
return new Response("Success!");
}
};
What's Next
Now that we can intercept, modify, and return basic HTTP traffic, we need to explore how to persist and read data across requests.
Proceed to Module 5: State and Storage to learn about databases and object storage on the edge.