服务器路由

服务器路由是 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 路由。

无布局路由和 breakout 路由

由于统一的路由系统,无布局路由和 breakout 路由支持用于围绕服务器路由中间件的类似功能。

  • 无布局路由可用于为一组路由添加中间件
  • Breakout 路由可用于“打破”父中间件

嵌套目录 vs 文件名

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

处理服务器路由请求

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

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

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

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

Start handler 负责将传入的请求匹配到服务器路由并执行相应的中间件和处理程序。

请记住,如果你需要自定义服务器处理程序,可以通过创建一个自定义处理程序,然后将事件传递给 Start handler 来实现。

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

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

  return startHandler(event)
})
// server.ts
import { createStartHandler } from '@tanstack/solid-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/solid-start 中的任何辅助函数来操作响应。

动态路径参数

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

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() 方法返回一个 Promise,该 Promise 解析为请求的解析后的 JSON 主体。你需要 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/solid-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/solid-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/solid-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/solid-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/solid-start/server` 的 `setResponseStatus` 辅助函数

    ts
    // routes/hello.ts
    import { json } from '@tanstack/solid-start'
    import { setResponseStatus } from '@tanstack/solid-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/solid-start'
    import { setResponseStatus } from '@tanstack/solid-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/solid-start/server` 的 `setHeaders` 辅助函数。

    ts
    // routes/hello.ts
    import { setHeaders } from '@tanstack/solid-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/solid-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

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