The web ecosystem constantly ebbs and flows between simplicity and complexity. The nature of an ever-evolving development model makes it essential to keep your tooling updated, or else you find yourself stranded on a “maintenance island”. The current tool du jour is Vite, and I find the decisions made with it a refreshing net positive for the frontend development toolkit. It’s one of the more straightforward tooling options in the ecosystem with sane defaults but ultimate reconfigurability.

While the frontend ecosystem is handily winning the UI/UX development conversation, there’s also a lot that ASP.NET Core’s Razor can offer application developers, and it’s arguably a better option to rely on both when building user-facing applications.

In this post, we’ll see how to integrate Vite’s development server with ASP.NET Core, and you’ll be surprised it’s much simpler than you may think. Let’s get started.

What’s Vite?

Before jumping into ASP.NET code, let’s talk about Vite and how it can help your development workflow. Vite is a development environment for developers using various frameworks, such as React, Angular, Vue, and other libraries. Vite also is “smart” and can recognize common development dependencies, such as TypeScript and Sass, giving developers a low-ceremony approach to adding the dependencies to their projects. Finally, Vite is compatible with Rollup plugins to process other assets, such as images, styles, and HTML.

For developers, Vite operates in two modes: development and build. Vite’s development mode utilizes hot-module reload capabilities to provide developers with immediate feedback when assets change on disk. This mode increases the feedback-loop time, increasing developer productivity. It also allows developers to see unprocessed assets to improve the ability to debug issues.

During the build mode, assets are processed through plugins and ultimately readied for deployment. The build includes all referenced JavaScript, Stylesheets, and HTML files. Additionally, Vite copies all static assets to the final build directory.

In summary, Vite is a development tool that simplifies an increasingly complex frontend ecosystem, and I believe it works. Check out the official site to learn more. https://vitejs.dev/

Adding Vite To ASP.NET Core

The first step to adding Vite to your ASP.NET Core applications is getting all your files and folders in the right place. The two most essential elements will be where you place your package.json and your frontend development folder. I named my development folder Client, but feel free to call it whatever you’d like.

Project
|- wwwroot
|- /Client
  |- /public
  |- /src
  |- main.ts
|- package.json

In your packages.json, you’ll add all your development dependencies, with the most critical being vite and typescript (if you want to use TypeScript). Here’s my package.json file for my sample project, including my scripts.

{
  "name": "my-webcomponents",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview"
  },
  "devDependencies": {
    "ts-node": "^10.9.1",
    "typescript": "^5.0.2",
    "vite": "^4.2.0",
    "bootstrap": "^5.2.3",
    "jquery": "^3.6.4",
    "jquery-validation": "^1.19.5",
    "jquery-validation-unobtrusive": "^4.0.0",
    "sass": "^1.61.0"
  }
}

Next, we need both the tsconfig.json and vite.config.ts files at the root of our project. Here are mine, respectively.

{
  "compilerOptions": {
    "composite": true,
    "module": "ESNext",
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "declaration": true,
    "emitDeclarationOnly": true,
    "outDir": "./Client/types",
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "moduleResolution": "Node",
    "isolatedModules": true,
    "allowSyntheticDefaultImports": true,
    "experimentalDecorators": true,
    "forceConsistentCasingInFileNames": true,
    "useDefineForClassFields": false,
    "skipLibCheck": true,
    "types": [
      "vite/client"
    ]
  },
  "include": [
    "vite.config.ts",
    "Client"
  ]
}

The vite.config.ts file is a bit of a doozy, but it’s written to support the same HTTPS certificate your ASP.NET Core application will use by loading settings from your appSettings files.

/**
 * Name: vite.config.ts
 * Description: Vite configuration file
 */

import { UserConfig, defineConfig } from 'vite';
import { spawn } from "child_process";
import fs from "fs";
import path from "path";

// @ts-ignore
import appsettings from "./appsettings.json";
// @ts-ignore
import appsettingsDev from "./appsettings.Development.json";

import * as process from "process";

// Get base folder for certificates.
const baseFolder =
    process.env.APPDATA !== undefined && process.env.APPDATA !== ''
        ? `${process.env.APPDATA}/ASP.NET/https`
        : `${process.env.HOME}/.aspnet/https`;

// Generate the certificate name using the NPM package name
const certificateName = process.env.npm_package_name;

// Define certificate filepath
const certFilePath = path.join(baseFolder, `${certificateName}.pem`);
// Define key filepath
const keyFilePath = path.join(baseFolder, `${certificateName}.key`);

// Pattern for CSS files
const cssPattern = /\.css$/;
// Pattern for image files
const imagePattern = /\.(png|jpe?g|gif|svg|webp|avif)$/;

// Export Vite configuration
export default defineConfig(async () => {
  // Ensure the certificate and key exist
  if (!fs.existsSync(certFilePath) || !fs.existsSync(keyFilePath)) {
    // Wait for the certificate to be generated
    await new Promise<void>((resolve) => {
      spawn('dotnet', [
        'dev-certs',
        'https',
        '--export-path',
        certFilePath,
        '--format',
        'Pem',
        '--no-password',
      ], { stdio: 'inherit', })
          .on('exit', (code: any) => {
            resolve();
            if (code) {
              process.exit(code);
            }
          });
    });
  };

  // Define Vite configuration
  const config: UserConfig = {
    clearScreen: true,
    appType: 'mpa',
    root: 'Client',
    publicDir: 'public',
    build: {
      manifest: appsettings.Vite.Manifest,
      emptyOutDir: true,
      outDir: '../wwwroot',
      assetsDir: '',
      rollupOptions: {
        input: ['Client/main.ts', "Client/scss/site.scss" ],
        // remove hashing, but I could add it back in
        output: {
          // Save entry files to the appropriate folder
          entryFileNames: 'js/[name].js',
          // Save chunk files to the js folder
          chunkFileNames: 'js/[name]-chunk.js',
          // Save asset files to the appropriate folder
          assetFileNames: (info) => {
            if (info.name) {
              // If the file is a CSS file, save it to the css folder
              if (cssPattern.test(info.name)) {
                return 'css/[name][extname]';
              }
              // If the file is an image file, save it to the images folder
              if (imagePattern.test(info.name)) {
                return 'images/[name][extname]';
              }

              // If the file is any other type of file, save it to the assets folder 
              return 'assets/[name][extname]';
            } else {
              // If the file name is not specified, save it to the output directory
              return '[name][extname]';
            }
          },
        }
      },
    },
    server: {
      port: appsettingsDev.Vite.Server.Port,
      strictPort: true,
      https: {
        cert: certFilePath,
        key: keyFilePath
      },
      hmr: {
        host: "localhost",
        clientPort: appsettingsDev.Vite.Server.Port
      }
    }
  }

  return config;
});

Feel free to modify the vite.config.ts to match your use case. For example, I removed the hash from all build artifacts, but you may want to add them back.

You’ll also want to modify the input array to produce the required files on the build. In my case, I want to build both my site’s CSS and JavaScript as separate assets.

If you get stuck configuring your Vite configuration, I suggest you read the Vite documentation to see all available options.

Important note: The build assets should end up in wwwroot so ASP.NET Core can bundle and publish them correctly. Also, modify your csproj to include any files on publish. As this is project specific, you’ll need to figure this out independently.

Next, we must modify our appsettings.json and appsettings.Development.json files. So here they are, respectively.

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "Vite": {
    "Manifest": "assets.manifest.json"
  }
}

And The development settings, which includes a Vite section. Here we will tell the Middleware what port the Vite dev server will be listening on. You can also set the script name here, but by default, the Vite middleware will look for a script named dev.

{
  "DetailedErrors": true,
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "Vite": {
    "Server": {
      "Port": 5173,
      "Https": true
    }
  }
}

Let’s install the NuGet package of Vite.AspNetCore by adding it to our project’s csproj file.

<ItemGroup>
  <PackageReference Include="Vite.AspNetCore" Version="1.4.0" />
</ItemGroup>

Once you’ve installed the package, register the services and Middleware in your ASP.NET Core application.

var builder = WebApplication.CreateBuilder(args);

if (builder.Environment.IsDevelopment())
{
    builder.Services.AddViteDevMiddleware();
}
// Add the Vite Manifest Service.
builder.Services.AddViteManifest();

And here’s the registration for Middleware.

if (app.Environment.IsDevelopment())
{
    app.UseViteDevMiddleware();
}

Here is my complete Program.cs file, so you can see where to place each code block.

using Vite.AspNetCore.Extensions;

var builder = WebApplication.CreateBuilder(args);

if (builder.Environment.IsDevelopment())
{
    builder.Services.AddViteDevMiddleware();
}
// Add the Vite Manifest Service.
builder.Services.AddViteManifest();

// Add services to the container.
builder.Services.AddRazorPages();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapRazorPages();
app.MapControllers();

if (app.Environment.IsDevelopment())
{
    app.UseViteDevMiddleware();
}

app.Run();

There’s one more step, which is to connect the client to our Vite development server. We must modify the _Layout.cshtml page with additional environment tags to do that.

<environment include="Development">
    <!-- Vite development server script -->
    <script type="module" src="~/@@vite/client"></script>
    <script defer type="module" src="~/main.ts"></script>
</environment>
<environment include="Production">
    <link rel="stylesheet" href="~/@Manifest["scss/site.scss"]!.File" asp-append-version="true"/>
    <script defer type="module" src="~/@Manifest["main.ts"]!.File" asp-append-version="true"></script>
</environment>

This code block in your layout will depend on your vite.config.ts file and the resulting assets in your manifest. The Vite.AspNetCore package comes with a helpful manifest helper, so you don’t have to know what the artifact name will be on the build, you use the source name instead, and it finds the correct name for you.

If you’ve followed my steps correctly, you should now have an ASP.NET Core application talking to the Vite development server. You can also change any asset and see Vite’s HMR update your page immediately. Very cool!

Caveats

The Vite.AspNetCore project isn’t waiting for the Vite server to start, so initial page loads may not immediately connect to the server. I’ll be attempting to submit a pull request to fix this issue. If you don’t see your assets on the page, try reloading your page.

A Vite caveat, you must import Sass assets in your JavaScript or TypeScript files to take advantage of hot module reloads.

Vite configuration is a dense topic. While the defaults work in most situations, reading more about the topic, precisely things like Rollup plugins, doesn’t hurt.

You may want to exclude your wwwroot from source control, as running the build locally will create and change those files every time. In addition, churn can add a lot of noise to your pull requests and make code reviews a headache.

Conclusion

Vite is a powerful development tool for folks building frontend experiences. The Vite.AspNetCore Middleware is an excellent option for folks who want to merge their front and back end into a single process. I want to thank the author Quetzal Rivera for taking the time to create the package so the community can benefit from their work.

I’ve created a working solution at my GitHub repository if you want to see a functioning sample.

As always, thanks for reading.