服务器路由

服务器路由是 TanStack Start 的一项强大功能,允许你在应用程序中创建服务器端端点,对于处理原始 HTTP 请求、表单提交、用户认证等非常有用。

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

这是一个简单的服务器路由示例

ts
// routes/hello.ts

export const ServerRoute = createServerFileRoute().methods({
  GET: async ({ request }) => {
    return new Response('Hello, World!')
  },
})
// routes/hello.ts

export const ServerRoute = createServerFileRoute().methods({
  GET: async ({ request }) => {
    return new Response('Hello, World!')
  },
})

服务器路由和应用路由

因为服务器路由可以在与应用路由相同的目录中定义,你甚至可以为两者使用同一个文件!

tsx
// routes/hello.tsx

export const ServerRoute = createServerFileRoute().methods({
  POST: async ({ request }) => {
    const body = await request.json()
    return new Response(JSON.stringify({ message: `Hello, ${body.name}!` }))
  },
})

export const Route = createFileRoute('/hello')({
  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>
  )
}
// routes/hello.tsx

export const ServerRoute = createServerFileRoute().methods({
  POST: async ({ request }) => {
    const body = await request.json()
    return new Response(JSON.stringify({ message: `Hello, ${body.name}!` }))
  },
})

export const Route = createFileRoute('/hello')({
  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 目录中带有 ServerRoute 导出的每个文件都将被视为 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 路由

唯一路由路径

每个路由只能关联一个处理程序文件。因此,如果你有一个名为 routes/users.ts 的文件,其请求路径为 /users,则不能有其他也解析到相同路由的文件。例如,以下文件都将解析到相同路由并会报错:

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

转义匹配

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

无路径布局路由和分拆路由

由于统一路由系统,无路径布局路由和分拆路由也支持类似服务器路由中间件的功能。

  • 无路径布局路由可用于向一组路由添加中间件
  • 分拆路由可用于“跳出”父级中间件

嵌套目录与文件名

在上面的示例中,您可能已经注意到文件命名约定是灵活的,允许您混合搭配目录和文件名。这是有意为之的,允许您以适合您应用程序的方式组织服务器路由。您可以在 TanStack Router 文件路由指南中阅读更多相关信息。

处理服务器路由请求

服务器路由请求由 Start 的 createStartHandler 在您的 server.ts 入口文件中处理。

tsx
// server.ts
import {
  createStartHandler,
  defaultStreamHandler,
} from '@tanstack/react-start/server'
import { createRouter } from './router'

export default createStartHandler({
  createRouter,
})(defaultStreamHandler)
// server.ts
import {
  createStartHandler,
  defaultStreamHandler,
} from '@tanstack/react-start/server'
import { createRouter } from './router'

export default createStartHandler({
  createRouter,
})(defaultStreamHandler)

启动处理程序负责将传入请求与服务器路由匹配,并执行适当的中间件和处理程序。

请记住,如果您需要自定义服务器处理程序,可以通过创建自定义处理程序,然后将事件传递给启动处理程序来完成

tsx
// server.ts
import { createStartHandler } from '@tanstack/react-start/server'

export default defineHandler((event) => {
  const startHandler = createStartHandler({
    createRouter,
  })(defaultStreamHandler)

  return startHandler(event)
})
// server.ts
import { createStartHandler } from '@tanstack/react-start/server'

export default defineHandler((event) => {
  const startHandler = createStartHandler({
    createRouter,
  })(defaultStreamHandler)

  return startHandler(event)
})

定义服务器路由

服务器路由是通过从路由文件导出 ServerRoute 来创建的。ServerRoute 导出应通过调用 createServerFileRoute 函数创建。然后可以使用生成的构建器对象来:

  • 添加路由级别中间件
  • 为每个 HTTP 方法定义处理程序
ts
// routes/hello.ts
export const ServerRoute = createServerFileRoute().methods({
  GET: async ({ request }) => {
    return new Response('Hello, World! from ' + request.url)
  },
})
// routes/hello.ts
export const ServerRoute = createServerFileRoute().methods({
  GET: async ({ request }) => {
    return new Response('Hello, World! from ' + request.url)
  },
})

定义服务器路由处理程序

有两种方法可以为服务器路由定义处理程序。

  • 直接向方法提供处理函数
  • 通过在方法构建器对象上调用 handler 方法,以应对更高级的用例

直接向方法提供处理程序函数

对于简单的用例,您可以直接向该方法提供一个处理函数。

ts
// routes/hello.ts
export const ServerRoute = createServerFileRoute().methods({
  GET: async ({ request }) => {
    return new Response('Hello, World! from ' + request.url)
  },
})
// routes/hello.ts
export const ServerRoute = createServerFileRoute().methods({
  GET: async ({ request }) => {
    return new Response('Hello, World! from ' + request.url)
  },
})

通过方法构建器对象提供处理程序函数

对于更复杂的用例,您可以通过方法构建器对象提供处理程序函数。这允许您向该方法添加中间件。

tsx
// routes/hello.ts
export const ServerRoute = createServerFileRoute().methods((api) => ({
  GET: api.middleware([loggerMiddleware]).handler(async ({ request }) => {
    return new Response('Hello, World! from ' + request.url)
  }),
}))
// routes/hello.ts
export const ServerRoute = createServerFileRoute().methods((api) => ({
  GET: api.middleware([loggerMiddleware]).handler(async ({ request }) => {
    return new Response('Hello, World! from ' + request.url)
  }),
}))

处理程序上下文

每个 HTTP 方法处理程序接收一个包含以下属性的对象:

  • request:传入的请求对象。您可以在 MDN Web Docs 中阅读有关 Request 对象的更多信息。
  • 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 路由。

ts
// routes/users/$id.ts
export const ServerRoute = createServerFileRoute().methods({
  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.ts
export const ServerRoute = createServerFileRoute().methods({
  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 路由。

ts
// routes/users/$id/posts/$postId.ts
export const ServerRoute = createServerFileRoute().methods({
  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/users/$id/posts/$postId.ts
export const ServerRoute = createServerFileRoute().methods({
  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

通配符/Splat 参数

服务器路由也支持路径末尾的通配符参数,由一个 $ 符号后没有任何字符表示。例如,名为 routes/file/$.ts 的文件将在 /file/$ 创建一个接受通配符参数的 API 路由。

ts
// routes/file/$.ts
export const ServerRoute = createServerFileRoute().methods({
  GET: async ({ params }) => {
    const { _splat } = params
    return new Response(`File: ${_splat}`)
  },
})

// Visit /file/hello.txt to see the response
// File: hello.txt
// routes/file/$.ts
export const ServerRoute = createServerFileRoute().methods({
  GET: async ({ params }) => {
    const { _splat } = params
    return new Response(`File: ${_splat}`)
  },
})

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

处理带请求体的请求

要处理 POST 请求,您可以向路由对象添加一个 POST 处理程序。该处理程序将接收请求对象作为第一个参数,您可以使用 request.json() 方法访问请求体。

ts
// routes/hello.ts
export const ServerRoute = createServerFileRoute().methods({
  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!
// routes/hello.ts
export const ServerRoute = createServerFileRoute().methods({
  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。您可以在路由对象中为这些方法添加处理程序,并使用适当的方法访问请求体。

重要的是要记住,request.json() 方法返回一个解析为请求的解析 JSON 主体的 Promise。您需要 await 结果才能访问主体。

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

响应 JSON

当使用 Response 对象返回 JSON 时,这是一个常见的模式

ts
// routes/hello.ts
export const ServerRoute = createServerFileRoute().methods({
  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!"}
// routes/hello.ts
export const ServerRoute = createServerFileRoute().methods({
  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!"}

使用 json 辅助函数

或者您可以使用 json 辅助函数自动将 Content-Type 标头设置为 application/json 并为您序列化 JSON 对象。

ts
// routes/hello.ts
import { json } from '@tanstack/react-start'

export const ServerRoute = createServerFileRoute().methods({
  GET: async ({ request }) => {
    return json({ message: 'Hello, World!' })
  },
})

// Visit /hello to see the response
// {"message":"Hello, World!"}
// routes/hello.ts
import { json } from '@tanstack/react-start'

export const ServerRoute = createServerFileRoute().methods({
  GET: async ({ request }) => {
    return json({ message: 'Hello, World!' })
  },
})

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

响应状态码

您可以通过以下两种方式设置响应状态码:

  • 将其作为第二个参数的属性传递给 Response 构造函数

    ts
    // routes/hello.ts
    import { json } from '@tanstack/react-start'
    
    export const ServerRoute = createServerFileRoute().methods({
      GET: async ({ request, params }) => {
        const user = await findUser(params.id)
        if (!user) {
          return new Response('User not found', {
            status: 404,
          })
        }
        return json(user)
      },
    })
    
    // routes/hello.ts
    import { json } from '@tanstack/react-start'
    
    export const ServerRoute = createServerFileRoute().methods({
      GET: async ({ request, params }) => {
        const user = await findUser(params.id)
        if (!user) {
          return new Response('User not found', {
            status: 404,
          })
        }
        return json(user)
      },
    })
    
  • 使用 @tanstack/react-start/server 中的 setResponseStatus 辅助函数

    ts
    // routes/hello.ts
    import { json } from '@tanstack/react-start'
    import { setResponseStatus } from '@tanstack/react-start/server'
    
    export const ServerRoute = createServerFileRoute().methods({
      GET: async ({ request, params }) => {
        const user = await findUser(params.id)
        if (!user) {
          setResponseStatus(404)
          return new Response('User not found')
        }
        return json(user)
      },
    })
    
    // routes/hello.ts
    import { json } from '@tanstack/react-start'
    import { setResponseStatus } from '@tanstack/react-start/server'
    
    export const ServerRoute = createServerFileRoute().methods({
      GET: async ({ request, params }) => {
        const user = await findUser(params.id)
        if (!user) {
          setResponseStatus(404)
          return new Response('User not found')
        }
        return json(user)
      },
    })
    

在此示例中,如果未找到用户,我们将返回 404 状态码。您可以使用此方法设置任何有效的 HTTP 状态码。

设置响应头

有时您可能需要设置响应头。您可以通过以下两种方式完成此操作:

  • 将一个对象作为第二个参数传递给 Response 构造函数。

    ts
    // routes/hello.ts
    export const ServerRoute = createServerFileRoute().methods({
      GET: async ({ request }) => {
        return new Response('Hello, World!', {
          headers: {
            'Content-Type': 'text/plain',
          },
        })
      },
    })
    
    // Visit /hello to see the response
    // Hello, World!
    
    // routes/hello.ts
    export const ServerRoute = createServerFileRoute().methods({
      GET: async ({ request }) => {
        return new Response('Hello, World!', {
          headers: {
            'Content-Type': 'text/plain',
          },
        })
      },
    })
    
    // Visit /hello to see the response
    // Hello, World!
    
  • 或使用 @tanstack/react-start/server 中的 setHeaders 辅助函数。

    ts
    // routes/hello.ts
    import { setHeaders } from '@tanstack/react-start/server'
    
    export const ServerRoute = createServerFileRoute().methods({
      GET: async ({ request }) => {
        setHeaders({
          'Content-Type': 'text/plain',
        })
        return new Response('Hello, World!')
      },
    })
    
    // routes/hello.ts
    import { setHeaders } from '@tanstack/react-start/server'
    
    export const ServerRoute = createServerFileRoute().methods({
      GET: async ({ request }) => {
        setHeaders({
          'Content-Type': 'text/plain',
        })
        return new Response('Hello, World!')
      },
    })
    
我们的合作伙伴
Code Rabbit
Netlify
Neon
Clerk
Convex
Sentry
Prisma
订阅 Bytes

您的每周 JavaScript 资讯。每周一免费发送给超过 10 万开发者。

Bytes

无垃圾邮件。您可以随时取消订阅。

订阅 Bytes

您的每周 JavaScript 资讯。每周一免费发送给超过 10 万开发者。

Bytes

无垃圾邮件。您可以随时取消订阅。