json
helper function// TODO: Add redirect from api-routes to server-routes
Server routes are a powerful feature of TanStack Start that allow you to create server-side endpoints in your application and are useful for handling raw HTTP requests, form submissions, user authentication, and much more.
Server routes can be defined in your ./src/routes directory of your project right alongside your TanStack Router routes and are automatically handled by the TanStack Start server.
Here's what a simple server route looks like:
// 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!')
},
})
Because server routes can be defined in the same directory as your app routes, you can even use the same file for both!
// 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({
component: HelloComponent,
})
function HelloComponent() {
const [reply, setReply] = useState('')
return (
<div>
<button
onClick={() => {
fetch('/api/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({
component: HelloComponent,
})
function HelloComponent() {
const [reply, setReply] = useState('')
return (
<div>
<button
onClick={() => {
fetch('/api/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>
)
}
Server routes in TanStack Start, follow the same file-based routing conventions as TanStack Router. This means that each file in your routes directory with a ServerRoute export will be treated as an API route. Here are a few examples:
Each route can only have a single handler file associated with it. So, if you have a file named routes/users.ts which'd equal the request path of /api/users, you cannot have other files that'd also resolve to the same route. For example, the following files would all resolve to the same route and would error:
Just as with normal routes, server routes can match on escaped characters. For example, a file named routes/users[.]json.ts will create an API route at /api/users[.]json.
Because of the unified routing system, pathless layout routes and break-out routes are supported for similar functionality around server route middleware.
In the examples above, you may have noticed that the file naming conventions are flexible and allow you to mix and match directories and file names. This is intentional and allows you to organize your Server routes in a way that makes sense for your application. You can read more about this in the TanStack Router File-based Routing Guide.
Server route requests are handled by Start's createStartHandler in your server.ts entry file.
// 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)
The start handler is responsible for matching an incoming request to a server route and executing the appropriate middleware and handler.
Remember, if you need to customize the server handler, you can do so by creating a custom handler and then passing the event to the start handler:
// 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)
})
Server routes are created by exporting a ServerRoute from a route file. The ServerRoute export should be created by calling the createServerFileRoute function. The resulting builder object can then be used to:
// 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)
},
})
There are two ways to define a handler for a server route.
For simple use cases, you can provide a handler function directly to the method.
// 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)
},
})
For more complex use cases, you can provide a handler function via the method builder object. This allows you to add middleware to the method.
// 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)
}),
}))
Each HTTP method handler receives an object with the following properties:
Once you've processed the request, you can return a Response object or Promise<Response> or even use any of the helpers from @tanstack/react-start to manipulate the response.
Server routes support dynamic path parameters in the same way as TanStack Router. For example, a file named routes/users/$id.ts will create an API route at /users/$id that accepts a dynamic id parameter.
// routes/users/$id.ts
export const ServerRoute = createServerFileRoute().methods({
GET: async ({ params }) => {
const { id } = params
return new Response(`User ID: ${id}`)
},
})
// Visit /api/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 /api/users/123 to see the response
// User ID: 123
You can also have multiple dynamic path parameters in a single route. For example, a file named routes/users/$id/posts/$postId.ts will create an API route at /api/users/$id/posts/$postId that accepts two dynamic parameters.
// 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 /api/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 /api/users/123/posts/456 to see the response
// User ID: 123, Post ID: 456
Server routes also support wildcard parameters at the end of the path, which are denoted by a $ followed by nothing. For example, a file named routes/file/$.ts will create an API route at /api/file/$ that accepts a wildcard parameter.
// routes/file/$.ts
export const ServerRoute = createServerFileRoute().methods({
GET: async ({ params }) => {
const { _splat } = params
return new Response(`File: ${_splat}`)
},
})
// Visit /api/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 /api/file/hello.txt to see the response
// File: hello.txt
To handle POST requests,you can add a POST handler to the route object. The handler will receive the request object as the first argument, and you can access the request body using the request.json() method.
// 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 /api/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 /api/hello with a JSON body like { "name": "Tanner" }
// Hello, Tanner!
This also applies to other HTTP methods like PUT, PATCH, and DELETE. You can add handlers for these methods in the route object and access the request body using the appropriate method.
It's important to remember that the request.json() method returns a Promise that resolves to the parsed JSON body of the request. You need to await the result to access the body.
This is a common pattern for handling POST requests in Server routes/ You can also use other methods like request.text() or request.formData() to access the body of the request.
When returning JSON using a Response object, this is a common pattern:
// 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 /api/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 /api/hello to see the response
// {"message":"Hello, World!"}
Or you can use the json helper function to automatically set the Content-Type header to application/json and serialize the JSON object for you.
// routes/hello.ts
import { json } from '@tanstack/react-start'
export const ServerRoute = createServerFileRoute().methods({
GET: async ({ request }) => {
return json({ message: 'Hello, World!' })
},
})
// Visit /api/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 /api/hello to see the response
// {"message":"Hello, World!"}
You can set the status code of the response by either:
Passing it as a property of the second argument to the Response constructor
// 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)
},
})
Using the setResponseStatus helper function from @tanstack/react-start/server
// 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)
},
})
In this example, we're returning a 404 status code if the user is not found. You can set any valid HTTP status code using this method.
Sometimes you may need to set headers in the response. You can do this by either:
Passing an object as the second argument to the Response constructor.
// routes/hello.ts
export const ServerRoute = createServerFileRoute().methods({
GET: async ({ request }) => {
return new Response('Hello, World!', {
headers: {
'Content-Type': 'text/plain',
},
})
},
})
// Visit /api/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 /api/hello to see the response
// Hello, World!
Or using the setHeaders helper function from @tanstack/react-start/server.
// 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!')
},
})
Your weekly dose of JavaScript news. Delivered every Monday to over 100,000 devs, for free.