スキル一覧に戻る
kcc989

rwsdk-routing-middleware

by kcc989

0🍴 0📅 2025年12月27日
GitHubで見るManusで実行

SKILL.md


rwsdk Routing & Middleware

Overview

rwsdk uses defineApp() to define request handling as an ordered array of middleware and route handlers. Routes match by pattern (static, parameter, wildcard), support HTTP method routing, and can use interrupters for authentication/validation. Middleware populates a shared context object available throughout the request lifecycle.

When to Use

Use when:

  • Building Cloudflare Workers apps with rwsdk
  • Need request routing with middleware pipeline
  • Want authentication guards per route
  • Sharing data between middleware and handlers
  • Need type-safe link generation
  • Rendering React Server Components

Don't use when:

  • Using different routing framework (Next.js, Remix)
  • Simple Worker without routing needs (just use fetch handler)

Quick Start Pattern

import { defineApp } from 'rwsdk/worker';
import { route, render } from 'rwsdk/router';

export default defineApp([
  // 1. Middleware (runs before routing)
  sessionMiddleware,
  getUserMiddleware,

  // 2. Routes with interrupters
  route('/admin', [isAuthenticated, isAdmin, AdminPage]),
  route('/users/:id', UserProfilePage),

  // 3. Wrapped in Document
  render(Document, [route('/', HomePage), route('/about', AboutPage)]),
]);

Execution order: Middleware → Route matching → Interrupters → Handler

Route Matching Patterns

PatternExampleMatchAccess Values
Staticroute("/about", ...)Exact path /aboutN/A
Parameterroute("/users/:id", ...)/users/123params.id
Multi-paramroute("/users/:id/groups/:groupId", ...)/users/123/groups/456params.id, params.groupId
Wildcardroute("/files/*", ...)/files/any/pathparams.$0
Complex wildcardroute("/files/*/preview", ...)/files/docs/previewparams.$0 (= "docs")

Key behaviors:

  • Routes match in definition order (first match wins)
  • Trailing slashes normalized automatically (/about = /about/)
  • Parameters available in params object

Request Handlers

Two return types supported:

Response Object

route('/api/users', ({ request, params, ctx }) => {
  return new Response(JSON.stringify(users), {
    headers: { 'Content-Type': 'application/json' },
  });
});

JSX (React Server Components)

route("/profile/:id", ({ params }) => {
  return <UserProfile userId={params.id} />;
});

JSX is streamed: Browser progressively renders before hydration.

HTTP Method Routing

route('/api/users', {
  get: () => new Response(JSON.stringify(users)),
  post: ({ request }) => new Response('Created', { status: 201 }),
  delete: () => new Response('Deleted', { status: 204 }),
  custom: {
    report: () => new Response('Report data'), // Custom methods
  },
});

Automatic behaviors:

  • OPTIONS returns 204 No Content with Allow header
  • Unsupported methods return 405 Method Not Allowed

Disable auto-behaviors:

route('/api/users', {
  get: () => new Response('OK'),
  config: {
    disableOptions: true, // OPTIONS returns 405
    disable405: true, // Unsupported methods fall through to 404
  },
});

Interrupters (Authentication Guards)

Interrupters are arrays of functions that execute in sequence. Return a Response to short-circuit:

function isAuthenticated({ request, ctx }) {
  if (!ctx.user) {
    return new Response('Unauthorized', { status: 401 });
  }
  // Return nothing to continue
}

function isAdmin({ ctx }) {
  if (ctx.user.role !== 'admin') {
    return new Response('Forbidden', { status: 403 });
  }
}

defineApp([
  route('/admin', [isAuthenticated, isAdmin, AdminDashboard]),
  route('/profile', [isAuthenticated, UserProfile]),
]);

Per-method interrupters:

route('/api/users', {
  get: [isAuthenticated, () => new Response(JSON.stringify(users))],
  post: [isAuthenticated, isAdmin, validateUser, createUserHandler],
});

Middleware & Context

Middleware runs before route matching and populates the shared ctx object:

import { defineApp } from "rwsdk/worker";

defineApp([
  // Middleware 1: Session
  async function sessionMiddleware({ request, ctx }) {
    ctx.session = await getSession(request);
  },

  // Middleware 2: User (depends on session)
  async function getUserMiddleware({ request, ctx }) {
    if (ctx.session?.userId) {
      ctx.user = await db.selectFrom("users")
        .where("id", "=", ctx.session.userId)
        .selectAll()
        .executeTakeFirst();
    }
  },

  // Routes can access ctx.user
  route("/dashboard", [
    ({ ctx }) => {
      if (!ctx.user) {
        return new Response("Unauthorized", { status: 401 });
      }
    },
    ({ ctx }) => <Dashboard user={ctx.user} />,
  ]),
]);

Context flow: Middleware populates → Interrupters check → Handlers use

Documents (HTML Shell)

Documents define the HTML structure (<html>, <head>, <body>):

import { render } from "rwsdk/router";

export const Document = ({ children }) => (
  <html lang="en">
    <head>
      <meta charSet="utf-8" />
      <meta name="viewport" content="width=device-width, initial-scale=1" />
      <link rel="stylesheet" href="/styles.css" />
      <script type="module" src="/src/client.tsx"></script>
    </head>
    <body>
      <div id="root">{children}</div>
    </body>
  </html>
);

defineApp([
  render(Document, [
    route("/", HomePage),
    route("/about", AboutPage),
  ]),
]);

Document applies to all nested routes - wrap route arrays with render().

Request Info Object

Access request/response in server functions using requestInfo:

import { requestInfo } from "rwsdk/worker";

export async function myServerFunction() {
  const { request, response, ctx, cf } = requestInfo;

  // Mutate response
  response.status = 404;
  response.headers.set("Cache-Control", "no-store");

  // Access context
  const user = ctx.user;

  // Access Cloudflare context
  const country = cf.country;

  return <NotFound />;
}

Available properties:

  • request: Incoming HTTP Request object
  • response: ResponseInit object (mutate for status/headers)
  • ctx: Application context
  • rw: rwsdk-specific context
  • cf: Cloudflare Execution Context API

Generate links with full type safety using linkFor:

// src/lib/links.ts
import { linkFor } from 'rwsdk/router';

type App = typeof import('../../worker').default;

export const link = linkFor<App>();

Usage (in client or server code):

import { link } from '@/lib/links';

// Static routes
const homeUrl = link('/');
const aboutUrl = link('/about');

// Dynamic routes with params
const userUrl = link('/users/:id', { id: '123' });
// Result: "/users/123"

const editUrl = link('/users/:id/edit', { id: userId });
// TypeScript ensures userId is provided

Key benefits:

  • Type-only import (no Worker code in client bundles)
  • TypeScript verifies route exists
  • TypeScript ensures required params provided
  • Autocomplete for all routes

Common Mistakes

MistakeFix
Routes in wrong orderPut specific routes before wildcards: /users/:id before /users/*
Missing return in interruptersExplicitly return new Response(...) to short-circuit, or return nothing to continue
Middleware after routesMiddleware must come before route definitions in array
Mutating request objectRequest is immutable - use ctx for shared state
Forgetting params accessUse params.id not request.params.id
Not awaiting async middlewareMark middleware functions as async if they use await
linkFor without type importUse typeof import("...") pattern for type-only reference

Request Handler Patterns

API Routes (JSON)

route('/api/todos', {
  get: async ({ ctx }) => {
    const todos = await db.selectFrom('todos').selectAll().execute();
    return new Response(JSON.stringify(todos), {
      headers: { 'Content-Type': 'application/json' },
    });
  },
  post: async ({ request, ctx }) => {
    const body = await request.json();
    await db.insertInto('todos').values(body).execute();
    return new Response('Created', { status: 201 });
  },
});

Protected Routes

function requireAuth({ ctx }) {
  if (!ctx.user) {
    return new Response('Unauthorized', { status: 401 });
  }
}

route('/settings', [requireAuth, SettingsPage]);

Conditional Rendering

route("/post/:slug", async ({ params, ctx }) => {
  const post = await db.selectFrom("posts")
    .where("slug", "=", params.slug)
    .selectAll()
    .executeTakeFirst();

  if (!post) {
    requestInfo.response.status = 404;
    return <NotFoundPage />;
  }

  return <PostPage post={post} />;
});

Middleware Composition Pattern

// Composable middleware
const withAuth = [sessionMiddleware, getUserMiddleware];
const withRateLimit = [rateLimitMiddleware];

defineApp([
  ...withAuth,
  ...withRateLimit,

  route('/', HomePage),
  route('/api/data', [isAuthenticated, DataHandler]),
]);

Advanced: Multiple Documents

defineApp([
  middleware,

  // Marketing site with marketing document
  render(MarketingDocument, [
    route('/', LandingPage),
    route('/pricing', PricingPage),
  ]),

  // App with app document (includes navigation)
  render(AppDocument, [
    route('/dashboard', DashboardPage),
    route('/settings', SettingsPage),
  ]),
]);

Execution Flow Diagram

Request → Middleware 1 → Middleware 2 → Route Matching
                                              ↓
                                        Interrupter 1
                                              ↓
                                        Interrupter 2
                                              ↓
                                         Handler
                                              ↓
                                    Wrap in Document
                                              ↓
                                         Response

Quick Reference Card

TaskCode
Define appdefineApp([...middleware, ...routes])
Static routeroute("/path", handler)
Dynamic routeroute("/users/:id", handler)
Wildcardroute("/files/*", handler)
HTTP methodsroute("/api", { get, post, delete })
Guard routeroute("/admin", [isAuth, handler])
Middlewarefunction mid({ request, ctx }) { ctx.x = y }
Documentrender(Document, [routes])
Type-safe linkslink("/users/:id", { id: "123" })
Mutate responserequestInfo.response.status = 404

Performance Notes

  • Routes match sequentially: Order matters for performance
  • Middleware runs on every request: Keep lightweight
  • Context is per-request: No shared state between requests
  • JSX streaming: Progressive rendering before hydration
  • Type imports: Zero runtime cost for linkFor type references

スコア

総合スコア

50/100

リポジトリの品質指標に基づく評価

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

0/10
説明文

100文字以上の説明がある

0/10
人気

GitHub Stars 100以上

0/15
最近の活動

3ヶ月以内に更新がある

0/10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

0/5

レビュー

💬

レビュー機能は近日公開予定です