TanStack Start 中文文档
服务器与执行

服务器路由(Server Routes)

服务器路由(Server Routes)是 TanStack Start 的一项强大特性,让你在应用中创建服务端端点,可用于处理原始 HTTP 请求、表单提交、用户认证等场景。

服务器路由可以在项目的 ./src/routes 目录中定义,与你的 TanStack Router 路由放在一起,并由 TanStack Start 服务器自动处理。

下面是一个简单的服务器路由:

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

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

注意

服务器路由面向的是需要从 TanStack Start 应用外部调用的 HTTP 端点。如果你只需要在 Start 应用内部调用服务端逻辑,并希望 Start 帮你处理序列化,请改用服务器函数

译者注:服务器函数 vs 服务器路由

两者容易混淆。简单区分:服务器函数是给应用内部用的 RPC(类型安全、自动序列化、带 CSRF 保护);服务器路由是给外部世界用的 HTTP API(直接暴露端点,返回 Response)。如果你在写一个公开的 REST/JSON API,用服务器路由;如果是页面内部的数据读写,用服务器函数。

服务器路由与应用路由

因为服务器路由可以与应用路由定义在同一个目录,你甚至可以用同一个文件同时承担两种职责!

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

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

function HelloComponent() {
  const [reply, setReply] = useState('')

  return (
    <div>
      <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
      </button>
    </div>
  )
}

文件路由约定

TanStack Start 中的服务器路由遵循与 TanStack Router 相同的基于文件的路由约定。也就是说,routes 目录中每个在 createFileRoute 调用里带有 server 属性的文件,都会被当作 API 路由处理。这里有几个例子:

  • /routes/users.ts 会在 /users 创建一个 API 路由
  • /routes/users.index.ts 同样会在 /users 创建 API 路由(但如果定义了重复的方法会报错)
  • /routes/users/$id.ts 会在 /users/$id 创建一个 API 路由
  • /routes/users/$id/posts.ts 会在 /users/$id/posts 创建一个 API 路由
  • /routes/users.$id.posts.ts 会在 /users/$id/posts 创建一个 API 路由
  • /routes/api/file/$.ts 会在 /api/file/$ 创建一个 API 路由
  • /routes/my-script[.]js.ts 会在 /my-script.js 创建一个 API 路由

唯一路由路径

每个路由只能有一个与之关联的 handler 文件。所以,如果你有一个名为 routes/users.ts 的文件(对应请求路径 /users),就不能再有其他也解析到同一路径的文件。例如,下面这些文件都会解析到同一路由并报错:

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

转义匹配

与普通路由一样,服务器路由可以匹配转义字符。例如,名为 routes/users[.]json.ts 的文件会在 /users.json 创建一个 API 路由。

无路径布局路由与跳出路由(Break-out Routes)

因为采用统一的路由系统,无路径布局路由和跳出路由在服务器路由中间件上也支持类似的功能。

  • 无路径布局路由可以为一组路由添加中间件
  • 跳出路由可以「跳出」父级中间件

嵌套目录 vs 文件名

在上面的例子中,你可能注意到文件命名约定很灵活,允许你混合搭配目录和文件名。这是有意为之,让你能以对应用有意义的方式组织服务器路由。更多内容可以阅读 TanStack Router 基于文件的路由指南

处理服务器路由请求

服务器路由请求默认由 Start 自动处理,或者在自定义的 src/server.ts 入口点文件中由 Start 的 createStartHandler 处理。

Start handler 负责把传入的请求匹配到对应的服务器路由,并执行合适的中间件和 handler。

如果你需要定制服务器 handler,可以创建一个自定义 handler,然后把事件传给 Start handler。见服务器入口点

定义服务器路由

服务器路由通过在 createFileRoute 调用中添加 server 属性创建。server 属性包含:

  • handlers——一个把 HTTP 方法映射到 handler 函数的对象,或者一个接收 createHandlers 的函数(用于更高级的用法)
  • middleware——可选的路由级中间件数组,应用于所有 handler
// routes/hello.ts
import { createFileRoute } from '@tanstack/react-router'

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

定义服务器路由 Handler

有两种方式定义 handler:

  • 简单 handler:直接在 handlers 对象中提供 handler 函数
  • 带中间件的 handler:使用 createHandlers 函数定义带中间件的 handler

简单 handler

对于简单的场景,可以直接在 handlers 对象中提供 handler 函数。

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

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

给特定 handler 添加中间件

对于更复杂的场景,可以给特定 handler 添加中间件。这需要使用 createHandlers 函数:

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

export const Route = createFileRoute('/hello')({
  server: {
    handlers: ({ createHandlers }) =>
      createHandlers({
        GET: {
          middleware: [loggerMiddleware],
          handler: async ({ request }) => {
            return new Response('Hello, World! from ' + request.url)
          },
        },
      }),
  },
})

给所有 handler 添加中间件

你也可以通过 server 层的 middleware 属性,添加应用于路由所有 handler 的中间件:

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

export const Route = createFileRoute('/hello')({
  server: {
    middleware: [authMiddleware, loggerMiddleware], // Applies to all handlers
    handlers: {
      GET: async ({ request }) => {
        return new Response('Hello, World! from ' + request.url)
      },
      POST: async ({ request }) => {
        const body = await request.json()
        return new Response(`Hello, ${body.name}!`)
      },
    },
  },
})

组合路由级与 handler 级中间件

你可以组合两种方式——路由级中间件会先运行,然后是 handler 级中间件:

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

export const Route = createFileRoute('/hello')({
  server: {
    middleware: [authMiddleware], // Runs first for all handlers
    handlers: ({ createHandlers }) =>
      createHandlers({
        GET: async ({ request }) => {
          return new Response('Hello, World!')
        },
        POST: {
          middleware: [validationMiddleware], // Runs after authMiddleware, only for POST
          handler: async ({ request }) => {
            const body = await request.json()
            return new Response(`Hello, ${body.name}!`)
          },
        },
      }),
  },
})

Handler 上下文

每个 HTTP 方法 handler 都会接收一个包含以下属性的对象:

  • request:传入的请求对象。更多关于 Request 对象的信息见 MDN Web Docs
  • params:包含路由动态路径参数的对象。例如,如果路由路径是 /users/$id,请求打到 /users/123,那么 params 就是 { id: '123' }。我们稍后会在这篇指南中介绍动态路径参数和通配参数。
  • context:包含请求上下文的对象。用于在中间件之间传递数据。

处理完请求后,你可以返回一个 Response 对象或 Promise<Response>,甚至可以使用 @tanstack/react-start 中的任何辅助函数来操作响应。

动态路径参数

服务器路由支持与 TanStack Router 相同的动态路径参数。例如,名为 routes/users/$id.ts 的文件会在 /users/$id 创建一个接受动态 id 参数的 API 路由。

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

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

// Visit /users/123 to see the response
// User ID: 123

你还可以在单个路由中有多个动态路径参数。例如,名为 routes/users/$id/posts/$postId.ts 的文件会在 /users/$id/posts/$postId 创建一个接受两个动态参数的 API 路由。

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

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

// Visit /users/123/posts/456 to see the response
// User ID: 123, Post ID: 456

通配/全匹配参数

服务器路由还支持路径末尾的通配参数,用 $ 后跟空来表示。例如,名为 routes/file/$.ts 的文件会在 /file/$ 创建一个接受通配参数的 API 路由。

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

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

// Visit /file/hello.txt to see the response
// File: hello.txt

处理带请求体的请求

要处理 POST 请求,可以在路由对象中添加 POST handler。handler 会把请求对象作为第一个参数接收,你可以用 request.json() 方法访问请求体。

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

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

// Send a POST request to /hello with a JSON body like { "name": "Tanner" }
// Hello, Tanner!

这也适用于其他 HTTP 方法,比如 PUTPATCHDELETE。你可以在路由对象中为这些方法添加 handler,并用相应的方法访问请求体。

要记住,request.json() 方法返回一个解析为请求体 JSON 的 Promise。你需要 await 它才能访问请求体。

这是服务器路由中处理 POST 请求的常见模式。你也可以使用 request.text()request.formData() 等其他方法来访问请求体。

返回 JSON

用 Response 对象返回 JSON 时,这是常见模式:

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

export const Route = createFileRoute('/hello')({
  server: {
    handlers: {
      GET: async ({ request }) => {
        return new Response(JSON.stringify({ message: 'Hello, World!' }), {
          headers: {
            'Content-Type': 'application/json',
          },
        })
      },
    },
  },
})

// Visit /hello to see the response
// {"message":"Hello, World!"}

使用 Response.json 辅助函数

或者你可以使用 Response.json 辅助函数,它会自动把 Content-Type 头设为 application/json 并帮你序列化 JSON 对象。

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

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

// Visit /hello to see the response
// {"message":"Hello, World!"}

返回状态码

你可以在 Response 构造函数的第二个参数中传入属性来设置响应状态码:

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

export const Route = createFileRoute('/hello')({
  server: {
    handlers: {
      GET: async ({ request, params }) => {
        const user = await findUser(params.id)
        if (!user) {
          return new Response('User not found', {
            status: 404,
          })
        }
        return Response.json(user)
      },
    },
  },
})

在这个例子中,如果用户不存在,我们返回 404 状态码。你可以用这种方法设置任何合法的 HTTP 状态码。

在响应中设置请求头

有时你可能需要在响应中设置请求头。你可以通过在 Response 构造函数的第二个参数中传一个对象来实现。

// routes/hello.ts
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/hello')({
  server: {
    handlers: {
      GET: async ({ request }) => {
        return new Response('Hello, World!', {
          headers: {
            'Content-Type': 'text/plain',
          },
        })
      },
    },
  },
})
// Visit /hello to see the response
// Hello, World!

On this page