Request Handling & Routing
The request/response cycle 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 "@redwoodjs/sdk/worker";import { route } from "@redwoodjs/sdk/router";
export default defineApp([ // Middleware function middleware({ request, env, ctx }) { /* Modify context */ }, function middleware({ request, env, ctx }) { /* Modify context */ }, // Request Handlers route("/", function handler({ request, env, ctx }) { return new Response("Hello, world!") }), route("/ping", function handler({ request, env, ctx }) { return new Response("Pong!") }),]);
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
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 "@redwoodjs/sdk/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
Match exact pathnames.
route("/", ...)route("/about", ...)route("/contact", ...)
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
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/*", ...)
Request Handlers
The request handler is a function, or array of functions (See Interruptors), that are executed when a request is matched.
import { route } from "@redwoodjs/sdk/router";
defineApp([ route("/a-standard-response", ({ request, params, env, ctx }) => { return new Response("Hello, world!") }), route('/a-jsx-response', () => { return <div>Hello, JSX world!</div> }),])
The request handler function takes in the following options:
request
: The request object.params
: The matched parameters from the request URL.env
: The Cloudflare environment.ctx
: The context object (See Middleware & Context).
Return values:
Response
: A standard response object.JSX
: A React component, which is statically rendered to HTML on the server, streamed to the client, and then hydrated on the client side.
Interruptors
Interruptors 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 "@redwoodjs/sdk/worker";import { route } from "@redwoodjs/sdk/router";import { EditBlogPage } from "src/pages/blog/EditBlogPage";
function isAuthenticated({ request, env, 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
Context is a mutable object that is passed to each request handler, interruptors, 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 "@redwoodjs/sdk/worker";import { route } from "@redwoodjs/sdk/router";
defineApp([ sessionMiddleware, async function getUserMiddleware({ request, env, 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 ctx
object:
sessionMiddleware
is a function that is used to populate thectx.session
objectgetUserMiddleware
is a middleware function that is used to populate thectx.user
object"/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
Layouts
Layouts are a way to wrap your pages in a consistent layout. In RedwoodSDK, layouts are defined using the layout
function in defineApp
.
import { defineApp } from "@redwoodjs/sdk/worker";import { route, layout } from "@redwoodjs/sdk/router";
import { Document } from "@/pages/Document";import { HomePage } from "@/pages/HomePage";
export default defineApp([ layout( Document, [route("/", HomePage)])]);
The layout
function takes a React component and an array of route handlers. The layout will be applied to all the routes that are passed to it.
export const Document = ({ children }) => ( <html lang="en"> <head> <meta charSet="utf-8" /> <script type="module" src="/src/client.tsx"></script> </head> <body> <div id="root">{children}</div> </body> </html>);