Este conteúdo não está disponível em sua língua ainda.
JavaScript app resources can represent static browser frontends, Node.js servers, SSR frameworks, and specialized resources such as Next.js apps. The deployment decision is less about which AppHost API you started with and more about which resource owns the public HTTP surface in production.
Use this table as the starting point:
What are you deploying?
Production entrypoint
Aspire pattern
Static frontend served by a backend
Backend, API, or web server
PublishWithContainerFiles
Static frontend served by a gateway or BFF
Reverse proxy
PublishWithStaticFiles
Static frontend served by its own JavaScript resource
JavaScript app resource
PublishAsStaticWebsite
SSR or Node.js app with a built server artifact
JavaScript app resource
PublishAsNodeServer
SSR or Node.js app started by a package script
JavaScript app resource
PublishAsNpmScript
Next.js standalone app
Next.js app resource
AddNextJsApp
Static frontend hosted separately from the backend
Static host + separate API
Custom topology
This article is deployment-target agnostic. It explains the JavaScript hosting models you can use with Aspire, not the full steps for a specific deployment target.
For deployment, most JavaScript app resources should be treated as build-only resources until you choose a production serving model. Different app resource APIs provide different defaults, such as Vite conventions, direct Node.js script execution, command-driven JavaScript apps, or Next.js standalone output.
To deploy a JavaScript app, choose which resource owns the public HTTP surface in production:
Use PublishWithContainerFiles(...) when your backend or web server serves the built frontend files.
Use PublishWithStaticFiles(...) when your reverse proxy, gateway, or BFF serves the built frontend files.
Use a JavaScript publish method when the JavaScript app resource should become the deployed static website or Node.js server.
Use AddNextJsApp(...) for a Next.js app that publishes as a standalone Next.js server.
If you only add a Vite or JavaScript app and reference backend services, Aspire still needs one of these production hosting patterns to know who serves requests in deployment. During publish or deploy, Aspire validates that build-only containers are either consumed by another resource through PublishWithContainerFiles(...) or PublishWithStaticFiles(...), or converted into a deployable JavaScript resource with a JavaScript publish method. An unconsumed build-only container does not participate in deployment.
Use this shape when your backend, API, or web server is responsible for serving static frontend files in production from wwwroot, static, or a similar directory.
This shape only works if the deployed application service can actually serve those files. PublishWithContainerFiles(...) copies the built frontend assets into the destination container; it does not configure the destination app to serve them.
flowchart LR
Browser --> App["Node app<br/>serving built frontend files"]
App --> Frontend["Vite build output"]
The backend container gets larger because it contains both backend code and frontend assets.
Frontend and backend are deployed together, which is convenient when they change together but less flexible if you want to scale or release them independently.
Authentication, caching, headers, and fallback routing are handled where the backend serves the files.
This usually gives the simplest mental model: one deployed service, one public endpoint, one place to troubleshoot.
Use this shape when a reverse proxy should be the public entrypoint for your app, either as a gateway or as a BFF. In Aspire, YARP is the built-in example, but the same topology applies when you use another reverse proxy such as Nginx or Caddy.
Use PublishAsStaticWebsite when the framework produces static files and you want the JavaScript app resource to become the deployed static website. The generated container uses YARP to serve the built files and can optionally proxy API requests to a backend resource using service discovery.
Choose this shape instead of PublishWithStaticFiles(...) when you do not already have a gateway or BFF resource that should own the public route table. If you already have an explicit YARP resource for gateway or BFF behavior, keep using PublishWithStaticFiles(...) on that resource.
The build output directory that contains the static files to serve.
StripPrefix / stripPrefix
false
Whether to remove the API path prefix before forwarding to the API backend. Set to true if the API does not expect the route prefix in the request path.
TargetEndpointName / targetEndpointName
null
The endpoint name on the API resource to use for proxying. When unset, YARP uses service discovery and prefers HTTPS when available.
You can call PublishAsStaticWebsite without an API target when the static website does not need proxy routes. Add apiPath and apiTarget when the static website should also route API requests to a backend resource.
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 instead of 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 through WithReference or implicitly when you pass apiTarget to PublishAsStaticWebsite, Aspire exposes the backend URL through service-discovery environment variables. The variable name follows the pattern <RESOURCENAME>_<SCHEME> 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 mode through the framework’s dev proxy and production through YARP. This avoids VITE_* build-time variables and CORS configuration for this shape. Substitute the actual variable name your resource produces (<RESOURCENAME>_<SCHEME>) if you don’t name your backend api.
SSR or Node.js app served by its own JavaScript resource
Use this shape when the JavaScript framework output should become the deployed web server and you do not need to attach the build output to a separate backend or gateway resource.
There are two common runtime shapes:
Use PublishAsNodeServer when the build produces a self-contained server artifact that can run directly with node.
Use PublishAsNpmScript when the production server starts through a package-manager script and needs production dependencies from node_modules.
Use PublishAsNodeServer for frameworks that produce a self-contained Node.js server artifact during build, such as SvelteKit and TanStack Start. Aspire generates a runtime container that runs the built artifact directly with node.
Choose this method instead of PublishAsNpmScript when the build output does not need a production node_modules install at runtime. The resulting image can be smaller because it copies the server artifact rather than the full application with production dependencies.
The generated container sets HOST=0.0.0.0 and HOSTNAME=0.0.0.0 so the Node.js server binds to all interfaces and is reachable inside the container network.
Use PublishAsNpmScript for SSR frameworks that start production by running an npm-compatible script, such as Nuxt, Astro SSR, and Remix. Aspire generates a multi-stage Dockerfile that installs production dependencies and uses the package manager script as the container entrypoint.
Choose this method instead of PublishAsNodeServer when the production server imports packages from node_modules at runtime or the framework’s recommended production command is an npm script.
Use AddNextJsApp for Next.js apps that use standalone output. It is more specific than AddJavaScriptApp because it configures the Next.js development server port argument and publish-time standalone container shape, and it validates that the Next.js configuration enables output: "standalone".
Choose this method instead of AddJavaScriptApp(...).PublishAsNpmScript(...) for Next.js standalone deployments. Use the more general npm-script publish method for frameworks where the production server should be started through the package manager and the framework does not have a dedicated Aspire resource.
This is a natural model for many SPA teams, especially when they already think in terms of “static site + API”. It can work, but it is not the primary Aspire deployment story for AddViteApp and AddJavaScriptApp.
This shape pushes more work onto the browser/frontend boundary:
The browser now talks to a different origin, so you often need to configure CORS.
The frontend needs to know the backend URL for each environment.
Vite apps usually consume those values at build time, which means the backend URL must be known when the frontend is built or injected through a separate runtime configuration pattern.
Local Vite proxy behavior often hides these production concerns until you try to deploy.
The following example looks reasonable, but it is a trap in publish/deploy:
Example: WithEnvironment(...) / withEnvironment(...) on AddViteApp / addViteApp to set VITE_API_BASE_URL.
Associated failure: Vite usually reads VITE_* values at build time, so the deployed browser app does not learn its backend URL from the Vite resource at runtime.
Pit 2 - Switching the same value to a build arg
Example: WithBuildArg(...) / withBuildArg(...) to set the backend URL during the frontend image build.
Associated failure: the backend URL is usually not known when the frontend image is being built.
Pit 3 - Trying to wire both sides of the relationship
Example: the frontend needs the backend URL, while the backend also needs the frontend origin for CORS.
Associated failure: this creates a deployment-time cycle between the frontend and backend. In publish/deploy, the Vite resource is a build resource, not the runtime web server, so it cannot be the place where the browser discovers the backend URL.
Aspire can still orchestrate the frontend build and the backend resource, but this topology is less integrated than the backend-serves-frontend or gateway-serves-frontend shapes. If this is the model you want, plan for explicit runtime configuration and CORS management.
These are issues that aren’t always called out in framework deployment docs but matter for the corresponding publish method to actually work.
The SSR examples below assume the AppHost captures the backend endpoint with api.GetEndpoint("http") / api.getEndpoint('http') and passes it to the framework resource as API_URL. For frameworks with their own runtime-config environment variable conventions, also set the framework-specific variable.
The AppHost snippets in this section assume this shared setup:
Directory structure: Nuxt 4 places pages in app/pages/, not a root pages/ directory.
Environment variables: Nuxt maps runtimeConfig keys to env vars with a NUXT_ prefix. To pass the backend URL, set NUXT_API_URL on the resource so Nuxt sees it as runtimeConfig.apiUrl. You can also set API_URL for code that reads process.env.API_URL directly.
Server API routes: The recommended pattern for calling external APIs from Nuxt is a server API route (server/api/<name>.ts) that uses useRuntimeConfig(), consumed from a page via useAsyncData.
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.
Adapter: Use @astrojs/node so Astro produces a Node SSR build.
Pre-rendering: Astro pre-renders pages 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 confirms node_modules must be copied into the runtime image.
Adapter: The default @sveltejs/adapter-auto does not produce a deployable Node.js artifact. Install @sveltejs/adapter-node and update svelte.config.js to use it.
Server-side data: Use a +page.server.tsload function for server-side fetching. process.env.API_URL is available inside the load function.
Output shape: The build/ directory is fully self-contained, so no node_modules are required at runtime. This makes SvelteKit a good fit for PublishAsNodeServer.
Standalone output: Set output: "standalone" 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 if you need to do it manually.
Server components: Default App Router components are server components. Use async directly in the component body to fetch data; no special loader pattern is needed.
Nitro preset: Uses Nitro 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 for other deployment targets.
Server functions: Use createServerFn for server-side data loading from route loaders.
Environment variables: process.env.API_URL is available inside server functions at runtime.
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 and the node-custom-server template for production server patterns.
Port binding: Pass -- --port "$PORT" as runScriptArguments so the server listens on Aspire’s assigned port.
Node version: Qwik uses Vite 7, which requires Node 20+. Set engines.node in package.json accordingly.
Server adapter: Requires the Qwik Node adapter. 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$ for server-side data loading. Read the backend URL via process.env['API_URL'].
Vite-based: Angular 17+ uses Vite internally via @angular/build. AddViteApp works correctly because Aspire injects --port into ng serve.
Dev proxy: Angular doesn’t expose vite.config.ts. Use a proxy.conf.js (not .json) that reads process.env.API_HTTP, referenced from angular.json under serve.options.proxyConfig.
Output path: Set outputPath in angular.json to { "base": "dist", "browser": "" } so the production build writes directly to dist/ for PublishAsStaticWebsite.
Preview is not production: Both Vite 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 in vite.config.ts reading process.env.API_HTTP to forward /api/* to the backend in dev mode.
AddViteApp and AddJavaScriptApp are best thought of as development commands plus publish-output resources until you choose the production serving model:
In run mode, they start the configured development command, such as the Vite dev server with HMR.
In publish mode, they produce static frontend assets, Node.js server output, or both.
Another resource can serve those artifacts in production, or a JavaScript publish method can make the JavaScript resource publish as its own static website or Node.js server.
If a build-only JavaScript resource is not consumed by another resource, publish/deploy validation fails because that resource would not participate in the deployed app. The error looks similar to:
Build-only container resource(s) 'frontend' are not consumed by another resource and won't participate in publish or deploy. Reference them from another resource, for example using 'PublishWithContainerFiles' or 'PublishWithStaticFiles', or suppress this validation for the app by calling 'builder.Pipeline.DisableBuildOnlyContainerValidation()'.
Prefer fixing the app model by choosing one of the deployment shapes in this article. The app-wide DisableBuildOnlyContainerValidation() escape hatch exists for exceptional cases, but it should not be the normal way to deploy JavaScript frontends.
To disable the validation for the whole app, call DisableBuildOnlyContainerValidation on the pipeline:
The DisableBuildOnlyContainerValidation method is marked experimental with the ASPIREPIPELINES001 diagnostic.
AddNextJsApp is different because it represents the Next.js standalone-server publish model directly. Use it when the app is a Next.js app with output: "standalone" rather than modeling the app with AddJavaScriptApp and choosing a generic npm-script publish method.
Expecting AddViteApp or AddJavaScriptApp to be the deployed production web server without choosing a publish method.
Modeling a Next.js standalone deployment with generic npm-script publishing instead of AddNextJsApp.
Exposing the Vite resource instead of the backend, reverse proxy, or JavaScript publish resource that serves production requests.
Adding AddViteApp plus .WithReference(...) and assuming that is enough to deploy the frontend.
Using .WithEnvironment(...) on AddViteApp to pass the API URL to the deployed SPA.
Calling .WithHttpEndpoint() on AddViteApp.
Using VITE_* variables for values that must be resolved at runtime in an already-built SPA.
Adding a Vite or JavaScript app without choosing a production serving model, which causes a publish/deploy validation error for an unconsumed build-only container.