From 466a849155d973e375d11131766f372f5e8d47b7 Mon Sep 17 00:00:00 2001 From: David Fowler Date: Thu, 30 Apr 2026 09:41:11 -0700 Subject: [PATCH 1/3] Document framework-specific gotchas for JS publish methods Expands the JavaScript apps deployment doc with framework-specific guidance covering the three new publish methods (PublishAsStaticWebsite, PublishAsNodeServer, PublishAsNpmScript) added in Aspire 13.3. - Add a Dev-mode API proxying subsection covering the API_HTTP injection and per-framework dev-server proxy patterns (Vite, Astro, Angular). - Expand the framework reference table with build entry points and the framework-side configuration each publish method requires. - Add a Framework-specific gotchas section with verified links to the canonical framework docs for Nuxt, Astro SSR, SvelteKit, Next.js, TanStack Start, Remix / React Router, Qwik City, Angular, and Vite. - Cross-link from AddNextJsApp and AddViteApp on the JavaScript integration page to the relevant deployment-page sections. - Fix a pre-existing broken link to /fundamentals/service-discovery/overview/ introduced in #765. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../docs/deployment/javascript-apps.mdx | 100 +++++++++++++++--- .../integrations/frameworks/javascript.mdx | 8 ++ 2 files changed, 93 insertions(+), 15 deletions(-) diff --git a/src/frontend/src/content/docs/deployment/javascript-apps.mdx b/src/frontend/src/content/docs/deployment/javascript-apps.mdx index 6a099acd9..18804f633 100644 --- a/src/frontend/src/content/docs/deployment/javascript-apps.mdx +++ b/src/frontend/src/content/docs/deployment/javascript-apps.mdx @@ -475,7 +475,7 @@ Aspire 13.3 introduces three dedicated publish extension methods on JavaScript r Use `PublishAsStaticWebsite` to deploy a built static frontend via a YARP reverse proxy container. This is the recommended method for static-output frameworks such as Vite, React, Vue, and Astro (static mode). -YARP serves the built static files directly and can optionally proxy API requests to a backend service using [service discovery](/fundamentals/service-discovery/overview/). +YARP serves the built static files directly and can optionally proxy API requests to a backend service using [service discovery](/fundamentals/service-discovery/). ```csharp title="C# — AppHost.cs" #pragma warning disable ASPIREEXTENSION001 @@ -515,6 +515,16 @@ The two overloads are: `StripPrefix` defaults to `false`, which means the full request path (including the prefix) is forwarded to the backend. Set `StripPrefix = true` if the backend API does not expect the path prefix. ::: +#### Dev-mode API proxying + +`PublishAsStaticWebsite` only takes effect at publish time. In **run mode**, each framework still uses its own dev server, and the browser hits the dev server's origin — not YARP. To keep the same `/api/*` shape working in development, Aspire injects an `API_HTTP` environment variable on the frontend resource that points at the backend. Each framework needs a small dev-server proxy config that reads that variable: + +- **Vite, React, Vue**: add `server.proxy` in `vite.config.ts` and read `process.env.API_HTTP`. +- **Astro**: add `vite.server.proxy` in `astro.config.mjs` and read `process.env.API_HTTP`. +- **Angular**: add a `proxy.conf.js` (not `.json`) that reads `process.env.API_HTTP`, then reference it from `angular.json` under `serve.options.proxyConfig`. + +Once that's in place, `/api/*` resolves to the backend in both dev (via the framework's dev proxy) and production (via YARP) — no `VITE_*` build-time variables, no CORS configuration. + ### `PublishAsNodeServer` Use `PublishAsNodeServer` for SSR frameworks that produce a self-contained Node.js server artifact, such as SvelteKit and TanStack Start. @@ -569,20 +579,80 @@ Use this method for frameworks where: ### Framework reference -The following table summarizes which publish method fits each common framework: - -| Framework | Recommended method | -|---|---| -| Vite | `PublishAsStaticWebsite` | -| React (CRA / Vite) | `PublishAsStaticWebsite` | -| Vue | `PublishAsStaticWebsite` | -| Astro (static output) | `PublishAsStaticWebsite` | -| SvelteKit | `PublishAsNodeServer` | -| TanStack Start | `PublishAsNodeServer` | -| Next.js | `AddNextJsApp` (uses standalone output) | -| Nuxt | `PublishAsNpmScript` | -| Astro SSR | `PublishAsNpmScript` | -| Remix | `PublishAsNpmScript` | +The following table summarizes which publish method fits each common framework, the build output it expects, and any framework-side configuration required: + +| Framework | Recommended method | Entry point | Configuration required | +|---|---|---|---| +| Vite / React / Vue | `PublishAsStaticWebsite` | N/A (YARP serves `dist/`) | None | +| Angular | `PublishAsStaticWebsite` | N/A (YARP serves `dist/`) | `outputPath` in `angular.json` so the build writes to `dist/` directly | +| Astro (static output) | `PublishAsStaticWebsite` | N/A (YARP serves `dist/`) | None | +| SvelteKit | `PublishAsNodeServer` | `build/index.js` | `@sveltejs/adapter-node` | +| TanStack Start | `PublishAsNodeServer` | `.output/server/index.mjs` | None (Nitro `node-server` preset by default) | +| Next.js | `AddNextJsApp` | `server.js` (in `.next/standalone/`) | `output: "standalone"` in `next.config.*` | +| Nuxt | `PublishAsNpmScript` | `node .output/server/index.mjs` (via `start`) | `NUXT_` prefix on `runtimeConfig` env vars | +| Astro SSR | `PublishAsNpmScript` | `node ./dist/server/entry.mjs` (via `start`) | `@astrojs/node`, `prerender: false` per page | +| Remix / React Router | `PublishAsNpmScript` | `react-router-serve` (via `start`) | None | +| Qwik City | `PublishAsNpmScript` | `node server/entry.node-server.js` (via `start`) | Node server adapter, Node 20+ | + +### Framework-specific gotchas + +These are issues that aren't always called out in framework deployment docs but matter for the corresponding publish method to actually work. + +#### Nuxt + +- **Directory structure**: Nuxt 4 places pages in `app/pages/`, not a root `pages/` directory. +- **Environment variables**: Nuxt maps [`runtimeConfig`](https://nuxt.com/docs/getting-started/configuration#runtime-config) keys to env vars with a `NUXT_` prefix. To pass `apiUrl` at runtime, set `NUXT_API_URL` on the resource — not `API_URL`. +- **Server API routes**: The recommended pattern for calling external APIs from Nuxt is a [server API route](https://nuxt.com/docs/guide/directory-structure/server) (`server/api/.ts`) that uses `useRuntimeConfig()`, consumed from a page via [`useAsyncData`](https://nuxt.com/docs/api/composables/use-async-data). +- **Publish method**: Always use `PublishAsNpmScript` for Nuxt. The Nitro `.output/` looks self-contained, but server-side data fetching via `useAsyncData` / `useFetch` fails without the full `node_modules` available at runtime. + +#### Astro SSR + +- **Adapter**: Use [`@astrojs/node`](https://docs.astro.build/en/guides/integrations-guide/node/) so Astro produces a Node SSR build. +- **Pre-rendering**: Astro [pre-renders pages](https://docs.astro.build/en/guides/on-demand-rendering/) at build time by default, even with the Node adapter. Add `export const prerender = false` to any page that needs to run at request time. +- **Environment variables**: Use `process.env.API_URL`, not `import.meta.env.API_URL`. `import.meta.env` values are resolved at build time and baked into the output. +- **Runtime dependencies**: The built `entry.mjs` imports unbundled `@astrojs/*` packages, so SSR Astro must use `PublishAsNpmScript`. The [official Docker recipe](https://docs.astro.build/en/recipes/docker/#multi-stage-build-using-ssr) confirms `node_modules` must be copied into the runtime image. + +#### SvelteKit + +- **Adapter**: The default `@sveltejs/adapter-auto` does not produce a deployable Node.js artifact. Install [`@sveltejs/adapter-node`](https://svelte.dev/docs/kit/adapter-node) and update `svelte.config.js` to use it. +- **Server-side data**: Use a [`+page.server.ts`](https://svelte.dev/docs/kit/load) `load` function for server-side fetching. `process.env.API_URL` is available inside the load function. +- **Output shape**: The `build/` directory is fully self-contained — no `node_modules` are required at runtime, which makes SvelteKit a good fit for `PublishAsNodeServer`. + +#### Next.js + +- **Standalone output**: Set [`output: "standalone"`](https://nextjs.org/docs/app/api-reference/config/next-config-js/output) in `next.config.*`. Without this, the build output requires `node_modules` at runtime and the generated container won't run. `AddNextJsApp` validates this configuration at deploy time. +- **Copy shape**: The standalone build produces three directories that must be copied separately into the runtime image: `.next/standalone/` (server + bundled deps), `.next/static/` (client assets), and `public/` (static files). `AddNextJsApp` handles this automatically; see the [official with-docker example](https://github.com/vercel/next.js/tree/canary/examples/with-docker) if you need to do it manually. +- **Server components**: Default App Router components are [server components](https://nextjs.org/docs/app/getting-started/server-and-client-components). Use `async` directly in the component body to fetch data — no special loader pattern needed. + +#### TanStack Start + +- **Nitro preset**: Uses [Nitro](https://nitro.build/deploy/runtimes/node) with the `node-server` preset by default, which produces a self-contained `.output/server/index.mjs`. This is why TanStack Start works with `PublishAsNodeServer` out of the box. See [TanStack Start hosting](https://tanstack.com/start/latest/docs/framework/react/hosting) for other deployment targets. +- **Server functions**: Use [`createServerFn`](https://tanstack.com/start/latest/docs/framework/react/server-functions) for server-side data loading from route loaders. +- **Environment variables**: `process.env.API_URL` is available inside server functions at runtime. + +#### Remix / React Router + +- **Server binary**: `react-router-serve` lives in `node_modules` — it's not bundled into the build output. This is why Remix needs `PublishAsNpmScript` rather than `PublishAsNodeServer`. See the [React Router deployment guide](https://reactrouter.com/start/framework/deploying) and the [`node-custom-server` template](https://github.com/remix-run/react-router-templates/tree/main/node-custom-server) for production server patterns. +- **Port binding**: Pass `-- --port "$PORT"` as `runScriptArguments` so the server listens on Aspire's assigned port. + +#### Qwik City + +- **Node version**: Qwik uses Vite 7, which requires Node 20+. Set `engines.node` in `package.json` accordingly. +- **Server adapter**: Requires the [Qwik Node adapter](https://qwik.dev/docs/deployments/node/). Add `adaptors/node-server/vite.config.ts` with `nodeServerAdapter()` and a corresponding `src/entry.node-server.tsx`. +- **Build steps**: Requires both `npm run build.client` and `npm run build.server`. The default `npm run build` runs both via `qwik build`. +- **SSR data loading**: Use [`routeLoader$`](https://qwik.dev/docs/route-loader/) for server-side data loading. Read the backend URL via `process.env['API_URL']`. + +#### Angular + +- **Vite-based**: Angular 17+ uses Vite internally via `@angular/build`. `AddViteApp` works correctly — Aspire injects `--port` into `ng serve`. +- **Dev proxy**: Angular doesn't expose `vite.config.ts`. Use a [`proxy.conf.js`](https://angular.dev/tools/cli/serve) (not `.json`) that reads `process.env.API_HTTP`, referenced from `angular.json` under `serve.options.proxyConfig`. +- **Output path**: Set [`outputPath`](https://angular.dev/reference/configs/workspace-config) in `angular.json` to `{ "base": "dist", "browser": "" }` so the production build writes directly to `dist/` for `PublishAsStaticWebsite`. + +#### Vite / React / Vue (static) + +- **Preview is not production**: Both [Vite](https://vite.dev/guide/cli.html#vite-preview) and the framework docs explicitly state that `vite preview` is not a production server. Always use `PublishAsStaticWebsite`. +- **API calls**: Use the `apiPath` / `apiTarget` options on `PublishAsStaticWebsite` so the backend is reachable through YARP. Don't use `VITE_*` env vars for runtime API URLs — they're baked at build time. +- **Dev proxy**: Add [`server.proxy`](https://vite.dev/config/server-options.html#server-proxy) in `vite.config.ts` reading `process.env.API_HTTP` to forward `/api/*` to the backend in dev mode. For setting up `AddNextJsApp` with its deploy-time `output: "standalone"` validation, see [JavaScript integration — Add Next.js application](/integrations/frameworks/javascript/#add-nextjs-application). diff --git a/src/frontend/src/content/docs/integrations/frameworks/javascript.mdx b/src/frontend/src/content/docs/integrations/frameworks/javascript.mdx index 5236b8642..1e4456415 100644 --- a/src/frontend/src/content/docs/integrations/frameworks/javascript.mdx +++ b/src/frontend/src/content/docs/integrations/frameworks/javascript.mdx @@ -141,6 +141,10 @@ var nextApp = builder.AddNextJsApp("next-app", "./next-app") .DisableBuildValidation(); ``` + + For Next.js publish-method requirements (standalone output, copy shape, server components), see [Deploy JavaScript apps — Next.js gotchas](/deployment/javascript-apps/#nextjs). + + ## Add Vite application For Vite applications, you can use the `AddViteApp` extension method which provides Vite-specific defaults and optimizations: @@ -179,6 +183,10 @@ The method accepts the same parameters as `AddJavaScriptApp`: - **appDirectory**: The path to the directory containing the Vite app. - **runScriptName** (optional): The name of the script that runs the Vite app. Defaults to "dev". + + For framework-specific publish guidance — Vite/React/Vue, Angular, Astro, SvelteKit, TanStack Start, Nuxt, Remix, and Qwik — see [Deploy JavaScript apps — Framework-specific gotchas](/deployment/javascript-apps/#framework-specific-gotchas). + + ## Configure package managers Aspire automatically detects and supports multiple JavaScript package managers with intelligent defaults for both development and production scenarios. From a4aee7a24aa8a3c9a251470a3323bc5e2ae3adc7 Mon Sep 17 00:00:00 2001 From: David Fowler Date: Thu, 30 Apr 2026 12:12:31 -0700 Subject: [PATCH 2/3] Clarify API_HTTP and API_URL env var conventions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address PR #787 review feedback: - Explain that API_HTTP follows the service-discovery _ convention and is added by WithReference (or by passing apiTarget to PublishAsStaticWebsite). - Clarify that API_URL is not auto-injected — it is a custom env var that SSR-framework examples set explicitly via WithEnvironment. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/content/docs/deployment/javascript-apps.mdx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/frontend/src/content/docs/deployment/javascript-apps.mdx b/src/frontend/src/content/docs/deployment/javascript-apps.mdx index 18804f633..dfa1355c7 100644 --- a/src/frontend/src/content/docs/deployment/javascript-apps.mdx +++ b/src/frontend/src/content/docs/deployment/javascript-apps.mdx @@ -517,13 +517,17 @@ The two overloads are: #### Dev-mode API proxying -`PublishAsStaticWebsite` only takes effect at publish time. In **run mode**, each framework still uses its own dev server, and the browser hits the dev server's origin — not YARP. To keep the same `/api/*` shape working in development, Aspire injects an `API_HTTP` environment variable on the frontend resource that points at the backend. Each framework needs a small dev-server proxy config that reads that variable: +`PublishAsStaticWebsite` only takes effect at publish time. In **run mode**, each framework still uses its own dev server, and the browser hits the dev server's origin — not YARP. To keep the same `/api/*` shape working in development, the dev server itself needs a small proxy config that forwards `/api/*` to the backend resource. + +When the frontend references a backend resource — either explicitly via `WithReference` or implicitly when you pass `apiTarget` to `PublishAsStaticWebsite` — Aspire exposes the backend's URL through service-discovery environment variables. The variable name follows the pattern `_` in upper case. For a backend resource named `api` with an `http` endpoint, that's `API_HTTP`. If you rename the resource (for example `weather`) or use `https`, the variable becomes `WEATHER_HTTP` or `API_HTTPS`. `apiTarget` adds the reference for you, so no extra `WithReference` call is required when you use `PublishAsStaticWebsite(apiPath, apiTarget)`. + +Each framework reads that variable from its own dev-server config: - **Vite, React, Vue**: add `server.proxy` in `vite.config.ts` and read `process.env.API_HTTP`. - **Astro**: add `vite.server.proxy` in `astro.config.mjs` and read `process.env.API_HTTP`. - **Angular**: add a `proxy.conf.js` (not `.json`) that reads `process.env.API_HTTP`, then reference it from `angular.json` under `serve.options.proxyConfig`. -Once that's in place, `/api/*` resolves to the backend in both dev (via the framework's dev proxy) and production (via YARP) — no `VITE_*` build-time variables, no CORS configuration. +Once that's in place, `/api/*` resolves to the backend in both dev (via the framework's dev proxy) and production (via YARP) — no `VITE_*` build-time variables, no CORS configuration. Substitute the actual variable name your resource produces (`_`) if you don't name your backend `api`. ### `PublishAsNodeServer` @@ -598,6 +602,8 @@ The following table summarizes which publish method fits each common framework, These are issues that aren't always called out in framework deployment docs but matter for the corresponding publish method to actually work. +For SSR frameworks that fetch from the backend at request time, the examples below assume an explicit `WithEnvironment("API_URL", api.GetEndpoint("http"))` on the frontend resource. `API_URL` is not a built-in Aspire convention — it's a custom variable name that the framework code reads. If you'd rather use the service-discovery variable described in [Dev-mode API proxying](#dev-mode-api-proxying) above, read `_` (for example `API_HTTP`) instead. + #### Nuxt - **Directory structure**: Nuxt 4 places pages in `app/pages/`, not a root `pages/` directory. From 0f133c885a7acb7c3544d2f66ec46a2f03b27c0e Mon Sep 17 00:00:00 2001 From: David Fowler Date: Thu, 30 Apr 2026 12:17:46 -0700 Subject: [PATCH 3/3] Use API_HTTP consistently; drop fictional API_URL references MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit API_URL is not an Aspire convention — it was a custom variable used in the sample repo. Use the actual auto-injected service-discovery variable API_HTTP (_) throughout the SSR gotchas so readers can reproduce the examples without an extra WithEnvironment call. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/content/docs/deployment/javascript-apps.mdx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/frontend/src/content/docs/deployment/javascript-apps.mdx b/src/frontend/src/content/docs/deployment/javascript-apps.mdx index dfa1355c7..e4462a87c 100644 --- a/src/frontend/src/content/docs/deployment/javascript-apps.mdx +++ b/src/frontend/src/content/docs/deployment/javascript-apps.mdx @@ -602,12 +602,12 @@ The following table summarizes which publish method fits each common framework, These are issues that aren't always called out in framework deployment docs but matter for the corresponding publish method to actually work. -For SSR frameworks that fetch from the backend at request time, the examples below assume an explicit `WithEnvironment("API_URL", api.GetEndpoint("http"))` on the frontend resource. `API_URL` is not a built-in Aspire convention — it's a custom variable name that the framework code reads. If you'd rather use the service-discovery variable described in [Dev-mode API proxying](#dev-mode-api-proxying) above, read `_` (for example `API_HTTP`) instead. +The SSR examples below assume a backend resource named `api` referenced from the frontend (via `WithReference` or by passing `apiTarget` to `PublishAsStaticWebsite`). Aspire then exposes its URL as `API_HTTP` following the `_` convention from [Dev-mode API proxying](#dev-mode-api-proxying). If your backend has a different name or scheme, substitute the matching variable. #### Nuxt - **Directory structure**: Nuxt 4 places pages in `app/pages/`, not a root `pages/` directory. -- **Environment variables**: Nuxt maps [`runtimeConfig`](https://nuxt.com/docs/getting-started/configuration#runtime-config) keys to env vars with a `NUXT_` prefix. To pass `apiUrl` at runtime, set `NUXT_API_URL` on the resource — not `API_URL`. +- **Environment variables**: Nuxt maps [`runtimeConfig`](https://nuxt.com/docs/getting-started/configuration#runtime-config) keys to env vars with a `NUXT_` prefix. To pass the backend URL, set `NUXT_API_HTTP` on the resource so Nuxt sees it as `runtimeConfig.apiHttp`. - **Server API routes**: The recommended pattern for calling external APIs from Nuxt is a [server API route](https://nuxt.com/docs/guide/directory-structure/server) (`server/api/.ts`) that uses `useRuntimeConfig()`, consumed from a page via [`useAsyncData`](https://nuxt.com/docs/api/composables/use-async-data). - **Publish method**: Always use `PublishAsNpmScript` for Nuxt. The Nitro `.output/` looks self-contained, but server-side data fetching via `useAsyncData` / `useFetch` fails without the full `node_modules` available at runtime. @@ -615,13 +615,13 @@ For SSR frameworks that fetch from the backend at request time, the examples bel - **Adapter**: Use [`@astrojs/node`](https://docs.astro.build/en/guides/integrations-guide/node/) so Astro produces a Node SSR build. - **Pre-rendering**: Astro [pre-renders pages](https://docs.astro.build/en/guides/on-demand-rendering/) at build time by default, even with the Node adapter. Add `export const prerender = false` to any page that needs to run at request time. -- **Environment variables**: Use `process.env.API_URL`, not `import.meta.env.API_URL`. `import.meta.env` values are resolved at build time and baked into the output. +- **Environment variables**: Use `process.env.API_HTTP`, not `import.meta.env.API_HTTP`. `import.meta.env` values are resolved at build time and baked into the output. - **Runtime dependencies**: The built `entry.mjs` imports unbundled `@astrojs/*` packages, so SSR Astro must use `PublishAsNpmScript`. The [official Docker recipe](https://docs.astro.build/en/recipes/docker/#multi-stage-build-using-ssr) confirms `node_modules` must be copied into the runtime image. #### SvelteKit - **Adapter**: The default `@sveltejs/adapter-auto` does not produce a deployable Node.js artifact. Install [`@sveltejs/adapter-node`](https://svelte.dev/docs/kit/adapter-node) and update `svelte.config.js` to use it. -- **Server-side data**: Use a [`+page.server.ts`](https://svelte.dev/docs/kit/load) `load` function for server-side fetching. `process.env.API_URL` is available inside the load function. +- **Server-side data**: Use a [`+page.server.ts`](https://svelte.dev/docs/kit/load) `load` function for server-side fetching. `process.env.API_HTTP` is available inside the load function. - **Output shape**: The `build/` directory is fully self-contained — no `node_modules` are required at runtime, which makes SvelteKit a good fit for `PublishAsNodeServer`. #### Next.js @@ -634,7 +634,7 @@ For SSR frameworks that fetch from the backend at request time, the examples bel - **Nitro preset**: Uses [Nitro](https://nitro.build/deploy/runtimes/node) with the `node-server` preset by default, which produces a self-contained `.output/server/index.mjs`. This is why TanStack Start works with `PublishAsNodeServer` out of the box. See [TanStack Start hosting](https://tanstack.com/start/latest/docs/framework/react/hosting) for other deployment targets. - **Server functions**: Use [`createServerFn`](https://tanstack.com/start/latest/docs/framework/react/server-functions) for server-side data loading from route loaders. -- **Environment variables**: `process.env.API_URL` is available inside server functions at runtime. +- **Environment variables**: `process.env.API_HTTP` is available inside server functions at runtime. #### Remix / React Router @@ -646,7 +646,7 @@ For SSR frameworks that fetch from the backend at request time, the examples bel - **Node version**: Qwik uses Vite 7, which requires Node 20+. Set `engines.node` in `package.json` accordingly. - **Server adapter**: Requires the [Qwik Node adapter](https://qwik.dev/docs/deployments/node/). Add `adaptors/node-server/vite.config.ts` with `nodeServerAdapter()` and a corresponding `src/entry.node-server.tsx`. - **Build steps**: Requires both `npm run build.client` and `npm run build.server`. The default `npm run build` runs both via `qwik build`. -- **SSR data loading**: Use [`routeLoader$`](https://qwik.dev/docs/route-loader/) for server-side data loading. Read the backend URL via `process.env['API_URL']`. +- **SSR data loading**: Use [`routeLoader$`](https://qwik.dev/docs/route-loader/) for server-side data loading. Read the backend URL via `process.env['API_HTTP']`. #### Angular