Deploy Full Stack Nitro Apps to Azure SWA
🖥️

Deploy Full Stack Nitro Apps to Azure SWA

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-CHUpdated Apr 13, 2026

Project Setup

First create a nitro directory in the project root with the following structure.
notion image
The azure-swa runtimes file can be found in the nitro code base here, 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.
//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:azure
Trick 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.
    • notion image
  • 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.
    • notion image

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.
notion image
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.
notion image
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.
notion image

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.
notion image
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-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 to nitro, if this gets merge, hopefully in the future we can simply use the azure-swa preset.
 

If you liked this post, you can support me by:

or
Deploy Full Stack Nitro Apps to Azure SWA | Chen Xiang