Using npm Packages in Blazor Applications

Ruslan Dudchenko
Ruslan Dudchenko
28 Dec 2025

7 min read

Introduction

Modern applications often combine different technologies, and this is especially true for projects built with .NET Blazor. While Blazor provides a strong and flexible framework, there are cases where existing JavaScript solutions already solve a problem very well.
In one of our recent projects, we needed to integrate functionality that was already available as an npm package. The package was quite large, highly specialized, and actively maintained by a third-party vendor. Rewriting it in C# was not a good option. It would require significant effort and would make it difficult to keep up with future updates.
Because of this, we decided to use the npm package directly in our Blazor application. However, doing this in a clean and maintainable way is not always obvious, especially in a Blazor setup. Questions quickly appear around project structure, build process, and long-term maintainability.
This article describes an approach that allows npm packages to be used in a Blazor application without mixing frontend tooling into the main app project. The solution keeps responsibilities clearly separated and integrates naturally into the .NET build pipeline.
A complete demo project is available on GitHub and is referenced in this article, so the full setup can be reviewed and reused if needed.

The General Idea Behind the Approach

The idea behind this approach is very simple.
We take an npm package and build it into a single, minified JavaScript file that can be used by the main Blazor application. For this, we use Vite, which handles bundling and dependency resolution for us.
All npm-related work lives in a separate .NET Razor Class Library. This is important because Razor class libraries can expose static web assets automatically. By keeping JavaScript in its own project, we avoid mixing frontend tooling with the main application code.
The build process is controlled from the .csproj file. During a normal .NET build, MSBuild runs the npm build script and then copies the generated JavaScript file into wwwroot. Because the output is placed under wwwroot, it becomes available to the Blazor app as a standard static web asset. This is also why the project type matters — the Razor SDK handles this asset routing for us.
Folders related to npm, such as node_modules, build output, and the _generated folder inside wwwroot, are excluded from Git. These folders contain either third-party dependencies or generated files. They can change between builds, and committing them would only pollute the repository without adding real value.
As a result, the repository stays clean, the build remains reproducible, and the main Blazor application consumes only a stable JavaScript file produced by the build pipeline.

Step-by-Step Demo with simple-statistics npm

To demonstrate this approach, we will use the simple-statistics npm package. It is a good example because it provides useful functionality, has real dependencies, and is clearly something that makes more sense to reuse than to rewrite in C#.
The first step is to create a Razor Class Library. This project will contain all npm-related code and configuration. Inside the project folder, a standard npm setup is initialized by running npm init, which creates the package.json file. After that, the simple-statistics package is added by running npm install simple-statistics. These steps prepare the project to build JavaScript code using npm and Vite.
Next, Vite is configured. Vite is responsible for bundling the npm package and its dependencies into a single JavaScript file. It is added to the project by running npm install vite and then creating a basic vite.config.js file. Below is demo of the vite.config.js file.
import { defineConfig } from "vite";

export default defineConfig({
    build: {
        lib: {
            entry: "src/index.js",
            name: "simpleStats",
            formats: ["iife"],
            fileName: () => "index.js",
        },
        outDir: "dist",
        emptyOutDir: true,
        sourcemap: true,
        minify: true,
    },
});
A small JavaScript entry file is defined, usually under a src folder. This file imports the required functions from simple-statistics and exposes them in a simple form that can later be called from Blazor using JavaScript interop. When the Vite build runs, it processes this entry file and produces a single, minified JavaScript file as output. Below is demo index.js that exposes functions to be used in Blazor.
import * as ss from "simple-statistics";

export function analyzeNumbers(values) {
    const nums = (values ?? []).map(Number).filter((x) => Number.isFinite(x));

    if (nums.length === 0) {
        return [{ metric: "error", value: "No valid numbers" }];
    }

    const sorted = [...nums].sort((a, b) => a - b);

    return [
        { metric: "count", value: nums.length },
        { metric: "min", value: ss.min(nums) },
        { metric: "max", value: ss.max(nums) },
        { metric: "mean", value: ss.mean(nums) },
        { metric: "median", value: ss.medianSorted(sorted) },
        { metric: "stdev(sample)", value: nums.length > 1 ? ss.sampleStandardDeviation(nums) : 0 },
        { metric: "variance(sample)", value: nums.length > 1 ? ss.sampleVariance(nums) : 0 },
        { metric: "q25", value: ss.quantileSorted(sorted, 0.25) },
        { metric: "q75", value: ss.quantileSorted(sorted, 0.75) },
    ];
}

export function analyzeColumn(rows, column) {
    const nums = (rows ?? [])
        .map((r) => r?.[column])
        .map(Number)
        .filter((x) => Number.isFinite(x));

    return analyzeNumbers(nums);
}

export function linearRegressionTable(rows, xKey, yKey) {
    const points = (rows ?? [])
        .map((r) => [Number(r?.[xKey]), Number(r?.[yKey])])
        .filter(([x, y]) => Number.isFinite(x) && Number.isFinite(y));

    if (points.length < 2) {
        return [{ metric: "error", value: "Need at least 2 valid (x,y) points" }];
    }

    const lr = ss.linearRegression(points);              // { m, b }
    const line = ss.linearRegressionLine(lr);            // function(x) => y

    const ys = points.map((p) => p[1]);
    const yMean = ss.mean(ys);
    const ssTot = ys.reduce((acc, y) => acc + (y - yMean) ** 2, 0);
    const ssRes = points.reduce((acc, [x, y]) => acc + (y - line(x)) ** 2, 0);
    const r2 = ssTot === 0 ? 1 : 1 - ssRes / ssTot;

    return [
        { metric: "count", value: points.length },
        { metric: "slope(m)", value: lr.m },
        { metric: "intercept(b)", value: lr.b },
        { metric: "r2", value: r2 },
    ];
}
The JavaScript build is triggered from the .csproj file. During the .NET build process, MSBuild runs the npm build script and waits for it to finish. Below is demo of .csproj file.
<Project Sdk="Microsoft.NET.Sdk.Razor">
    <PropertyGroup>
        <TargetFramework>net9.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>

        <Configurations>Debug;Release;</Configurations>
        <Platforms>AnyCPU</Platforms>
    </PropertyGroup>

    <ItemGroup>
        <SupportedPlatform Include="browser" />
    </ItemGroup>
    
    <ItemGroup>
        <PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="9.0.2" />
    </ItemGroup>

    <Target Name="RunNpmBuild" BeforeTargets="Build">
        <Exec Command="npm install" WorkingDirectory="$(ProjectDir)"/>
        <Exec Command="npm run build" WorkingDirectory="$(ProjectDir)"/>
    </Target>

    <Target Name="CopyBuiltJsToWeb" BeforeTargets="Build">
        <Copy
                SourceFiles="$(ProjectDir)bin\dist\index.js"
                DestinationFiles="wwwroot\_generated\index.js"/>
    </Target>
</Project>
After that, the generated JavaScript file is copied into the _generated folder under wwwroot. Because this folder is part of wwwroot, the file is automatically exposed as a static web asset. Below is an image demonstrating the _generated folder in the project structure.
At this point, no manual steps are required. Running dotnet build or building the solution from an IDE produces the JavaScript file every time, ensuring that the output is always in sync with the npm sources.
In the main Blazor application, the generated file is referenced using a normal <script> tag. From the application’s point of view, this JavaScript file is no different from any other static file. Blazor JavaScript interop can then be used to call the functions provided by simple-statistics.
All npm-related folders, including node_modules, intermediate build output dist, and the _generated folder, are excluded from version control (in .gitignore). These files are generated automatically and may change between builds, so keeping them out of Git helps maintain a clean and stable repository.
This completes the setup. The Blazor application consumes a stable JavaScript bundle, while the npm package remains isolated, easy to update, and fully controlled by the build pipeline. The resulting project structure of the .Net peoject that builds npm looks like on the image below.

Why This Works Well in Practice

This approach has a few advantages that became very clear over time.
First, JavaScript and .NET responsibilities are clearly separated. The Blazor app remains clean and focused on UI and application logic, while the npm project behaves like any other frontend library.
Second, updates are easy. When the vendor releases a new version of the npm package, we update package.json, rebuild, and we’re done. No rewrites, no fragile ports.
Third, the build is deterministic. Everything that ends up in wwwroot/_generated is produced by the build pipeline, not by hand. That makes CI/CD predictable and reduces “works on my machine” problems.
Finally, this pattern works just as well for MAUI Blazor as it does for regular ASP.NET Core Blazor apps, which was a major requirement for us.
Below is screenshot of the of the app showing usage of the npm package functions.

Final Thoughts

If you are working with Blazor or MAUI Blazor and need to integrate complex or actively maintained npm packages, this type of setup may be worth considering. It allows responsibilities to remain clearly separated, scales well as projects grow, and fits naturally with both the JavaScript and .NET ecosystems.
If you have questions about any part of this integration, want to discuss the approach in more detail, or are working on something similar, feel free to reach out. You can contact me directly or connect with me on LinkedIn — I’m always open to technical discussions and feedback.
Thank you for reading, and happy coding!

You may also read

Streaming Log Data Across Networks: Building a Real-Time Log Pipeline in .NET and Blazor
A deep dive into how we built a chunked NDJSON streaming pipeline that moves API logs from a remote client application, through a relay API, to a Blazor viewer — with two modes of consumption, a Command Pattern foundation, and zero buffering on the server.
Andrii Kozmenchuk
Andrii Kozmenchuk
28 May 2026
Implementing Google OAuth in a .NET MAUI App Using an ASP.NET Backend
Adding Google authentication to an application often looks simple at first glance. The idea is straightforward: the user clicks a login button, signs in with Google, and returns to the application already authenticated. Many tutorials make it appear as if the process requires only a few configuration steps and a small amount of code.
Oleksandr Hutsul
Oleksandr Hutsul
13 Mar 2026
Generating Code 128C Barcodes in Blazor: A Step-by-Step Guide
This article delves into the implementation of barcode rendering, focusing specifically on Code 128C, using Blazor and C#. The goal is to generate barcodes entirely within a Blazor application by implementing the encoding algorithm in C#. While this article primarily focuses on Code 128C, the same algorithmic approach can be adapted to render other types of barcodes, making it a versatile solution for various use cases.
Ruslan Dudchenko
Ruslan Dudchenko
22 Nov 2024

What our clients say

Senior Angular Developer

Aug 11, 2025

Need help fixing an Azure Key Vault issue in Blazor Server

Apr 3, 2025

Need help fixing Graph connection issue in Blazor Server (prod env)

Mar 11, 2025

C# Blazor + EF Core Tutor — 1:1 mentoring / pair debugging

Feb 13, 2026

Azure Pipeline for .Net Maui

Jan 4, 2025

Epic Solutions Grocery project

Dec 9, 2024

.NET C# Blazor Developer for CRM Rewrite

Oct 6, 2022

Excellent work from start to finish. The developer resolved all problems in our Angular app and delivered new features without any fuss. Professional, reliable, and highly skilled.

FAQ

Do you provide support after launch?

Yes, we provide post-launch support, including maintenance, updates, bug fixes, and product improvements as needed.

How long does it take to develop a website or an app?

The timeline depends on the project complexity. On average: a website takes 2–6 weeks, and an app takes 2–4 months.

Can you help if I don’t have a clear idea yet?

Yes, we help shape your idea, define features, and create a product concept.

How much does project development cost?

The cost depends on the scope and complexity. We provide a custom estimate after discussing your requirements.

Can I order a redesign of an existing website or app?

Yes, we offer redesign services, improve UX/UI, and update the functionality of existing products. We also have strong experience working with legacy systems and complex codebases.

Is it possible to order only design or development?

Yes, you can order design or development separately based on your needs.

Which countries do you work with?

We work with clients worldwide and have experience with international projects.

Let’s team up!

Fill out the form to get in touch

By clicking this button I accept Privacy Policy of this site.

Got an idea?

We're all ears 😊

Drop us a line

admin@vertexcode.dev