# Deploy Full Stack Nitro Apps to Azure SWA

_2026-04-13_

> Guide on how to host Tanstack / Nitro app on Azure Static Web App by fixing the preset runtime.

Canonical URL: https://cxiang.site/blog/deploy-full-stack-nitro-apps-to-azure-swa

To deploy a full stack [Nitro](https://nitro.build/) 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](https://nitro.build/deploy/providers/azure), 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

```TypeScript
// 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](https://github.com/Xiang-CH/tanstack-azure-swa-template)

# Project Setup

First create a nitro directory in the project root with the following structure.

![](https://www.notion.so/image/attachment%3A23583705-ff4c-4ef4-8be9-d76025ad8f3b%3AScreenshot_2026-04-13_at_4.55.34_PM.png?table=block&id=341f94f7-98c1-80ac-a196-f6713577b132&cache=v2)
The `azure-swa` runtime files can be found in the nitro code base [here](https://github.com/nitrojs/nitro/tree/main/src/presets/azure/runtime), and we will modify it in the following steps.
   ‣ 
  
The `azure-swa-custom.mjs` file contains the following, where we extend the original preset to use our modified runtime.

```TypeScript
//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.

```TypeScript
// 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.

```TypeScript
// 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:azure`

<details>
<summary>Trick when running `preview:azure` (@azure/static-web-apps-cli)</summary>
- The CLI automatically starts a azure function server, but it prompt you to select a runtime.
![](https://www.notion.so/image/attachment%3Aee9755ee-f5bb-4ce9-83a9-d136045ab6e7%3AScreenshot_2026-04-13_at_5.08.04_PM.png?table=block&id=341f94f7-98c1-804a-8be5-fa79f5aec9d9&cache=v2)
- 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.
![](https://www.notion.so/image/attachment%3Af75fc654-bfc6-4c3d-9eb9-57877aeafd43%3AScreenshot_2026-04-13_at_5.11.34_PM.png?table=block&id=341f94f7-98c1-8050-93ef-e81ab08e83ff&cache=v2)
</details>
# 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.

![](https://www.notion.so/image/attachment%3A88622d40-8234-4a98-ba08-36cb2c402aab%3AScreenshot_2026-04-13_at_3.45.08_PM.png?table=block&id=341f94f7-98c1-808d-aa6c-de8a1c4a881e&cache=v2)
**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.

```TypeScript
// 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. 

```TypeScript
// 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.

![](https://www.notion.so/image/attachment%3A552a38ee-de4d-4a75-9bf4-52a60f5c6205%3AScreenshot_2026-04-13_at_4.10.31_PM.png?table=block&id=341f94f7-98c1-8054-9438-d1b7e9b98ef2&cache=v2)
**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.

```TypeScript
// 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.

```TypeScript
// 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.

![](https://www.notion.so/image/attachment%3A92d6fb64-df18-44f6-b082-0603449e055d%3AScreenshot_2026-04-13_at_4.27.53_PM.png?table=block&id=341f94f7-98c1-80a5-89b6-f7a5b685be80&cache=v2)
## 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.

![](https://www.notion.so/image/attachment%3A85b4688b-e124-41e3-9343-fa12f394f84f%3AScreenshot_2026-04-13_at_4.40.41_PM.png?table=block&id=341f94f7-98c1-8043-b188-d3ce5fb4a216&cache=v2)
Even though the original preset already have the following route mapping:

```TypeScript
{
  "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.

```TypeScript
// 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 config
```
Finally everything work as expected! 

# Summary

- **Custom Runtime:** Override the default nitro `azure-swa` preset
- **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](https://github.com/nitrojs/nitro/pull/4195) to nitro, if this gets merge, hopefully in the future we can simply use the `azure-swa` preset.


## Sitemap

See the full [sitemap](/sitemap.md) for all pages.


## Contact Information

Chen Xiang (陈想)

- Email: xiiang.ch@gmail.com
- GitHub: https://github.com/Xiang-CH
- LinkedIn: https://www.linkedin.com/in/xiang-chen-62389526a/
- Instagram: https://www.instagram.com/chen.xiiang/
- X(Twitter): https://x.com/cxiiang

This page is also available as markdown: append `.md` to the URL path (for example, `/blog/my-post.md`) or send `Accept: text/markdown` for the same path.

