{"id":241,"date":"2026-01-12T09:00:00","date_gmt":"2026-01-12T17:00:00","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/aspire\/?p=241"},"modified":"2026-01-08T07:38:21","modified_gmt":"2026-01-08T15:38:21","slug":"aspire-for-javascript-developers","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/aspire\/aspire-for-javascript-developers\/","title":{"rendered":"Aspire for JavaScript developers"},"content":{"rendered":"<p>Remember when Aspire was just for .NET folks? Yeah, those days are over. With Aspire 13, JavaScript and TypeScript developers get to join the party\u2014and I&#8217;m not talking about some half-baked afterthought integration. This is first-class, full-featured support for orchestrating your JavaScript apps in distributed systems.<\/p>\n<p><div class=\"alert alert-info\"><p class=\"alert-divider\"><i class=\"fabric-icon fabric-icon--Info\"><\/i><strong>Note<\/strong><\/p> To be clear, we&#8217;re not done yet, we have lots of work to do in fact, and the JavaScript support is being actively improved. <\/div><\/p>\n<p>The <a href=\"https:\/\/www.nuget.org\/packages\/Aspire.Hosting.JavaScript\">\ud83d\udce6 <code>Aspire.Hosting.JavaScript<\/code><\/a> package (formerly <code>Aspire.Hosting.NodeJs<\/code>, because we renamed it to be less confusing\u2014you&#8217;re welcome) brings comprehensive support for developing, debugging, and deploying JavaScript applications. Whether you&#8217;re building slick frontends with Vite, REST APIs with Express, or full-stack apps with Next.js, Aspire&#8217;s got your back.<\/p>\n<h2>\ud83d\ude80 Adding JavaScript applications to Aspire<\/h2>\n<p>Aspire 13 gives you three different ways to run JavaScript code, each tailored for specific scenarios. Think of them as different tools in your toolbox\u2014you wouldn&#8217;t use a sledgehammer to hang a picture frame, right? (Well, you could, but results may vary \ud83e\udd23!)<\/p>\n<h3>\ud83d\udce6 JavaScript applications: The npm script runner<\/h3>\n<p>Got a <code>package.json<\/code> with scripts? <code>AddJavaScriptApp()<\/code> is your new best friend. It&#8217;s perfect for literally any JavaScript project that uses npm scripts\u2014React, Angular, Vue, Express, Next.js, that weird side project you started at 2 AM, you name it.<\/p>\n<pre><code class=\"language-csharp\">var builder = DistributedApplication.CreateBuilder(args);\n\nvar frontend = builder.AddJavaScriptApp(\"frontend\", \"..\/frontend\");\n\nbuilder.Build().Run();<\/code><\/pre>\n<p>Dead simple. By default, it runs your <code>\"dev\"<\/code> script during local development and your <code>\"build\"<\/code> script when you&#8217;re deploying. No fuss, no ceremony, just works.<\/p>\n<h4>\ud83d\udce5 Package manager support: Because one size doesn&#8217;t fit all<\/h4>\n<p>Look, I know the JavaScript ecosystem has opinions, lots of them, I get it&#8230;but Aspire supports them already. If your project has a <code>package.json<\/code>, npm is auto-configured as the default. But if you&#8217;re team Yarn or team pnpm (respect), switching is trivial:<\/p>\n<pre><code class=\"language-csharp\">\/\/ npm (default) with custom arguments\nvar app = builder.AddJavaScriptApp(\"app\", \"..\/app\")\n    .WithNpm(installArgs: [\"--legacy-peer-deps\"]);\n\n\/\/ Yarn\nvar yarnApp = builder.AddJavaScriptApp(\"yarn-app\", \"..\/yarn-app\")\n    .WithYarn();\n\n\/\/ pnpm\nvar pnpmApp = builder.AddJavaScriptApp(\"pnpm-app\", \"..\/pnpm-app\")\n    .WithPnpm();\n\n\/\/ Disable automatic installation (for pre-installed dependencies)\nvar noInstallApp = builder.AddJavaScriptApp(\"no-install\", \"..\/no-install\")\n    .WithNpm(install: false);<\/code><\/pre>\n<p>Here&#8217;s what&#8217;s happening under the hood (because details matter):<\/p>\n<p><strong>npm&#8217;s got smarts:<\/strong><\/p>\n<ul>\n<li>Development: Uses <code>npm install<\/code> (the classic)<\/li>\n<li>Production: Automatically switches to <code>npm ci<\/code> if it spots a <code>package-lock.json<\/code> (faster, more reliable, chef&#8217;s kiss)<\/li>\n<\/ul>\n<p><strong>Yarn plays nice:<\/strong><\/p>\n<ul>\n<li>Auto-detects your Yarn version by checking for <code>.yarnrc.yml<\/code> or the <code>.yarn<\/code> directory<\/li>\n<li>Yarn 2+: Uses <code>yarn install --immutable<\/code> in production when <code>yarn.lock<\/code> exists (no surprises)<\/li>\n<li>Yarn 1.x: Uses <code>yarn install --frozen-lockfile<\/code> in production (because legacy support matters)<\/li>\n<\/ul>\n<p><strong>pnpm just works:<\/strong><\/p>\n<ul>\n<li>Uses <code>pnpm install --frozen-lockfile<\/code> in production when <code>pnpm-lock.yaml<\/code> exists<\/li>\n<li>Automatically enables pnpm via corepack in Docker builds (we&#8217;re not savages)<\/li>\n<\/ul>\n<p><div class=\"alert alert-primary\"><p class=\"alert-divider\"><i class=\"fabric-icon fabric-icon--Info\"><\/i><strong>Note<\/strong><\/p> The <code>install<\/code> parameter (default: <code>true<\/code>) controls whether packages are automatically installed before the application starts. Set to <code>false<\/code> to skip installation when dependencies are already in place. <\/div><\/p>\n<h4>\ud83d\udd27 Script customization: Your package.json, your rules<\/h4>\n<p>Don&#8217;t like the default <code>\"dev\"<\/code> and <code>\"build\"<\/code> scripts? No problem. Override them:<\/p>\n<pre><code class=\"language-csharp\">var app = builder.AddJavaScriptApp(\"app\", \"..\/app\")\n    .WithRunScript(\"local\")    \/\/ Use \"npm run local\" in development\n    .WithBuildScript(\"build:prod\");  \/\/ Use \"npm run build:prod\" when publishing<\/code><\/pre>\n<p>Need to pass arguments? We got you:<\/p>\n<pre><code class=\"language-csharp\">var app = builder.AddJavaScriptApp(\"app\", \"..\/app\")\n    .WithRunScript(\"dev\", [\"--port\", \"3000\", \"--host\"]);<\/code><\/pre>\n<h3>\u2699\ufe0f Node.js applications: Direct execution, no package manager required<\/h3>\n<p>Sometimes you just want to run <code>node server.js<\/code> and call it a day. No npm scripts, no build steps, just pure Node.js execution. That&#8217;s where <code>AddNodeApp()<\/code> shines:<\/p>\n<pre><code class=\"language-csharp\">var nodeApp = builder.AddNodeApp(\"node-app\", \"..\/node-app\", \"server.js\")\n    .WithHttpEndpoint(env: \"PORT\");<\/code><\/pre>\n<p>Perfect for simple Node.js scripts, microservices, background workers, or when you want direct control over the Node.js process without the npm script ceremony. (Sometimes less is more, you know?)<\/p>\n<h4>\ud83d\udce5 Adding dependencies to Node apps<\/h4>\n<p>Your Node app has dependencies? Of course it does. When there&#8217;s a <code>package.json<\/code>, npm kicks in automatically. You can mix and match package managers with npm scripts if that&#8217;s your jam:<\/p>\n<pre><code class=\"language-csharp\">\/\/ Use Yarn with npm scripts\nvar nodeApp = builder.AddNodeApp(\"node-app\", \"..\/node-app\", \"server.js\")\n    .WithYarn()\n    .WithRunScript(\"dev\");  \/\/ Now uses \"yarn run dev\" instead of \"node server.js\"\n\n\/\/ Use pnpm\nvar pnpmNode = builder.AddNodeApp(\"pnpm-node\", \"..\/pnpm-node\", \"server.js\")\n    .WithPnpm();<\/code><\/pre>\n<h4>\ud83d\ude80 What happens in production<\/h4>\n<p>When you publish <code>AddNodeApp()<\/code>, Aspire generates multi-stage Dockerfiles that are actually good (I know, shocking). Here&#8217;s what you get:<\/p>\n<ul>\n<li><strong>Build stage<\/strong> installs dependencies and runs your build scripts<\/li>\n<li><strong>Runtime stage<\/strong> copies only the built artifacts (smaller images, faster deploys, happier DevOps team)<\/li>\n<li>Runs as the non-privileged <code>node<\/code> user (because security isn&#8217;t optional)<\/li>\n<li>Sets proper entrypoint as <code>[\"node\", \"your-script.js\"]<\/code> (no weird shell wrapping nonsense)<\/li>\n<\/ul>\n<h3>\u26a1 Vite applications: Frontend frameworks, but faster<\/h3>\n<p>If you&#8217;re building modern frontends with Vite (and you should be\u2014it&#8217;s ridiculously fast), use <code>AddViteApp()<\/code> for Vite-specific optimizations:<\/p>\n<pre><code class=\"language-csharp\">var viteApp = builder.AddViteApp(\"vite-app\", \"..\/vite-app\");<\/code><\/pre>\n<p>Works beautifully with React, Vue, Svelte, Astro, or any Vite-based framework.<\/p>\n<h4>\u2699\ufe0f What Aspire does for you automatically<\/h4>\n<p><code>AddViteApp()<\/code> is kind of magical. Behind the scenes, it:<\/p>\n<ul>\n<li>Configures an HTTP endpoint (port gets allocated dynamically\u2014no more port conflicts!)<\/li>\n<li>Passes the <code>--port<\/code> argument to Vite with the allocated port<\/li>\n<li>Runs the &#8220;dev&#8221; script during development<\/li>\n<li>Runs the &#8220;build&#8221; script when publishing<\/li>\n<li>Handles the <code>--<\/code> separator correctly for pnpm (because pnpm is special and doesn&#8217;t strip it like npm\/Yarn do)<\/li>\n<li>Generates production-ready Dockerfiles<\/li>\n<\/ul>\n<p>All of this, without you lifting a finger. It&#8217;s almost too easy.<\/p>\n<h4>\ud83d\udce5 Package managers for Vite apps<\/h4>\n<p>Like <code>AddJavaScriptApp()<\/code>, Vite apps support all the package managers you&#8217;d expect:<\/p>\n<pre><code class=\"language-csharp\">\/\/ npm (default)\nvar viteApp = builder.AddViteApp(\"vite-app\", \"..\/vite-app\");\n\n\/\/ Yarn\nvar yarnVite = builder.AddViteApp(\"yarn-vite\", \"..\/yarn-vite\")\n    .WithYarn();\n\n\/\/ pnpm\nvar pnpmVite = builder.AddViteApp(\"pnpm-vite\", \"..\/pnpm-vite\")\n    .WithPnpm();<\/code><\/pre>\n<h4>\ud83d\udcc4 Custom Vite configs: For when defaults aren&#8217;t enough<\/h4>\n<p>Got multiple Vite configs for different environments? (Of course you do.) Point Aspire at the right one:<\/p>\n<pre><code class=\"language-csharp\">var viteApp = builder.AddViteApp(\"vite-app\", \"..\/vite-app\")\n    .WithViteConfig(\".\/vite.production.config.js\");<\/code><\/pre>\n<p>The path is relative to your Vite app&#8217;s root directory. Pretty straightforward stuff.<\/p>\n<h4>\ud83d\udd12 HTTPS configuration: Certificate trust without the trust issues<\/h4>\n<p>Here&#8217;s where things get clever. Aspire handles HTTPS for Vite without butchering your carefully crafted <code>vite.config.js<\/code>. When you configure HTTPS certificates, Aspire generates a <em>wrapper<\/em> config that layers HTTPS settings on top:<\/p>\n<pre><code class=\"language-csharp\">var viteApp = builder.AddViteApp(\"vite-app\", \"..\/vite-app\")\n    .WithHttpsEndpoint(env: \"PORT\")\n    .WithHttpsDeveloperCertificate();<\/code><\/pre>\n<p>What happens under the hood:<\/p>\n<ol>\n<li>Aspire finds your existing Vite config (or uses Vite&#8217;s default resolution)<\/li>\n<li>Generates a wrapper config at <code>node_modules\/.bin\/aspire.{your-config}.js<\/code><\/li>\n<li>Passes cert paths through <code>TLS_CONFIG_PFX<\/code> and <code>TLS_CONFIG_PASSWORD<\/code> environment variables<\/li>\n<li>Augments your config to use HTTPS if it&#8217;s not already set up<\/li>\n<\/ol>\n<p>Your original <code>vite.config.js<\/code>? Untouched. Beautiful. This is how you do non-invasive tooling.<\/p>\n<p><div class=\"alert alert-info\"><p class=\"alert-divider\"><i class=\"fabric-icon fabric-icon--Info\"><\/i><strong>Important<\/strong><\/p> Vite apps use HTTP by default. HTTPS is opt-in via <code>WithHttpsEndpoint()<\/code> and requires calling <code>WithHttpsDeveloperCertificate()<\/code> or providing a custom certificate. <\/div><\/p>\n<h2>\ud83c\udf10 Endpoints and networking: Ports without the pain<\/h2>\n<p>JavaScript apps love their environment variables for port configuration (it&#8217;s basically a universal constant). Aspire makes this dead simple:<\/p>\n<pre><code class=\"language-csharp\">var app = builder.AddJavaScriptApp(\"app\", \"..\/app\")\n    .WithHttpEndpoint(port: 3000, env: \"PORT\");<\/code><\/pre>\n<p>What this does:<\/p>\n<ul>\n<li>Allocates port 3000 for your app<\/li>\n<li>Sets the <code>PORT<\/code> environment variable<\/li>\n<li>Registers the endpoint in Aspire&#8217;s service discovery (so other services can find you)<\/li>\n<li>Makes everything visible in the Aspire dashboard<\/li>\n<\/ul>\n<p>Common port environment variables you&#8217;ll see in the wild:<\/p>\n<ul>\n<li><code>PORT<\/code> &#8211; The universal standard. Express, Fastify, Koa, Next.js, basically everyone uses this<\/li>\n<li><code>VITE_PORT<\/code> &#8211; Vite being Vite<\/li>\n<li><code>HOST<\/code> &#8211; For binding to specific network interfaces (less common but still useful)<\/li>\n<\/ul>\n<h2>\ud83d\udd0d Service discovery: Finding your friends in the distributed system<\/h2>\n<p>This is where things get genuinely cool. JavaScript apps in Aspire automatically participate in service discovery. No configuration files, no service mesh complexity, just straightforward environment variables:<\/p>\n<pre><code class=\"language-csharp\">var api = builder.AddProject&lt;Projects.BackendApi&gt;(\"api\");\n\nvar frontend = builder.AddViteApp(\"frontend\", \"..\/frontend\")\n    .WithReference(api);<\/code><\/pre>\n<p>The frontend automatically receives environment variables like <code>API_HTTP<\/code> and <code>API_HTTPS<\/code>. Connect to your backend without hardcoding URLs (gasp, imagine that):<\/p>\n<pre><code class=\"language-typescript\">\/\/ In your Vite\/React application\nconst apiUrl = import.meta.env.API_HTTP || 'http:\/\/localhost:5000';\n\nconst response = await fetch(`${apiUrl}\/api\/data`);\nconst data = await response.json();<\/code><\/pre>\n<h3>\ud83d\udd00 Using Vite proxy configuration<\/h3>\n<p>Alternatively, you can configure Vite to proxy API requests, eliminating the need to manage the API URL in your code. This approach is especially useful during development:<\/p>\n<pre><code class=\"language-typescript\">\/\/ vite.config.ts\nimport { defineConfig } from 'vite';\nimport react from '@vitejs\/plugin-react';\n\nexport default defineConfig({\n  plugins: [react()],\n  server: {\n    proxy: {\n      '\/api': {\n        target: process.env.APISERVICE_HTTP || 'http:\/\/localhost:5000',\n        changeOrigin: true,\n        secure: false,\n      },\n    },\n  },\n});<\/code><\/pre>\n<p>With this configuration, your frontend code becomes simpler:<\/p>\n<pre><code class=\"language-typescript\">\/\/ No need to manage apiUrl - just make requests directly\nconst response = await fetch('\/api\/data');\nconst data = await response.json();<\/code><\/pre>\n<p><div class=\"alert alert-success\"><p class=\"alert-divider\"><i class=\"fabric-icon fabric-icon--Lightbulb\"><\/i><strong>Tip<\/strong><\/p> Using a Vite proxy configuration allows you to make API requests without explicitly managing URLs in your application code. The proxy automatically forwards requests to your backend service based on the environment variables provided by Aspire. <\/div><\/p>\n<h2>\ud83d\uddc4\ufe0f Database connections: Connection strings that actually make sense<\/h2>\n<p>When you wire up databases in Aspire, you get connection information in multiple formats. Why? Because JavaScript&#8217;s Database library ecosystem is&#8230; diverse (that&#8217;s the polite way to put it). Some libraries want URIs, others want individual properties. Aspire gives you both:<\/p>\n<pre><code class=\"language-csharp\">var postgres = builder.AddPostgres(\"postgres\")\n    .AddDatabase(\"appdb\");\n\nvar api = builder.AddNodeApp(\"api\", \"..\/api\")\n    .WithReference(postgres);\n\nbuilder.AddJavaScriptApp(\"client\", \"..\/client\")\n    .WithReference(api);<\/code><\/pre>\n<p>Your Node.js API app gets both URI format <em>and<\/em> individual connection properties:<\/p>\n<pre><code class=\"language-javascript\">\/\/ Option 1: Use the URI (perfect for Prisma, TypeORM, etc.)\nconst databaseUrl = process.env.APPDB_URI;\n\/\/ postgresql:\/\/user:pass@host:port\/dbname\n\n\/\/ Option 2: Use individual properties (great for node-postgres)\nconst pool = new Pool({\n  host: process.env.APPDB_HOST,\n  port: process.env.APPDB_PORT,\n  user: process.env.APPDB_USERNAME,\n  password: process.env.APPDB_PASSWORD,\n  database: process.env.APPDB_DATABASE\n});<\/code><\/pre>\n<p>This flexibility means you can use Prisma, TypeORM, Sequelize, Knex, node-postgres, or whatever database library you prefer\u2014no Aspire-specific adapters required. It just works with what you&#8217;re already using.<\/p>\n<h2>\ud83c\udfdb\ufe0f Default behaviors and publishing: The magic behind the curtain<\/h2>\n<p>Okay, time for the deep dive. Understanding what Aspire does automatically helps you leverage its full power and tweak things when you need to. (And sometimes you will need to\u2014we&#8217;re engineers, we love to tinker.)<\/p>\n<h3>\ud83d\udd27 What every JavaScript resource gets for free<\/h3>\n<p>Every JavaScript resource you add to Aspire\u2014whether it&#8217;s <code>AddJavaScriptApp()<\/code>, <code>AddNodeApp()<\/code>, or <code>AddViteApp()<\/code>\u2014 automatically gets a bunch of sensible Node.js defaults through an internal <code>WithNodeDefaults()<\/code> method. You don&#8217;t call this yourself; it happens automatically. Here&#8217;s what you&#8217;re getting:<\/p>\n<p><strong>OpenTelemetry integration (observability for free):<\/strong><\/p>\n<p>Aspire automatically configures OpenTelemetry exporters for your JavaScript apps. Distributed tracing? Metrics collection? You get all of that out of the box. Your Node.js services automatically participate in Aspire&#8217;s observability story without you writing a single line of telemetry code. It&#8217;s glorious.<\/p>\n<p><strong>Environment-aware NODE_ENV (because context matters):<\/strong><\/p>\n<p>The <code>NODE_ENV<\/code> environment variable gets set automatically based on what you&#8217;re doing:<\/p>\n<ul>\n<li><code>\"development\"<\/code> when you&#8217;re hacking away locally<\/li>\n<li><code>\"production\"<\/code> when you&#8217;re shipping to prod<\/li>\n<\/ul>\n<p>This ensures frameworks and libraries use appropriate optimizations. Express, for instance, enables template caching and serves less verbose error messages in production. These small optimizations add up.<\/p>\n<p><strong>Automatic certificate trust (SSL\/TLS without the headaches):<\/strong><\/p>\n<p>Aspire handles certificate trust transparently so your Node.js apps can talk to other services over HTTPS during local development without angry certificate warnings:<\/p>\n<ul>\n<li>\n<p><strong>Append mode (default)<\/strong>: Sets <code>NODE_EXTRA_CA_CERTS<\/code> to point to Aspire&#8217;s certificate bundle. Node.js trusts Aspire-managed certificates <em>alongside<\/em> system certificates. Best of both worlds.<\/p>\n<\/li>\n<li>\n<p><strong>Replace mode<\/strong>: Modifies <code>NODE_OPTIONS<\/code> to include <code>--use-openssl-ca<\/code>, forcing Node.js to use OpenSSL&#8217;s certificate store instead.<\/p>\n<\/li>\n<\/ul>\n<p>Here&#8217;s the clever bit: if you&#8217;ve already set <code>NODE_OPTIONS<\/code> to something like <code>--max-old-space-size=4096<\/code> (because you&#8217;re dealing with large datasets or memory-hungry builds), Aspire <em>appends<\/em> <code>--use-openssl-ca<\/code> instead of nuking your existing settings. It&#8217;s respectful like that.<\/p>\n<h3>\ud83d\udc33 Publishing and Dockerfile generation: Production-ready containers without the pain<\/h3>\n<p>When you publish your Aspire project (to Azure Container Apps, Kubernetes, wherever), Aspire auto-generates production-ready Dockerfiles through <code>PublishAsDockerFile()<\/code>. Smart detail: if you already have a Dockerfile in your app directory, Aspire backs off and uses yours. It&#8217;s not going to overwrite your carefully crafted custom setup.<\/p>\n<p><strong>What Aspire generates for you:<\/strong><\/p>\n<p>For <code>AddJavaScriptApp()<\/code> and <code>AddViteApp()<\/code>, you get single-stage Dockerfiles optimized for build tools that spit out static assets:<\/p>\n<ol>\n<li><strong>Base image selection<\/strong>: Defaults to <code>node:22-slim<\/code> (or detects version from <code>.nvmrc<\/code>, <code>package.json<\/code> engines, or <code>.node-version<\/code>)<\/li>\n<li><strong>Working directory<\/strong>: Sets up <code>\/app<\/code> as home base<\/li>\n<li><strong>Package manager setup<\/strong>: Runs initialization commands (like <code>corepack enable pnpm<\/code> for pnpm users)<\/li>\n<li><strong>Smart layer caching<\/strong>: Copies package files first (<code>package.json<\/code>, lockfiles, etc.) <em>before<\/em> copying source code. This means dependency installation gets cached unless your dependencies actually change. Docker layer caching FTW.<\/li>\n<li><strong>Dependency installation<\/strong>: Runs your package manager&#8217;s install command with BuildKit cache mounts (more on this in a sec)<\/li>\n<li><strong>Build execution<\/strong>: Runs your configured build script (default: &#8220;build&#8221;) to produce production assets<\/li>\n<li><strong>Container files<\/strong>: Automatically marks <code>\/app\/dist<\/code> as a container files source for sharing build outputs between containers<\/li>\n<\/ol>\n<p>For <code>AddNodeApp()<\/code>, you get multi-stage Dockerfiles that separate build from runtime:<\/p>\n<ol>\n<li><strong>Build stage<\/strong>: Named &#8220;build&#8221;, uses the full Node.js image, installs deps, runs build scripts<\/li>\n<li><strong>Runtime stage<\/strong>: Named &#8220;runtime&#8221;, copies <em>only<\/em> built artifacts from the build stage (smaller images, faster deploys)<\/li>\n<li><strong>Security hardening<\/strong>: Switches to the non-privileged <code>node<\/code> user before running anything<\/li>\n<li><strong>Production environment<\/strong>: Sets <code>NODE_ENV=production<\/code> explicitly<\/li>\n<li><strong>Clean entrypoint<\/strong>: Configures as <code>[\"node\", \"your-script.js\"]<\/code> for efficient process execution<\/li>\n<\/ol>\n<p><strong>BuildKit cache mount magic:<\/strong><\/p>\n<p>Aspire leverages Docker BuildKit&#8217;s cache mount feature for package managers. This is a game-changer for build times:<\/p>\n<ul>\n<li>npm: <code>--mount=type=cache,target=\/root\/.npm<\/code><\/li>\n<li>Yarn v1: <code>--mount=type=cache,target=\/root\/.cache\/yarn<\/code><\/li>\n<li>Yarn v2+: <code>--mount=type=cache,target=.yarn\/cache<\/code><\/li>\n<li>pnpm: <code>--mount=type=cache,target=\/pnpm\/store<\/code><\/li>\n<\/ul>\n<p>These cache mounts persist package manager caches <em>across builds<\/em>. Translation: you only download packages that actually changed. Subsequent builds are dramatically faster because you&#8217;re not re-downloading React for the 500th time.<\/p>\n<p><strong>Smart build script execution:<\/strong><\/p>\n<p><code>AddJavaScriptApp()<\/code> and <code>AddViteApp()<\/code> automatically configure build scripts for you, so your apps build without extra configuration. For <code>AddNodeApp()<\/code>, you&#8217;ll need to explicitly call <code>WithBuildScript()<\/code> if you want a build step. If no build script is configured, the generated Dockerfile skips that step entirely. Perfect for apps that don&#8217;t need a compilation or bundling step\u2014why waste build time on something unnecessary?<\/p>\n<p><strong>Container files (build output sharing):<\/strong><\/p>\n<p>JavaScript applications using <code>AddJavaScriptApp()<\/code> or <code>AddViteApp()<\/code> can share their build outputs with other containers through Aspire&#8217;s container files feature. By default, Aspire marks <code>\/app\/dist<\/code> as a container files source, meaning other resources can reference and copy these built assets. Super useful for scenarios where you build a frontend in one container and serve it from another (like when you&#8217;re using Nginx as a reverse proxy). No weird volume mounts or hacky workarounds.<\/p>\n<p>Here&#8217;s a practical example where a Node.js backend serves the built Vite frontend:<\/p>\n<pre><code class=\"language-csharp\">var builder = DistributedApplication.CreateBuilder(args);\n\nvar server = builder.AddNodeApp(\"server\", \"..\/server\", \"app.js\")\n    .WithHttpEndpoint(env: \"PORT\")\n    .WithExternalHttpEndpoints();\n\nvar frontend = builder.AddViteApp(\"frontend\", \"..\/frontend\")\n    .WithReference(server)\n    .WaitFor(server);\n\n\/\/ The server will include the frontend's built assets in its public directory\nserver.PublishWithContainerFiles(frontend, \"public\");\n\nbuilder.Build().Run();<\/code><\/pre>\n<p>What&#8217;s happening here:<\/p>\n<ol>\n<li>The Vite app builds and exposes <code>\/app\/dist<\/code> as container files automatically<\/li>\n<li>The Node.js server uses <code>PublishWithContainerFiles()<\/code> to pull in those built assets<\/li>\n<li>Frontend assets get copied into the server&#8217;s <code>public<\/code> directory during publishing<\/li>\n<li>In the generated Dockerfile, you&#8217;ll see <code>COPY --from=frontend \/app\/dist public<\/code><\/li>\n<\/ol>\n<p>This pattern is perfect for scenarios where your Node.js backend (Express, Fastify, etc.) serves static frontend assets\u2014a common pattern for SPAs with API backends, or when you want a single deployable container for both frontend and backend.<\/p>\n<h3>\ud83c\udfa8 Customizing Docker images: When you need more control<\/h3>\n<p>Aspire&#8217;s defaults work for most scenarios, but sometimes you need to do your own thing. That&#8217;s totally fine.<\/p>\n<h4>\ud83d\udd22 Node.js version selection: Picking your Node<\/h4>\n<p>Aspire defaults to <strong>Node.js 22<\/strong>, but it&#8217;s smart enough to detect what you actually want. It checks your project files in this order:<\/p>\n<ol>\n<li><strong><code>.nvmrc<\/code> file<\/strong> &#8211; The standard for nvm users<\/li>\n<li><strong><code>.node-version<\/code> file<\/strong> &#8211; Alternative format supported by various Node version managers<\/li>\n<li><strong><code>package.json<\/code> engines.node field<\/strong> &#8211; The npm-official way to specify Node requirements<\/li>\n<\/ol>\n<p>Want Node.js 24? Create a <code>.nvmrc<\/code> file in your app directory:<\/p>\n<pre><code class=\"language-ini\">24<\/code><\/pre>\n<p>Or spec it in your <code>package.json<\/code>:<\/p>\n<pre><code class=\"language-json\">{    \n  \"name\": \"my-app\",\n  \"engines\": {\n    \"node\": \"&gt;=24.0.0\"\n  }\n}<\/code><\/pre>\n<p>Aspire extracts the major version and uses it when building the Docker base image (e.g., <code>node:24-slim<\/code>).<\/p>\n<p><div class=\"alert alert-primary\"><p class=\"alert-divider\"><i class=\"fabric-icon fabric-icon--Info\"><\/i><strong>Note<\/strong><\/p>Aspire only detects the <strong>major version<\/strong> from these files. If you specify <code>24.11.0<\/code> or <code>&gt;=24.0.0<\/code>, Aspire will use <code>node:24-slim<\/code> or <code>node:24-alpine<\/code> depending on the resource type.<\/div><\/p>\n<h4>\ud83c\udfaf Override the base image completely<\/h4>\n<p>If auto-detection doesn&#8217;t cut it, or you want a specific image variant, use <code>WithDockerfileBaseImage()<\/code>.<\/p>\n<p><strong>For client-side apps<\/strong> (<code>AddJavaScriptApp()<\/code> and <code>AddViteApp()<\/code>), there&#8217;s only a <code>buildImage<\/code> parameter since these generate single-stage Dockerfiles:<\/p>\n<pre><code class=\"language-csharp\">var app = builder.AddJavaScriptApp(\"app\", \"..\/app\")\n    .WithDockerfileBaseImage(buildImage: \"node:22-alpine\");\n\nvar viteApp = builder.AddViteApp(\"vite-app\", \"..\/vite-app\")\n    .WithDockerfileBaseImage(buildImage: \"node:24-slim\");<\/code><\/pre>\n<p><strong>For <code>AddNodeApp()<\/code><\/strong>, you can specify <em>both<\/em> a build image (first parameter) and a runtime image (because multi-stage Dockerfiles let you use different images for building vs running):<\/p>\n<pre><code class=\"language-csharp\">var nodeApp = builder.AddNodeApp(\"node-app\", \"..\/node-app\", \"server.js\")\n    .WithDockerfileBaseImage(\n        buildImage: \"node:22-bullseye\",\n        runtimeImage: \"node:22-slim\");<\/code><\/pre>\n<p>This lets you use a full-featured image for building (with Python, build tools, native dependencies, whatever) while keeping your runtime image minimal. Smaller images = faster deploys = reduced attack surface. Win-win-win.<\/p>\n<h2>\ud83d\udcbb Development experience: Local dev that doesn&#8217;t suck<\/h2>\n<p>JavaScript development is actually pretty great these days! I do thoroughly enjoy how easy and quick it is to write JavaScript (especially with TypeScript). Aspire aims to enhance that experience further by smoothing out the rough edges of local development when services need to work together from different projects. And of course, taking your apps from local dev to production without the usual headaches.<\/p>\n<h3>\ud83d\udd25 Hot Module Replacement: Changes without waiting<\/h3>\n<p>Vite apps get fast Hot Module Replacement during development automatically. Change your code, see it instantly in the browser without full page reloads. If you&#8217;ve used Vite before, you know how addictive this is.<\/p>\n<h3>\ud83d\udce6 What you get in those auto-generated Dockerfiles<\/h3>\n<p><strong>For <code>AddJavaScriptApp()<\/code> and <code>AddViteApp()<\/code>:<\/strong><\/p>\n<ul>\n<li>Multi-stage builds (or single stage when appropriate)<\/li>\n<li>Package files copied first for optimal Docker layer caching<\/li>\n<li>Package install commands with BuildKit cache mounts (faster builds are better builds)<\/li>\n<li>Build scripts executed to create production assets<\/li>\n<li><code>NODE_ENV=production<\/code> set for runtime<\/li>\n<li>Defaults to Node.js 22 (or your specified version)<\/li>\n<\/ul>\n<p><strong>For <code>AddNodeApp()<\/code>:<\/strong><\/p>\n<ul>\n<li>Separate <code>build<\/code> and <code>runtime<\/code> stages (smaller final images)<\/li>\n<li>Dependencies installed in build stage<\/li>\n<li>Build scripts run if you&#8217;ve configured them<\/li>\n<li>Built artifacts copied to minimal runtime image<\/li>\n<li>Proper entrypoint with the Node command and your script path<\/li>\n<li>Runs as non-root <code>node<\/code> user (because security isn&#8217;t optional)<\/li>\n<\/ul>\n<h3>\u26a1 Package manager-specific optimizations: The details that matter<\/h3>\n<p><strong>npm:<\/strong><\/p>\n<ul>\n<li>Copies <code>package*.json<\/code> for layer caching<\/li>\n<li>Uses BuildKit cache mount at <code>\/root\/.npm<\/code><\/li>\n<li>Switches to <code>npm ci<\/code> in production mode when <code>package-lock.json<\/code> exists (faster, more deterministic)<\/li>\n<\/ul>\n<p><strong>Yarn:<\/strong><\/p>\n<ul>\n<li>Copies <code>package.json<\/code>, <code>yarn.lock<\/code>, <code>.yarnrc.yml<\/code>, and <code>.yarn<\/code> directory<\/li>\n<li>Detects Yarn version and uses appropriate frozen lockfile flags<\/li>\n<li>Uses BuildKit cache mount at <code>\/root\/.cache\/yarn<\/code> (v1) or <code>.yarn\/cache<\/code> (v2+)<\/li>\n<\/ul>\n<p><strong>pnpm:<\/strong><\/p>\n<ul>\n<li>Copies <code>package.json<\/code> and <code>pnpm-lock.yaml<\/code><\/li>\n<li>Enables pnpm via <code>corepack enable pnpm<\/code> before installation<\/li>\n<li>Uses BuildKit cache mount at <code>\/pnpm\/store<\/code><\/li>\n<li>Uses <code>--frozen-lockfile<\/code> in production mode when lockfile exists<\/li>\n<\/ul>\n<h2>\ud83d\udcda Working with TypeScript: It just works<\/h2>\n<p>Aspire fully supports TypeScript apps without any special configuration. Your build process (defined in your <code>package.json<\/code> scripts) handles TypeScript compilation:<\/p>\n<pre><code class=\"language-json\">{\n  \"scripts\": {\n    \"dev\": \"tsx watch src\/index.ts\",\n    \"build\": \"tsc\",\n    \"start\": \"node dist\/index.js\"\n  }\n}<\/code><\/pre>\n<p>Aspire respects your TypeScript configuration and build pipeline. Whether you&#8217;re using <code>tsc<\/code>, <code>tsx<\/code>, <code>esbuild<\/code>, <code>swc<\/code>, or any other TypeScript compiler, configure your scripts and Aspire handles the rest. TypeScript is a first-class citizen here.<\/p>\n<h2>\ud83c\udf89 The bottom line<\/h2>\n<p>Aspire 13 brings JavaScript and TypeScript into the fold as first-class citizens\u2014not as an afterthought, but as a core part of the platform. Whether you&#8217;re orchestrating Vite frontends, Express APIs, or Node.js microservices alongside any other project you can imagine, Aspire gives you unified tooling, automatic service discovery, production-ready Dockerfiles, and a development experience that actually works.<\/p>\n<p><strong>Massive shoutout to the <a href=\"https:\/\/github.com\/CommunityToolkit\/Aspire\">Aspire Community Toolkit<\/a><\/strong> \ud83d\ude4c\u2014this JavaScript integration wouldn&#8217;t exist without them. They pioneered the Node.js support that became the foundation for what shipped in Aspire 13. The community saw what was missing, built it, refined it, and made it production-ready. This is community-driven development at its finest, and they deserve all the credit.<\/p>\n<p>Get started by installing the <a href=\"https:\/\/www.nuget.org\/packages\/Aspire.Hosting.JavaScript\"><code>Aspire.Hosting.JavaScript<\/code><\/a> NuGet package in your AppHost project, or try <code>aspire new aspire-ts-cs-starter<\/code> to see JavaScript and .NET working together. For complete docs, check out the <a href=\"https:\/\/aspire.dev\/integrations\/frameworks\/javascript\/\">JavaScript support documentation on aspire.dev<\/a>.<\/p>\n<p>Now go build something cool. \ud83d\ude80<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Aspire 13 brings comprehensive JavaScript and TypeScript support to cloud-native development, enabling you to orchestrate Node.js applications, Vite frontends, and JavaScript services alongside your .NET projects with unified tooling and seamless integration.<\/p>\n","protected":false},"author":24662,"featured_media":242,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1,17,19],"tags":[6,12,33,35,37,32,34,36],"class_list":["post-241","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-aspire-category","category-deep-dives","category-integrations","tag-aspire-13","tag-javascript","tag-node-js","tag-npm","tag-pnpm","tag-typescript","tag-vite","tag-yarn"],"acf":[],"blog_post_summary":"<p>Aspire 13 brings comprehensive JavaScript and TypeScript support to cloud-native development, enabling you to orchestrate Node.js applications, Vite frontends, and JavaScript services alongside your .NET projects with unified tooling and seamless integration.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/aspire\/wp-json\/wp\/v2\/posts\/241","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/aspire\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/aspire\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/aspire\/wp-json\/wp\/v2\/users\/24662"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/aspire\/wp-json\/wp\/v2\/comments?post=241"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/aspire\/wp-json\/wp\/v2\/posts\/241\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/aspire\/wp-json\/wp\/v2\/media\/242"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/aspire\/wp-json\/wp\/v2\/media?parent=241"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/aspire\/wp-json\/wp\/v2\/categories?post=241"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/aspire\/wp-json\/wp\/v2\/tags?post=241"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}