@tanstack/start-client-core
Version:
Modern and scalable routing for React applications
325 lines (262 loc) • 8.29 kB
Markdown
---
name: server-routes
description: >-
Server-side API endpoints using the server property on
createFileRoute, HTTP method handlers (GET, POST, PUT, DELETE),
createHandlers for per-handler middleware, handler context
(request, params, context), request body parsing, response
helpers, file naming for API routes.
metadata:
type: sub-skill
library: tanstack-start
library_version: '1.170.14'
requires:
- start-core
sources:
- TanStack/router:docs/start/framework/react/guide/server-routes.md
---
# Server Routes
Server routes are API endpoints defined alongside app routes in the `src/routes` directory. They use the `server` property on `createFileRoute` and handle raw HTTP requests.
Use server routes when callers need an HTTP contract. For data used only by the Start application, prefer a server function and call it directly from the loader. A route loader runs during SSR and client navigation, so `fetch('/api/...')` is not a portable loader pattern.
## Basic Server Route
```ts
// 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!')
},
},
},
})
```
The same file can define both a server route and a UI route:
```tsx
// 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>
)
}
```
The relative `fetch('/hello')` above is safe because it runs only in a browser click handler.
Do not make the SSR loader call its own server route. Put the business operation in a server-only service, then expose it through both boundaries when both are required:
```tsx
// 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(),
})
```
This keeps SSR independent of URL resolution and keeps one source of business logic.
Server routes follow TanStack Router file-based routing conventions:
| File | Route |
| --------------------------- | ----------------------------- |
| `routes/users.ts` | `/users` |
| `routes/users/$id.ts` | `/users/$id` |
| `routes/users/$id/posts.ts` | `/users/$id/posts` |
| `routes/api/file/$.ts` | `/api/file/$` (splat) |
| `routes/my-script[.]js.ts` | `/my-script.js` (escaped dot) |
Each route can only have a single handler file. These would conflict:
- `routes/users.ts`
- `routes/users.index.ts`
- `routes/users/index.ts`
Each handler receives:
- `request` — the incoming [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object
- `params` — dynamic path parameters
- `context` — context from middleware
- `pathname` — the matched pathname
- `next` — call to fall through to SSR (returns a `Response`)
## Dynamic Path Params
```ts
// 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}`)
},
},
},
})
```
```ts
// 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}`)
},
},
},
})
```
```ts
export const Route = createFileRoute('/api/users')({
server: {
handlers: {
POST: async ({ request }) => {
const body = await request.json()
return Response.json({ created: body.name })
},
},
},
})
```
Other body methods: `request.text()`, `request.formData()`.
```ts
// Using Response.json helper
handlers: {
GET: async () => {
return Response.json({ message: 'Hello!' })
},
}
```
```ts
handlers: {
GET: async ({ params }) => {
const user = await findUser(params.id)
if (!user) {
return new Response('Not found', { status: 404 })
}
return Response.json(user)
},
}
```
```ts
handlers: {
GET: async () => {
return new Response('Hello', {
headers: { 'Content-Type': 'text/plain' },
})
},
}
```
```tsx
export const Route = createFileRoute('/api/admin')({
server: {
middleware: [authMiddleware, loggerMiddleware],
handlers: {
GET: async ({ context }) => Response.json(context.user),
POST: async ({ request, context }) => {
/* ... */
},
},
},
})
```
```tsx
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 })
},
},
}),
},
})
```
```tsx
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 })
},
},
}),
},
})
```
Every handler that reads or writes private data must authenticate and authorize the request through route middleware, handler middleware, or an in-handler check. A router `beforeLoad` redirect does not protect `/api/...`. Test the handler directly without cookies and assert that no private payload is returned.
```text
routes/users.ts
routes/users/index.ts
routes/users.ts
```
```ts
// WRONG — body is a Promise, not the actual data
const body = request.json()
// CORRECT — await the promise
const body = await request.json()
```
Validate request input and test the serialized `Response` output. A typed service can still be projected or serialized without a newly added field. For schema changes, assert `await response.json()` at the server-route boundary.
- [start-core/middleware](../middleware/SKILL.md) — middleware for server routes
- [start-core/server-functions](../server-functions/SKILL.md) — alternative for RPC-style calls