To deploy a full stack Nitro project (e.g. TanStack Start) to Azure Static Web Apps with managed Azure Functions, we are told to use the
azure-swa preset in nitro. However, if we follow Nitro’s official guide, it leads to several errors because the Azure SWA preset is not very up to date. With the default Nitro configuration, server functions also fail to work properly.In this guide, I’ll walk through how to properly set up a TanStack project for deployment on Azure Static Web Apps with working server functions.
TL;DR
We override the default
azure-swa preset with our own custom runtime, and add a routing configuration in vite.config.ts to support server functions. The final vite config file will look something like this// vite.config.ts
import { defineConfig } from 'vite'
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
import { nitro } from 'nitro/vite'
// ... more imports
const config = defineConfig({
resolve: { tsconfigPaths: true },
plugins: [
nitro(
process.env.DEPLOY_TARGET === 'azure'
? {
preset: './nitro/presets/azure-swa-custom.mjs', //original template: 'azure-swa'
azure: {
config: {
routes: [
{
route: '/_serverFn/*',
rewrite: '/api/server',
},
],
},
},
}
: undefined,
),
tanstackStart(),
// ... more plugins
],
})
export default configA full project starting point template can be found here: 

tanstack-azure-swa-template
Github
tanstack-azure-swa-template
Owner
Xiang-CHUpdated
Apr 14, 2026Project Setup
First create a nitro directory in the project root with the following structure.

The
GitHubnitro/src/presets/azure/runtime at main · nitrojs/nitro
azure-swa runtime files can be found in the nitro code base here, and we will modify it in the following steps.nitro/src/presets/azure/runtime at main · nitrojs/nitro
Next Generation Server Toolkit. Create web servers with everything you need and deploy them wherever you prefer. - nitrojs/nitro
The
azure-swa-custom.mjs file contains the following, where we extend the original preset to use our modified runtime.//azure-swa-custom.mjs
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
const __dirname = dirname(fileURLToPath(import.meta.url))
const runtimeEntry = resolve(__dirname, '../runtime/azure-swa.mjs')
export default {
extends: 'azure-swa',
entry: runtimeEntry
}Then we update the nitro config to use this new custom preset.
// vite.config.ts
...
const config = defineConfig({
resolve: { tsconfigPaths: true },
plugins: [
nitro(
process.env.DEPLOY_TARGET === 'azure'
? {
// preset: 'azure-swa',
preset: './nitro/presets/azure-swa-custom.mjs',
}
: undefined,
),
// ... more plugins
],
})
export default configLastly we update the
package.json scripts to build and preview with the Azure SWA environment.// package.json
{
"scripts": {
"dev": "vite dev --port 3000",
"build": "vite build",
"build:azure": "DEPLOY_TARGET=azure vite build",
"preview": "vite preview",
"preview:azure": "npx @azure/static-web-apps-cli start .output/public --api-location .output/server",
"test": "vitest run"
},
...
}Now we can build the project for Azure SWA with command
pnpm build:azure and preview locally in a Azure SWA sandbox with pnpm preview:azureTrick when running preview:azure (@azure/static-web-apps-cli)
- The CLI automatically starts a azure function server, but it prompt you to select a runtime.

- However just inputing 3 does not work, because how the CLI renders.
- This tick is to press 3 and enter at the same time, and try multiple times if it didn’t work a first.

Problems when deploying
500 Internal server error
What broke: When we run the build project using the original
azure-swa preset the first problem is that that the server returns a 500 error when we access any page.
Why: The original runtime passes a
url variable that captures the relative path of the request to instantiate a new Request function. However in a Node.js environment, Request expects a absolute URL.// https://github.com/nitrojs/nitro/blob/main/src/presets/azure/runtime/azure-swa.ts
export async function handle(context: { res: HttpResponse }, req: HttpRequest) {
let url: string;
if (req.headers["x-ms-original-url"]) {
const parsedURL = parseURL(req.headers["x-ms-original-url"]);
url = parsedURL.pathname + parsedURL.search;
} else {
url = "/api/" + (req.params.url || "");
}
const request = new Request(url, { // URL here is relative
method: req.method || undefined,
body: req.bufferBody ?? req.rawBody,
});
...
}The fix: Create a new helper function to resolve the request host and build a absolute URL. Since we are here we can also pass the request header down to our app as well.
// https://github.com/Xiang-CH/tanstack-azure-swa-template/blob/main/nitro/runtime/azure-swa.mjs
function resolveBaseUrl(req) {
const forwardedProto = req.headers['x-forwarded-proto']
const forwardedHost = req.headers['x-forwarded-host']
const host = forwardedHost || req.headers['host']
if (host) {
return `${forwardedProto || 'http'}://${host}`
}
const originalUrl = req.headers['x-ms-original-url']
if (originalUrl) {
try {
return new URL(originalUrl).origin
} catch {
// ignore invalid original URL
}
}
return 'http://localhost'
}
export async function handle(context: { res: HttpResponse }, req: HttpRequest) {
...
const request = new Request(new URL(url, resolveBaseUrl(req)), {
method: req.method || undefined,
headers: new Headers(req.headers),
body: req.bufferBody ?? req.rawBody,
})
...
}Now with this fixed the page loads with out a server error.
Page return empty object
What broke: Even though the page now loads, it return a empty object like below, which isn’t the expected behavior of our app.

Why: Frameworks like Tanstack except a Response body to be a Buffer, but in the original runtime it return the body as it is, which is in ReadableStream format.
// https://github.com/nitrojs/nitro/blob/main/src/presets/azure/runtime/azure-swa.ts
export async function handle(context: { res: HttpResponse }, req: HttpRequest) {
...
context.res = {
status: response.status,
body: response.body,
cookies: getAzureParsedCookiesFromHeaders(response.headers),
headers: Object.fromEntries(
[...response.headers.entries()].filter(([key]) => key !== "set-cookie")
),
} satisfies HttpResponseSimple;
}The fix: Simply convert the body to a Buffer.
// https://github.com/Xiang-CH/tanstack-azure-swa-template/blob/main/nitro/runtime/azure-swa.mjs
export async function handle(context: { res: HttpResponse }, req: HttpRequest) {
...
const body = response.body
? Buffer.from(await response.arrayBuffer())
: undefined
context.res = {
status: response.status,
body,
cookies: getAzureParsedCookiesFromHeaders(response.headers),
headers: Object.fromEntries(
[...response.headers.entries()].filter(([key]) => key !== 'set-cookie'),
),
}
}Now the app should load as expected.

Server function error
What broke: If we have a server function is our app, we may see a 405 error when we interact with this function.
Why: This indicated that the server function path isn’t actually routed to
/api/server which is were the Azure function lives.
Even though the original preset already have the following route mapping:
{
"route": "/",
"rewrite": "/api/server"
}It might be that it ignored _underscored route like
/_serverFn/* which is use for tanstack server functions. The fix: Add another route rewrite in the nitro config.
// vite.config.ts
...
const config = defineConfig({
resolve: { tsconfigPaths: true },
plugins: [
nitro(
process.env.DEPLOY_TARGET === 'azure'
? {
preset: './nitro/presets/azure-swa-custom.mjs',
azure: {
config: {
routes: [
{
route: '/_serverFn/*', // change to the server function route of your framework
rewrite: '/api/server',
},
],
},
},
}
: undefined,
),
tanstackStart(),
// ... more plugins
],
})
export default configFinally everything work as expected!
Summary
- Custom Runtime: Override the default nitro
azure-swapreset
- Request URL Patch: Force absolute URLs in the Request object to stop those 500 errors
- Buffer Conversion: Convert response bodies to Buffers to fix the empty object rendering bug
- Route Rewrites: Map
/_serverFn/*to your Azure function endpoint so server functions actually resolve
I have also submitted a PR to nitro, if this gets merge, hopefully in the future we can simply use the
azure-swa preset.