Request Handling & Routing
The request/response paradigm is at the heart of web development - when a browser makes a request, your server needs to respond with content. RedwoodSDK makes this easy with the defineApp function, which lets you elegantly handle incoming requests and return the right responses.
import { defineApp } from "rwsdk/worker";import { route } from "rwsdk/router";import { env } from "cloudflare:workers";
export default defineApp([ // Middleware function middleware({ request, ctx }) { /* Modify context */ }, function middleware({ request, ctx }) { /* Modify context */ }, // Request Handlers route("/", function handler({ request, ctx }) { return new Response("Hello, world!"); }), route("/ping", function handler({ request, ctx }) { return new Response("Pong!"); }), route("/api/users", { get: () => new Response(JSON.stringify(users)), post: () => new Response("Created", { status: 201 }), }),]);The defineApp function takes an array of middleware and route handlers that are executed in the order they are defined. In this example the request is passed through two middleware functions before being “matched” by the route handlers.
Matching Patterns
Section titled “Matching Patterns”Routes are matched in the order they are defined. You define routes using the route function. Trailing slashes are optional and normalized internally.
import { route } from "rwsdk/router";
defineApp([route("/match-this", () => new Response("Hello, world!"))]);route parameters:
- The matching pattern string
- The request handler function
There are three matching patterns:
Static
Section titled “Static”Match exact pathnames.
route("/", ...)route("/about", ...)route("/contact", ...)Parameter
Section titled “Parameter”Match dynamic segments marked with a colon (:). The values are available in the route handler via params (params.id and params.groupId).
route("/users/:id", ...)route("/users/:id/edit", ...)route("/users/:id/addToGroup/:groupId", ...)Wildcard
Section titled “Wildcard”Match all remaining segments after the prefix, the values are available in the route handler via params.$0, params.$1, etc.
route("/files/*", ...)route("/files/*/preview", ...)route("/files/*/download/*", ...)Query Parameters
Section titled “Query Parameters”RedwoodSDK uses the standard Web Request object. To access query parameters, you can use the standard URL API:
route("/search", ({ request }) => { const url = new URL(request.url); const name = url.searchParams.get("name");
return <div>Hello, {name}!</div>;});To get multiple values for a single key (e.g., ?tag=js&tag=react):
route("/posts", ({ request }) => { const url = new URL(request.url); const tags = url.searchParams.getAll("tag"); // ["js", "react"]
return <div>Filtering by tags: {tags.join(", ")}</div>;});Request Handlers
Section titled “Request Handlers”The request handler is a function, or array of functions (See Interrupters), that are executed when a request is matched.
import { route } from "rwsdk/router";
defineApp([ route("/a-standard-response", ({ request, params, ctx }) => { return new Response("Hello, world!"); }), route("/a-jsx-response", () => { return <div>Hello, JSX world!</div>; }),]);The request handler function takes a RequestInfo object as its parameter.
Return values:
Response: A standard response object.JSX: A React component, which is rendered to HTML on the server and streamed to the client. This allows the browser to progressively render the page before it is hydrated on the client side.
HTTP Method Routing
Section titled “HTTP Method Routing”You can handle different HTTP methods (GET, POST, PUT, DELETE, etc.) on the same path by passing an object with method keys:
route("/api/users", { get: () => new Response(JSON.stringify(users)), post: ({ request }) => new Response("User created", { status: 201 }), delete: () => new Response("User deleted", { status: 204 }),});Method handlers can also be arrays of functions, allowing you to use interrupters per method:
route("/api/users", { get: [isAuthenticated, () => new Response(JSON.stringify(users))], post: [isAuthenticated, validateUser, createUserHandler],});Standard HTTP Methods: delete, get, head, patch, post, put
Custom Methods: Use the custom key for non-standard methods (case-insensitive):
route("/api/search", { custom: { report: () => new Response("Report data"), },});Automatic OPTIONS & 405 Support: By default, OPTIONS requests return 204 No Content with an Allow header, and unsupported methods return 405 Method Not Allowed.
Configuration: Disable automatic behaviors:
route("/api/users", { get: () => new Response("OK"), config: { disableOptions: true, // OPTIONS returns 405 disable405: true, // Unsupported methods fall through to 404 },});Interrupters
Section titled “Interrupters”Interrupters are an array of functions that are executed in sequence for each matched request. They can be used to modify the request, context, or to short-circuit the response. A typical use-case is to check for authentication on a per-request basis, as an example you’re trying to ensure that a specific user can access a specific resource.
2 collapsed lines
import { defineApp } from "rwsdk/worker";import { route } from "rwsdk/router";import { EditBlogPage } from "src/pages/blog/EditBlogPage";
function isAuthenticated({ request, ctx }) { // Ensure that this user is authenticated if (!ctx.user) { return new Response("Unauthorized", { status: 401 }); }}
defineApp([route("/blog/:slug/edit", [isAuthenticated, EditBlogPage])]);For the /blog/:slug/edit route, the isAuthenticated function will be executed first, if the user is not authenticated, the response will be a 401 Unauthorized. If the user is authenticated, the EditBlogPage component will be rendered. Therefore the flow is interrupted. The isAuthenticated function can be shared across multiple routes.
Middleware & Context
Section titled “Middleware & Context”The context object (ctx) is a mutable object that is passed to each request handler, interrupters, and React Server Functions. It’s used to share data between the different parts of your application. You populate the context on a per-request basis via Middleware.
Middleware runs before the request is matched to a route. You can specify multiple middleware functions, they’ll be executed in the order they are defined.
import { defineApp } from "rwsdk/worker";import { route } from "rwsdk/router";import { env } from "cloudflare:workers";
defineApp([ sessionMiddleware, async function getUserMiddleware({ request, ctx }) { if (ctx.session.userId) { ctx.user = await db.user.find({ where: { id: ctx.session.userId } }); } }, route("/hello", [ function ({ ctx }) { if (!ctx.user) { return new Response("Unauthorized", { status: 401 }); } }, function ({ ctx }) { return new Response(`Hello ${ctx.user.username}!`); }, ]),]);The context object:
sessionMiddlewareis a function that is used to populate thectx.sessionobjectgetUserMiddlewareis a middleware function that is used to populate thectx.userobject"/hello"is a an array of route handlers that are executed when “/hello” is matched:
- if the user is not authenticated the request will be interrupted and a 401 Unauthorized response will be returned
- if the user is authenticated the request will be passed to the next request handler and
"Hello {ctx.user.username}!"will be returned
Extending App Context Types
Section titled “Extending App Context Types”To get full type safety for your custom context data (like ctx.user), you can extend the DefaultAppContext interface in a global.d.ts file in your project’s root.
import { User } from "@db/index";import { DefaultAppContext } from "rwsdk/worker";
interface AppContext { user?: User; session?: { userId: string | null };}
declare module "rwsdk/worker" { interface DefaultAppContext extends AppContext {}}Now, whenever you access ctx in your handlers or via getRequestInfo().ctx, TypeScript will know about the user and session properties without needing manual casting.
Documents
Section titled “Documents”Documents are how you define the “shell” of your application’s html: the <html>, <head>, <meta> tags, scripts, stylesheets, <body>, and where in the <body> your actual page content is rendered. In RedwoodSDK, you tell it which document to use with the render() function in defineApp. In other words, you’re asking RedwoodSDK to “render” the document.
import { defineApp } from "rwsdk/worker";import { route, render } from "rwsdk/router";
import { Document } from "@/pages/document";import { HomePage } from "@/pages/home-page";
export default defineApp([render(Document, [route("/", HomePage)])]);The render function takes a React component and an array of route handlers. The document will be applied to all the routes that are passed to it.
This component will be rendered on the server side when the page loads. When defining this component, you’d add:
- Your application’s stylesheets and scripts
export const Document = ({ children }) => ( <html lang="en"> <head> <meta charSet="utf-8" /> <script type="module" src="/src/client.tsx"></script> </head> <body> {children} </body> </html>);