For Vite and other JavaScript frontends, there are three common production deployment models:
A backend service serves the built frontend files.
A reverse proxy serves the built frontend files.
A standalone static frontend talks directly to a separately hosted backend.
All three models use AddViteApp for local development and for producing the frontend build output during publishing. The difference is which resource becomes the production entrypoint and whether the browser talks to the backend directly.
These same production models also apply to AddJavaScriptApp. The difference
is that AddViteApp knows about Vite’s development conventions, while
AddJavaScriptApp makes fewer assumptions and leaves the run and build commands
under your control. 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, AddViteApp and AddJavaScriptApp should be treated as frontend build resources, not as the final production web server.
To deploy a JavaScript frontend, you must choose which other resource owns the public HTTP surface in production:
Use PublishWithContainerFiles(...) when your backend or web server will serve the built frontend files.
Use PublishWithStaticFiles(...) when your reverse proxy, gateway, or BFF will serve the built frontend files.
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 the built frontend in deployment.
Use this model when your backend, API, or server is responsible for serving static frontend files in production from wwwroot, static, or a similar directory.
This model only works if that backend or server can actually serve the built frontend assets. In other words, the deployed application service must be both the API host and the static file host for the frontend.
flowchart LR
Browser --> App["Node app<br/>serving built frontend files"]
App --> Frontend["Vite build output"]
This model keeps the production topology simple. The frontend build output becomes part of the same deployable unit as the backend, so one service owns the application surface, static files, and API behavior together.
It also maps well to frameworks that already know how to serve static files from wwwroot, static, or a similar folder. Instead of introducing an extra gateway or frontend-serving container, Aspire lets the backend stay responsible for the final HTTP response.
Your backend container gets larger because it now 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.
This is the default pattern Aspire is steering you toward for Vite frontends unless you intentionally introduce a gateway or BFF to own the public surface.
Use this model when a reverse proxy should be the public entrypoint for your app, either as a gateway or as a backend-for-frontend (BFF).
This model works well when you want a dedicated gateway or BFF in front of the rest of the application. In Aspire, YARP is the built-in example, but the same topology also applies when you use another reverse proxy such as Nginx or Caddy.
This model keeps the public entrypoint separate from the application services behind it. The reverse proxy becomes the stable edge for the app, while the frontend build and any backend services remain behind that gateway or BFF.
It is a good fit when you want one place to centralize routing, transforms, headers, and gateway or BFF concerns. YARP is Aspire’s first-class option here, which is why the example uses it, but the architectural tradeoffs are the same for other reverse proxies.
The reverse proxy owns the public endpoint, so backend services can stay internal behind the gateway or BFF.
Frontend hosting is decoupled from any individual backend service, which can make routing cleaner in multi-service apps.
Route rules now matter directly because the proxy decides which requests go to APIs and which requests go to the frontend.
You now have a dedicated gateway/BFF in the deployment, which adds one more moving part but also gives you more control over ingress behavior.
This is often the better choice when the frontend needs BFF-style behavior or when a standalone Vite build would otherwise need deployment-time configuration from backend resources.
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.
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.
This model looks familiar if you are used to deploying:
A Vite or React app to a static site host.
An API to another host.
Frontend JavaScript that calls the API directly from the browser.
It can seem like the most obvious path because it keeps the frontend as “just a
static site” and avoids adding a backend-served frontend or reverse proxy layer.
This model 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.
Aspire can still orchestrate the frontend build and the backend resource, but
this topology is less integrated than the two primary models above. In
particular, Aspire does not automatically solve:
Passing the final deployed backend URL into an already-built SPA.
Browser-to-API cross-origin concerns.
The split ownership between a standalone static site host and a separate API host.
If this is the model you want, plan for explicit runtime configuration and CORS
management. Otherwise, the backend-serves-frontend or reverse-proxy-serves-frontend
models are usually easier to reason about in Aspire.
Choose backend serves frontend when the backend already owns the app surface and should also own static file hosting.
Choose reverse proxy serves frontend when you want a gateway or BFF to stay in front of everything and route both frontend and backend traffic.
Choose the standalone static frontend + direct browser-to-backend model only
when you intentionally want separate frontend and backend hosting and are
willing to manage backend URL configuration and CORS explicitly.
In practice, the decision is usually about who should own the public HTTP surface in production:
If that should be your backend, use PublishWithContainerFiles.
If that should be your gateway or BFF, use PublishWithStaticFiles.
AddViteApp is best thought of as a development server plus a frontend build resource:
In run mode, it gives you the Vite dev server and HMR.
In publish mode, it produces frontend build artifacts.
Another resource serves those artifacts in production.
That distinction is easy to miss if you only read the JavaScript integration page or release notes. This article exists to make the production story explicit.
The important implication is that local Vite behavior does not automatically become production behavior. Routing and proxy setup often have to be expressed twice: once for local development and once for the resource that serves or routes traffic in deployment.
The same production decision applies to AddJavaScriptApp:
Another resource can serve the built assets.
Or a gateway/BFF can serve them.
The difference is that AddJavaScriptApp does not assume a particular development server. You choose the run script and the build script, but production still depends on deciding which deployed resource owns the final HTTP surface.