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 config
A full project starting point template can be found here:
tanstack-azure-swa-template
Xiang-CH • Updated Apr 13, 2026
Project Setup
First create a nitro directory in the project root with the following structure.

The
azure-swa runtimes file can be found in the nitro code base here, and we will modify it in the following steps.-
GitHubnitro/src/presets/azure/runtime at main · nitrojs/nitronitro/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 config
Lastly 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 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 inputing 3 just 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 when access any page is that that the server returns a 500 error.
Why: The original runtime pass 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: We 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 error.
Page return empty object
What broke: Even though the page now load 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 see that we get 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', //original template: 'azure-swa' azure: { config: { routes: [ { route: '/_serverFn/*', rewrite: '/api/server', }, ], }, }, } : undefined, ), tanstackStart(), // ... more plugins ], }) export default config
Finally 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.