Set up JavaScript apps in the AppHost
This article is the reference for the Aspire JavaScript hosting integration. It enumerates the AppHost APIs — with examples for both AppHost.cs and apphost.ts — that you use to orchestrate JavaScript and TypeScript applications in your AppHost project.
Hosting integration
Section titled “Hosting integration”To start building an Aspire app that uses JavaScript and TypeScript, install the 📦 Aspire.Hosting.JavaScript NuGet package:
aspire add javascriptThe Aspire CLI is interactive, be sure to select the appropriate search result when prompted:
Select an integration to add:
> javascript (Aspire.Hosting.JavaScript)> Other results listed as selectable options...#:package Aspire.Hosting.JavaScript@*<PackageReference Include="Aspire.Hosting.JavaScript" Version="*" />The integration exposes a number of app resource types:
JavaScriptAppResource: Added withAddJavaScriptApp/addJavaScriptAppfor general JavaScript applicationsNodeAppResource: Added withAddNodeApp/addNodeAppfor running specific JavaScript files with Node.jsViteAppResource: Added withAddViteApp/addViteAppfor Vite applications with Vite-specific defaultsNextJsAppResource: Added withAddNextJsApp/addNextJsAppfor Next.js applications with Next.js-specific run and publish defaults
Framework examples
Section titled “Framework examples”The following TypeScript AppHost examples show validated ways to wire common JavaScript frameworks into Aspire for both local development and Docker Compose deployment. Each sample assumes:
- A backend API app lives in
./frameworks/apiand listens on thePORTenvironment variable. - The framework app lives in
./frameworks/<framework-name>. - Docker Compose is shown as an example deployment target with
addDockerComposeEnvironment.
For production deployment choices, see Deploy JavaScript apps.
Start with a shared builder, Docker Compose deployment target, and API resource:
import { createBuilder } from './.modules/aspire.js';
const builder = await createBuilder();await builder.addDockerComposeEnvironment('compose');
const api = await builder .addNodeApp('api', './frameworks/api', 'server.js') .withHttpEndpoint({ port: 3001, env: 'PORT' }) .withExternalHttpEndpoints();
const apiEndpoint = await api.getEndpoint('http');Plain Vite apps that produce static browser files use addViteApp and publishAsStaticWebsite. The apiPath / apiTarget options configure the deployed static website to proxy /api requests to the backend.
await builder .addViteApp('vite', './frameworks/vite', { runScriptName: 'dev' }) .publishAsStaticWebsite({ apiPath: '/api', apiTarget: api }) .withExternalHttpEndpoints();export async function loadWeather() { const response = await fetch('/api/weather'); return response.json();}React apps created with Vite use the same static website pattern as other Vite browser apps.
await builder .addViteApp('react', './frameworks/react', { runScriptName: 'dev' }) .publishAsStaticWebsite({ apiPath: '/api', apiTarget: api }) .withExternalHttpEndpoints();export async function loadWeather() { const response = await fetch('/api/weather'); return response.json();}Vue apps created with Vite also use publishAsStaticWebsite.
await builder .addViteApp('vue', './frameworks/vue', { runScriptName: 'dev' }) .publishAsStaticWebsite({ apiPath: '/api', apiTarget: api }) .withExternalHttpEndpoints();<script setup lang="ts">const response = await fetch('/api/weather');const weather = await response.json();</script>Astro static
Section titled “Astro static”Static Astro apps use addViteApp and publishAsStaticWebsite.
await builder .addViteApp('astro', './frameworks/astro', { runScriptName: 'dev' }) .publishAsStaticWebsite({ apiPath: '/api', apiTarget: api }) .withExternalHttpEndpoints();Angular
Section titled “Angular”Angular 17+ uses Vite internally. Use addViteApp with the Angular app’s dev script, then publish the build output as a static website.
await builder .addViteApp('angular', './frameworks/angular', { runScriptName: 'dev' }) .publishAsStaticWebsite({ apiPath: '/api', apiTarget: api }) .withExternalHttpEndpoints();const target = process.env.API_HTTPS || process.env.API_HTTP;
if (!target) { throw new Error( 'API endpoint is not configured. Run the app through Aspire.' );}
module.exports = { '/api': { target, secure: false, changeOrigin: true, },};export async function loadWeather() { const response = await fetch('/api/weather'); return response.json();}Next.js
Section titled “Next.js”Next.js standalone apps use the dedicated addNextJsApp helper, not a generic Vite app. Read Aspire-provided values from server-side code paths with process.env.
await builder .addNextJsApp('nextjs', './frameworks/nextjs', { runScriptName: 'dev' }) .withEnvironment('API_URL', apiEndpoint) .withExternalHttpEndpoints();export default async function Home() { const apiUrl = process.env.API_URL;
if (!apiUrl) { throw new Error('API_URL is not configured.'); }
const response = await fetch(`${apiUrl}/api/weather`, { cache: 'no-store', }); const weather = response.ok ? await response.json() : [];
return <pre>{JSON.stringify(weather, null, 2)}</pre>;}Nuxt apps need node_modules at runtime for server-side rendering, so publish them with publishAsNpmScript. Set both API_URL for direct server-side code and NUXT_API_URL for Nuxt runtime config.
await builder .addViteApp('nuxt', './frameworks/nuxt', { runScriptName: 'dev' }) .publishAsNpmScript({ startScriptName: 'start' }) .withEnvironment('API_URL', apiEndpoint) .withEnvironment('NUXT_API_URL', apiEndpoint) .withExternalHttpEndpoints();export default defineNuxtConfig({ nitro: { preset: 'node-server', }, runtimeConfig: { apiUrl: '', // Overridden by NUXT_API_URL. },});export default defineEventHandler(async () => { const config = useRuntimeConfig(); const apiUrl = config.apiUrl;
if (!apiUrl) { throw new Error('NUXT_API_URL is not configured.'); }
return $fetch(`${apiUrl}/api/weather`);});SvelteKit
Section titled “SvelteKit”SvelteKit with @sveltejs/adapter-node produces a self-contained Node server artifact, so publish it with publishAsNodeServer.
await builder .addViteApp('sveltekit', './frameworks/sveltekit', { runScriptName: 'dev' }) .publishAsNodeServer('build/index.js', { outputPath: 'build' }) .withEnvironment('API_URL', apiEndpoint) .withExternalHttpEndpoints();import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ fetch }) => { const apiUrl = process.env.API_URL;
if (!apiUrl) { throw new Error('API_URL is not configured.'); }
const response = await fetch(`${apiUrl}/api/weather`);
return { weather: response.ok ? await response.json() : [], };};TanStack Start
Section titled “TanStack Start”TanStack Start uses Nitro’s Node server output and works with publishAsNodeServer.
await builder .addViteApp('tanstack-start', './frameworks/tanstack-start', { runScriptName: 'dev', }) .publishAsNodeServer('.output/server/index.mjs', { outputPath: '.output' }) .withEnvironment('API_URL', apiEndpoint) .withExternalHttpEndpoints();Astro SSR
Section titled “Astro SSR”Astro SSR apps using @astrojs/node need runtime dependencies, so publish them with publishAsNpmScript.
await builder .addViteApp('astro-ssr', './frameworks/astro-ssr', { runScriptName: 'dev' }) .publishAsNpmScript({ startScriptName: 'start' }) .withEnvironment('API_URL', apiEndpoint) .withExternalHttpEndpoints();Remix / React Router apps need node_modules at runtime. Pass the port argument through the package script so the server listens on Aspire’s assigned port.
await builder .addViteApp('remix', './frameworks/remix', { runScriptName: 'dev' }) .publishAsNpmScript({ startScriptName: 'start', runScriptArguments: '-- --port "$PORT"', }) .withEnvironment('API_URL', apiEndpoint) .withExternalHttpEndpoints();Qwik City
Section titled “Qwik City”Qwik City apps need runtime dependencies and the Node server adapter, so publish them with publishAsNpmScript.
await builder .addViteApp('qwik', './frameworks/qwik', { runScriptName: 'dev' }) .publishAsNpmScript({ startScriptName: 'start' }) .withEnvironment('API_URL', apiEndpoint) .withExternalHttpEndpoints();After adding the framework resources your app needs, build and run the AppHost:
await builder.build().run();Add JavaScript application
Section titled “Add JavaScript application”The AddJavaScriptApp method is the foundational method for adding JavaScript applications to your Aspire AppHost. It provides a consistent way to orchestrate JavaScript applications with automatic package manager detection and intelligent defaults.
var builder = DistributedApplication.CreateBuilder(args);
var api = builder .AddNodeApp("api", "./api", "server.js") .WithHttpEndpoint(port: 3001, env: "PORT");
var frontend = builder.AddJavaScriptApp("frontend", "./frontend") .WithHttpEndpoint(port: 3000, env: "PORT") .WithReference(api);
// After adding all resources, run the app...import { createBuilder } from './.modules/aspire.js';
const builder = await createBuilder();
const api = await builder .addNodeApp('api', './api', 'server.js') .withHttpEndpoint({ port: 3001, env: 'PORT' });
const frontend = await builder .addJavaScriptApp('frontend', './frontend') .withHttpEndpoint({ port: 3000, env: 'PORT' }) .withReference(api);
// After adding all resources, run the app...By default, AddJavaScriptApp:
- Uses npm as the package manager when
package.jsonis present - Runs the “dev” script during local development
- Runs the “build” script when publishing to create production build output
- Can generate publish-time container build artifacts for that build output
The method accepts the following parameters:
name: The name of the resource in the Aspire dashboardappDirectory: The path to the directory containing your JavaScript application (wherepackage.jsonis located)runScriptName(optional): The name of the npm script to run when starting the application. Defaults to ‘dev’.
Add Node.js application
Section titled “Add Node.js application”For Node.js applications that don’t use a package.json script runner, you can directly run a JavaScript file using the AddNodeApp extension method:
var builder = DistributedApplication.CreateBuilder(args);
var api = builder .AddNodeApp("api", "./api", "server.js") .WithHttpEndpoint(port: 3000, env: "PORT");
// After adding all resources, run the app...import { createBuilder } from './.modules/aspire.js';
const builder = await createBuilder();
const api = await builder .addNodeApp('api', './api', 'server.js') .withHttpEndpoint({ port: 3000, env: 'PORT' });
// After adding all resources, run the app...The AddNodeApp method requires:
- name: The name of the resource in the Aspire dashboard
- appDirectory: The path to the directory containing the node application.
- scriptPath The path to the script relative to the app directory to run.
Add Next.js application
Section titled “Add Next.js application”For Next.js applications, use the AddNextJsApp extension method. It provides Next.js-specific defaults for both run mode and publish mode:
#pragma warning disable ASPIREJAVASCRIPT001
var builder = DistributedApplication.CreateBuilder(args);
var api = builder .AddNodeApp("api", "./api", "server.js") .WithHttpEndpoint(port: 3001, env: "PORT");
var nextApp = builder.AddNextJsApp("next-app", "./next-app") .WithReference(api);
// After adding all resources, run the app...import { createBuilder } from './.modules/aspire.js';
const builder = await createBuilder();
const api = await builder .addNodeApp('api', './api', 'server.js') .withHttpEndpoint({ port: 3001, env: 'PORT' });
const nextApp = await builder .addNextJsApp('next-app', './next-app') .withReference(api);
// After adding all resources, run the app...AddNextJsApp configures:
- Run mode: Starts
next devwith the correct port binding (-pflag). - Publish mode: Generates a multi-stage Dockerfile using Next.js standalone output.
- Deploy-time validation: Checks
next.config.ts,next.config.js, ornext.config.mjsforoutput: "standalone"as a prerequisite step before building the container. Without standalone output, the generated Dockerfile will not work correctly.
To opt out of the configuration validation step, call DisableBuildValidation / disableBuildValidation:
#pragma warning disable ASPIREJAVASCRIPT001
var nextApp = builder.AddNextJsApp("next-app", "./next-app") .DisableBuildValidation();const nextApp = await 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.
Add Vite application
Section titled “Add Vite application”For Vite applications, you can use the AddViteApp extension method which provides Vite-specific defaults and optimizations:
var builder = DistributedApplication.CreateBuilder(args);
var api = builder .AddNodeApp("api", "./api", "server.js") .WithHttpEndpoint(port: 3001, env: "PORT");
var viteApp = builder.AddViteApp("vite-app", "./vite-app") .WithReference(api);
// After adding all resources, run the app...import { createBuilder } from './.modules/aspire.js';
const builder = await createBuilder();
const api = await builder .addNodeApp('api', './api', 'server.js') .withHttpEndpoint({ port: 3001, env: 'PORT' });
const viteApp = await builder .addViteApp('vite-app', './vite-app') .withReference(api);
// After adding all resources, run the app...AddViteApp automatically configures:
- HTTP endpoint: Registers an
httpendpoint and sets thePORTenvironment variable — you don’t need to callWithHttpEndpointyourself - Development script: Runs the “dev” script (typically
vite) during local development - Build script: Runs the “build” script (typically
vite build) when publishing - Package manager: Uses npm by default, but can be customized with
WithYarn(),WithPnpm(), orWithBun()
The method accepts the same parameters as AddJavaScriptApp:
- name: The name of the resource in the Aspire dashboard
- 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.
Configure package managers
Section titled “Configure package managers”Aspire automatically detects and supports multiple JavaScript package managers with intelligent defaults for both development and production scenarios.
Auto-install by default
Section titled “Auto-install by default”Package managers automatically install dependencies by default. This ensures dependencies are always up-to-date during development and publishing.
Use npm (default)
Section titled “Use npm (default)”npm is the default package manager. If your project has a package.json file, Aspire will use npm unless you specify otherwise:
var builder = DistributedApplication.CreateBuilder(args);
// npm is used by defaultvar app = builder.AddJavaScriptApp("app", "./app");
// Customize npm with additional flagsvar customApp = builder.AddJavaScriptApp("custom-app", "./custom-app") .WithNpm(installCommand: "ci", installArgs: ["--legacy-peer-deps"]);
// After adding all resources, run the app...import { createBuilder } from './.modules/aspire.js';
const builder = await createBuilder();
// npm is used by defaultconst app = await builder.addJavaScriptApp('app', './app');
// Customize npm with additional flagsconst customApp = await builder .addJavaScriptApp('custom-app', './custom-app') .withNpm({ installCommand: 'ci', installArgs: ['--legacy-peer-deps'] });
// After adding all resources, run the app...When publishing (production mode), Aspire automatically uses npm ci if package-lock.json exists, otherwise it uses npm install for deterministic builds.
Use yarn
Section titled “Use yarn”To use yarn as the package manager, call WithYarn / withYarn:
var builder = DistributedApplication.CreateBuilder(args);
var app = builder.AddJavaScriptApp("app", "./app") .WithYarn();
// Customize yarn with additional flagsvar customApp = builder.AddJavaScriptApp("custom-app", "./custom-app") .WithYarn(installArgs: ["--immutable"]);
// After adding all resources, run the app...import { createBuilder } from './.modules/aspire.js';
const builder = await createBuilder();
const app = await builder.addJavaScriptApp('app', './app').withYarn();
// Customize yarn with additional flagsconst customApp = await builder .addJavaScriptApp('custom-app', './custom-app') .withYarn({ installArgs: ['--immutable'] });
// After adding all resources, run the app...When publishing, Aspire uses:
yarn install --immutableifyarn.lockexists and yarn v2+ is detectedyarn install --frozen-lockfileifyarn.lockexists with yarn v1yarn installotherwise
Use pnpm
Section titled “Use pnpm”To use pnpm as the package manager, call WithPnpm / withPnpm:
var builder = DistributedApplication.CreateBuilder(args);
var app = builder.AddJavaScriptApp("app", "./app") .WithPnpm();
// Customize pnpm with additional flagsvar customApp = builder.AddJavaScriptApp("custom-app", "./custom-app") .WithPnpm(installArgs: ["--frozen-lockfile"]);
// After adding all resources, run the app...import { createBuilder } from './.modules/aspire.js';
const builder = await createBuilder();
const app = await builder.addJavaScriptApp('app', './app').withPnpm();
// Customize pnpm with additional flagsconst customApp = await builder .addJavaScriptApp('custom-app', './custom-app') .withPnpm({ installArgs: ['--frozen-lockfile'] });
// After adding all resources, run the app...When publishing, Aspire uses pnpm install --frozen-lockfile if pnpm-lock.yaml exists, otherwise it uses pnpm install.
Use Bun
Section titled “Use Bun”To use Bun as the package manager, call WithBun / withBun:
var builder = DistributedApplication.CreateBuilder(args);
var app = builder.AddViteApp("app", "./app") .WithBun();
// Customize Bun with additional flagsvar customApp = builder.AddViteApp("custom-app", "./custom-app") .WithBun(installArgs: ["--frozen-lockfile"]);
// After adding all resources, run the app...import { createBuilder } from './.modules/aspire.js';
const builder = await createBuilder();
const app = await builder.addViteApp('app', './app').withBun();
// Customize Bun with additional flagsconst customApp = await builder .addViteApp('custom-app', './custom-app') .withBun({ installArgs: ['--frozen-lockfile'] });
// After adding all resources, run the app...When publishing, Aspire uses bun install --frozen-lockfile if bun.lock or bun.lockb exists, otherwise it uses bun install.
Bun supports passing script arguments without the -- separator, so commands like bun run dev --port 3000 work without needing bun run dev -- --port 3000.
When publishing to a container, WithBun / withBun automatically configures a Bun build image (oven/bun:1) since Bun is not available in the default Node.js base images. To use a specific Bun version, configure a custom build image:
#pragma warning disable ASPIREDOCKERFILEBUILDER001
var builder = DistributedApplication.CreateBuilder(args);
var app = builder.AddViteApp("app", "./app") .WithBun() .WithDockerfileBaseImage(buildImage: "oven/bun:1.1");
// After adding all resources, run the app...import { createBuilder } from './.modules/aspire.js';
const builder = await createBuilder();
const app = await builder .addViteApp('app', './app') .withBun() .withDockerfileBaseImage({ buildImage: 'oven/bun:1.1' });
// After adding all resources, run the app...Customize scripts
Section titled “Customize scripts”You can customize which scripts run during development and build:
var builder = DistributedApplication.CreateBuilder(args);
// Use different script namesvar app = builder.AddJavaScriptApp("app", "./app") .WithRunScript("start") // Run "npm run start" during development instead of "dev" .WithBuildScript("prod"); // Run "npm run prod" during publish instead of "build"
// After adding all resources, run the app...import { createBuilder } from './.modules/aspire.js';
const builder = await createBuilder();
// Use different script namesconst app = await builder .addJavaScriptApp('app', './app') .withRunScript('start') // Run "npm run start" during development instead of "dev" .withBuildScript('prod'); // Run "npm run prod" during publish instead of "build"
// After adding all resources, run the app...Pass arguments to scripts
Section titled “Pass arguments to scripts”To pass command-line arguments to your scripts, use WithArgs / withArgs:
var builder = DistributedApplication.CreateBuilder(args);
var app = builder.AddJavaScriptApp("app", "./app") .WithRunScript("dev") .WithArgs("--port", "3000", "--host");
// After adding all resources, run the app...import { createBuilder } from './.modules/aspire.js';
const builder = await createBuilder();
const app = await builder .addJavaScriptApp('app', './app') .withRunScript('dev') .withArgs(['--port', '3000', '--host']);
// After adding all resources, run the app...Alternatively, you can define custom scripts in your package.json with arguments baked in:
{ "scripts": { "dev": "vite", "dev:custom": "vite --port 3000 --host" }}Then reference the custom script:
var builder = DistributedApplication.CreateBuilder(args);
var app = builder.AddJavaScriptApp("app", "./app") .WithRunScript("dev:custom");
// After adding all resources, run the app...import { createBuilder } from './.modules/aspire.js';
const builder = await createBuilder();
const app = await builder .addJavaScriptApp('app', './app') .withRunScript('dev:custom');
// After adding all resources, run the app...Configure endpoints
Section titled “Configure endpoints”JavaScript applications typically use environment variables to configure the port they listen on. Use WithHttpEndpoint / withHttpEndpoint to configure the port and set the environment variable:
var builder = DistributedApplication.CreateBuilder(args);
var app = builder.AddJavaScriptApp("app", "./app") .WithHttpEndpoint(port: 3000, env: "PORT");
// After adding all resources, run the app...import { createBuilder } from './.modules/aspire.js';
const builder = await createBuilder();
const app = await builder .addJavaScriptApp('app', './app') .withHttpEndpoint({ port: 3000, env: 'PORT' });
// After adding all resources, run the app...Common environment variables for JavaScript frameworks:
- PORT: Generic port configuration used by many frameworks (Express, Vite, Next.js)
- VITE_PORT: For Vite applications
- HOST: Some frameworks also use this to bind to specific interfaces
Customize Vite configuration
Section titled “Customize Vite configuration”For Vite applications, you can specify a custom configuration file if you need to override the default Vite configuration resolution behavior:
var builder = DistributedApplication.CreateBuilder(args);
var viteApp = builder.AddViteApp("vite-app", "./vite-app") // Path is relative to the Vite service project root .WithViteConfig("./vite.production.config.js");
// After adding all resources, run the app...import { createBuilder } from './.modules/aspire.js';
const builder = await createBuilder();
const viteApp = await builder .addViteApp('vite-app', './vite-app') // Path is relative to the Vite service project root .withViteConfig('./vite.production.config.js');
// After adding all resources, run the app...The WithViteConfig / withViteConfig configuration accepts:
- configPath: The path to the Vite configuration file, relative to the Vite service project root.
This is useful when you have multiple Vite configuration files for different scenarios (development, staging, production).
HTTPS configuration
Section titled “HTTPS configuration”Aspire automatically augments existing Vite configurations to enable HTTPS endpoints at runtime, eliminating manual certificate configuration for development. When you configure HTTPS endpoints on a Vite resource, Aspire dynamically injects the necessary HTTPS configuration:
#pragma warning disable ASPIRECERTIFICATES001
var builder = DistributedApplication.CreateBuilder(args);
var viteApp = builder.AddViteApp("vite-app", "./vite-app") .WithHttpsEndpoint(env: "PORT") .WithHttpsDeveloperCertificate();
// After adding all resources, run the app...import { createBuilder } from './.modules/aspire.js';
const builder = await createBuilder();
const viteApp = await builder .addViteApp('vite-app', './vite-app') .withHttpsEndpoint({ env: 'PORT' }) .withHttpsDeveloperCertificate();
// After adding all resources, run the app...The HTTPS configuration is automatically applied without modifying your vite.config.js file. For more information about certificate configuration, see Certificate configuration.
Pass API URLs to Vite apps
Section titled “Pass API URLs to Vite apps”When your Vite app needs to communicate with a backend API, pass the API URL via an environment variable. Vite only exposes variables prefixed with VITE_ to client-side code.
In your AppHost, expose the API URL to the Vite app using WithEnvironment / withEnvironment:
var builder = DistributedApplication.CreateBuilder(args);
var api = builder .AddNodeApp("api", "./api", "server.js") .WithHttpEndpoint(port: 3001, env: "PORT") .WithExternalHttpEndpoints();
var viteApp = builder.AddViteApp("vite-app", "./vite-app") .WithReference(api) .WithEnvironment("VITE_API_BASE_URL", api.GetEndpoint("http"));
// After adding all resources, run the app...import { createBuilder } from './.modules/aspire.js';
const builder = await createBuilder();
const api = await builder .addNodeApp('api', './api', 'server.js') .withHttpEndpoint({ port: 3001, env: 'PORT' }) .withExternalHttpEndpoints();
const viteApp = await builder .addViteApp('vite-app', './vite-app') .withReference(api) .withEnvironment('VITE_API_BASE_URL', await api.getEndpoint('http'));
// After adding all resources, run the app...In your Vite app, read the variable from import.meta.env:
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL;
export async function fetchData() { const response = await fetch(`${apiBaseUrl}/api/data`); return response.json();}Pass runtime configuration to SPA frontends
Section titled “Pass runtime configuration to SPA frontends”Vite and other SPA build tools bake environment variables (such as VITE_*) into the JavaScript bundle at build time (for example, when building the client for production). However, Aspire sets environment variables at runtime. This means calling WithEnvironment("VITE_GOOGLE_CLIENT_ID", parameter) / withEnvironment('VITE_GOOGLE_CLIENT_ID', parameter) on a Vite resource won’t change values that were already baked into a previously built production bundle.
To bridge this gap, pass the parameter to your API app as a standard environment variable and expose it through a configuration endpoint that the SPA fetches at startup.
-
Pass the parameter to the API in the AppHost
Define the parameter in the AppHost and pass it to the API app using
WithEnvironment/withEnvironment. Then reference the API from the frontend so it can call the endpoint:AppHost.cs var builder = DistributedApplication.CreateBuilder(args);var googleClientId = builder.AddParameter("google-client-id");var api = builder.AddNodeApp("api", "./api", "server.js").WithHttpEndpoint(port: 3001, env: "PORT").WithEnvironment("GOOGLE_CLIENT_ID", googleClientId);var frontend = builder.AddViteApp("frontend", "./frontend").WithPnpm().WithReference(api);// After adding all resources, run the app...apphost.ts import { createBuilder } from './.modules/aspire.js';const builder = await createBuilder();const googleClientId = await builder.addParameter('google-client-id');const api = await builder.addNodeApp('api', './api', 'server.js').withHttpEndpoint({ port: 3001, env: 'PORT' }).withEnvironment('GOOGLE_CLIENT_ID', googleClientId);const frontend = await builder.addViteApp('frontend', './frontend').withPnpm().withReference(api);// After adding all resources, run the app... -
Expose a config endpoint in the API
Create an endpoint in your API app that reads the environment variable from
process.envand returns it to the frontend:JavaScript — api/server.js import http from 'node:http';const port = process.env.PORT ?? 3000;const clientId = process.env.GOOGLE_CLIENT_ID;const server = http.createServer((request, response) => {if (request.url !== '/api/config/google-client-id') {response.writeHead(404).end();return;}if (!clientId) {response.writeHead(404).end();return;}response.setHeader('content-type', 'application/json');response.end(JSON.stringify({ clientId }));});server.listen(port); -
Fetch the config value in the SPA
In your frontend application, fetch the configuration value at startup instead of reading from
import.meta.env:TypeScript — config.ts export async function getConfig() {const response = await fetch('/api/config/google-client-id');if (!response.ok) {throw new Error('Failed to load configuration');}const { clientId } = await response.json();return { googleClientId: clientId };}
Monorepo and Turborepo patterns
Section titled “Monorepo and Turborepo patterns”Aspire supports monorepo layouts where multiple JavaScript apps share a single root workspace. Each app is added as a separate resource in the AppHost pointing to its own subdirectory.
pnpm workspaces
Section titled “pnpm workspaces”For a pnpm monorepo, install dependencies from the workspace root and reference individual app directories:
var builder = DistributedApplication.CreateBuilder(args);
var api = builder .AddNodeApp("api", "./apps/api", "server.js") .WithHttpEndpoint(port: 3001, env: "PORT");
// Each app lives in its own subdirectory with its own package.jsonvar frontend = builder.AddViteApp("frontend", "./apps/frontend") .WithPnpm() .WithReference(api);
var dashboard = builder.AddViteApp("dashboard", "./apps/dashboard") .WithPnpm() .WithReference(api);
// After adding all resources, run the app...import { createBuilder } from './.modules/aspire.js';
const builder = await createBuilder();
const api = await builder .addNodeApp('api', './apps/api', 'server.js') .withHttpEndpoint({ port: 3001, env: 'PORT' });
// Each app lives in its own subdirectory with its own package.jsonconst frontend = await builder .addViteApp('frontend', './apps/frontend') .withPnpm() .withReference(api);
const dashboard = await builder .addViteApp('dashboard', './apps/dashboard') .withPnpm() .withReference(api);
// After adding all resources, run the app...Turborepo
Section titled “Turborepo”Turborepo orchestrates builds across a monorepo. Use a custom run script that delegates to the Turborepo pipeline for a specific app:
{ "scripts": { "dev": "turbo run dev --filter=frontend" }}var builder = DistributedApplication.CreateBuilder(args);
var api = builder .AddNodeApp("api", "./apps/api", "server.js") .WithHttpEndpoint(port: 3001, env: "PORT");
var frontend = builder.AddJavaScriptApp("frontend", "./apps/frontend") .WithPnpm() .WithRunScript("dev") .WithReference(api);
// After adding all resources, run the app...import { createBuilder } from './.modules/aspire.js';
const builder = await createBuilder();
const api = await builder .addNodeApp('api', './apps/api', 'server.js') .withHttpEndpoint({ port: 3001, env: 'PORT' });
const frontend = await builder .addJavaScriptApp('frontend', './apps/frontend') .withPnpm() .withRunScript('dev') .withReference(api);
// After adding all resources, run the app...Production builds
Section titled “Production builds”When you publish your application, Aspire automatically:
- Generates publish-time build artifacts for containerized deployment
- Installs dependencies using deterministic install commands based on lockfiles
- Runs the build script (typically “build”) to create production assets
- Produces frontend build output that another resource can include or serve
This ensures your JavaScript applications are built consistently across environments and can participate in Aspire publishing workflows.
For the production deployment patterns used by AddJavaScriptApp and
AddViteApp, including who serves the built frontend in production, see
Deploy JavaScript apps.
See also
Section titled “See also”- External parameters - Learn how to use parameters in Aspire
- Node.js hosting extensions - Community Toolkit extensions for Vite, Yarn, and pnpm
- Deploy JavaScript apps - Production deployment patterns including
PublishAsStaticWebsite,PublishAsNodeServer, andPublishAsNpmScript - What’s new in Aspire 13 - Learn about first-class JavaScript support
- Aspire integrations overview
- Aspire GitHub repo