mcpField
S
server-routes

server-routes

github.com/tanstack开发15.3k195 次安装4 天前更新
代码仓库

关于 server-routes

面向 TanStack Router 开发者的技能,用于在应用路由旁构建 HTTP 端点。内容涵盖文件路由约定、请求处理、中间件和安全注意事项,适合需要明确 HTTP 契约的场景。

主要功能

  1. 01基于文件的服务端路由约定
  2. 02HTTP 方法处理器
  3. 03请求与响应处理
  4. 04路由及处理器中间件
  5. 05动态参数与通配符参数

适用场景

  1. 01在应用路由旁提供 HTTP API
  2. 02处理 JSON 请求体和状态码
  3. 03应用身份验证或校验中间件

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/tanstack/router --skill server-routes

安装后在 Claude Code 中输入/server-routes确认已加载。

详细介绍

服务端路由

服务端路由是在 src/routes 目录中与应用路由一同定义的 API 端点。它们使用 createFileRoute 上的 server 属性,并处理原始 HTTP 请求。

当调用方需要 HTTP 契约时,请使用服务端路由。对于仅供 Start 应用使用的数据,优先使用服务端函数,并在 loader 中直接调用。路由 loader 会在 SSR 和客户端导航期间运行,因此 fetch('/api/...') 并不是可移植的 loader 模式。

基本服务端路由

// src/routes/api/hello.ts
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/api/hello')({
  server: {
    handlers: {
      GET: async ({ request }) => {
        return new Response('Hello, World!')
      },
    },
  },
})

组合服务端路由和应用路由

同一个文件可以同时定义服务端路由和 UI 路由:

// src/routes/hello.tsx
import { createFileRoute } from '@tanstack/react-router'
import { useState } from 'react'

export const Route = createFileRoute('/hello')({
  server: {
    handlers: {
      POST: async ({ request }) => {
        const body = await request.json()
        return Response.json({ message: `Hello, ${body.name}!` })
      },
    },
  },
  component: HelloComponent,
})

function HelloComponent() {
  const [reply, setReply] = useState('')
  return (
    <button
      onClick={() => {
        fetch('/hello', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ name: 'Tanner' }),
        })
          .then((res) => res.json())
          .then((data) => setReply(data.message))
      }}
    >
      Say Hello {reply && `- ${reply}`}
    </button>
  )
}

上面的相对 fetch('/hello') 是安全的,因为它只会在浏览器点击处理程序中运行。

与 Start 路由共享数据

不要让 SSR loader 调用自己的服务端路由。将业务操作放入仅服务端使用的服务中,然后在两种边界都需要时,通过它们分别对外提供:

// src/server/issues.server.ts
export function listIssues() {
  return db.issues.findMany()
}

// src/routes/api/issues.ts
export const Route = createFileRoute('/api/issues')({
  server: {
    handlers: {
      GET: async () => Response.json(await listIssues()),
    },
  },
})

// src/routes/issues.tsx
const getIssues = createServerFn({ method: 'GET' }).handler(() => {
  return listIssues()
})

export const Route = createFileRoute('/issues')({
  loader: () => getIssues(),
})

这样可以让 SSR 不依赖 URL 解析,并确保业务逻辑只有一个来源。

文件路由约定

服务端路由遵循 TanStack Router 的基于文件的路由约定:

文件路由
routes/users.ts/users
routes/users/$id.ts/users/$id
routes/users/$id/posts.ts/users/$id/posts
routes/api/file/$.ts/api/file/$(通配符)
routes/my-script[.]js.ts/my-script.js(转义点号)

唯一路由路径

每个路由只能有一个处理程序文件。以下文件会发生冲突:

  • routes/users.ts
  • routes/users.index.ts
  • routes/users/index.ts

处理程序上下文

每个处理程序都会接收:

  • request — 传入的 Request 对象
  • params — 动态路径参数
  • context — 来自中间件的上下文
  • pathname — 匹配的路径名
  • next — 继续交由 SSR 处理的调用(返回一个 Response)

动态路径参数

// routes/users/$id.ts
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/users/$id')({
  server: {
    handlers: {
      GET: async ({ params }) => {
        return new Response(`User ID: ${params.id}`)
      },
    },
  },
})

通配符参数

// routes/file/$.ts
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/file/$')({
  server: {
    handlers: {
      GET: async ({ params }) => {
        return new Response(`File: ${params._splat}`)
      },
    },
  },
})

请求正文处理

export const Route = createFileRoute('/api/users')({
  server: {
    handlers: {
      POST: async ({ request }) => {
        const body = await request.json()
        return Response.json({ created: body.name })
      },
    },
  },
})

其他正文方法:request.text()、request.formData()。

JSON 响应

// Using Response.json helper
handlers: {
  GET: async () => {
    return Response.json({ message: 'Hello!' })
  },
}

状态码和标头

handlers: {
  GET: async ({ params }) => {
    const user = await findUser(params.id)
    if (!user) {
      return new Response('Not found', { status: 404 })
    }
    return Response.json(user)
  },
}
handlers: {
  GET: async () => {
    return new Response('Hello', {
      headers: { 'Content-Type': 'text/plain' },
    })
  },
}

服务端路由上的中间件

所有处理程序
export const Route = createFileRoute('/api/admin')({
  server: {
    middleware: [authMiddleware, loggerMiddleware],
    handlers: {
      GET: async ({ context }) => Response.json(context.user),
      POST: async ({ request, context }) => {
        /* ... */
      },
    },
  },
})
使用 createHandlers 指定处理程序
export const Route = createFileRoute('/api/data')({
  server: {
    handlers: ({ createHandlers }) =>
      createHandlers({
        GET: async () => Response.json({ public: true }),
        POST: {
          middleware: [authMiddleware],
          handler: async ({ context }) => {
            return Response.json({ user: context.session.user })
          },
        },
      }),
  },
})
组合路由级别和处理程序专属中间件
export const Route = createFileRoute('/api/posts')({
  server: {
    middleware: [authMiddleware], // runs first for all
    handlers: ({ createHandlers }) =>
      createHandlers({
        GET: async () => Response.json([]),
        POST: {
          middleware: [validationMiddleware], // runs after auth, POST only
          handler: async ({ request }) => {
            const body = await request.json()
            return Response.json({ created: true })
          },
        },
      }),
  },
})

常见错误

1. 严重:保护页面,却未保护服务端路由

每个读取或写入私有数据的处理程序,都必须通过路由中间件、处理程序中间件或处理程序内的检查,对请求进行身份验证和授权。路由器的 beforeLoad 重定向无法保护 /api/...。请在不携带 cookie 的情况下直接测试处理程序,并断言不会返回私有负载。

2. 中等:路由路径重复
# WRONG — both resolve to /users, causes error
routes/users.ts
routes/users/index.ts

# CORRECT — pick one
routes/users.ts
3. 中等:忘记等待请求正文方法完成
// WRONG — body is a Promise, not the actual data
const body = request.json()

// CORRECT — await the promise
const body = await request.json()
4. 高:将 TypeScript 类型检查误当作响应架构验证

请验证请求输入,并测试序列化后的 Response 输出。有类型的服务仍可能在投影或序列化时遗漏新添加的字段。修改架构时,请在服务端路由边界断言 await response.json()。

交叉引用

评论

支持 Markdown