Using npm (Node Package Manager) with .NET Razor pages

, by Catalin Gavan

As the modern frontend development has greatly evolved in the past years, some once very popular JavaScript libraries like jQuery, Knockout.js, Moment.js, RequireJS, Bootstrap are now considered legacy.

But, many web applications still use the "legacy" web libraries successfully, either from technical constraints, or simply by preference. For example, Azure Portal, one of the most popular cloud providers, is currently using jQuery, Knockout.js and RequireJS for its web platform.

Since NuGet has never been great at managing UI libraries, I created a simple scaffolding setup for integrating npm (Node Package Manager) into legacy .NET Razor-based web applications.

The stack looks like this:

  • npm manages third-party JavaScript/CSS packages
  • Gulp copies distributable files from node_modules into wwwroot
  • .NET (Razor pages) - hosts the web application

The solution is simple yet very efficient. Additionally, no special static files configuration is needed for the .NET application, as other tutorials suggest.

Setup npm in your .NET web project

1. In your .NET web application project folder, create a package.json file.

{
  "name": "my-web-application",
  "version": "1.0.0",
  "scripts": {},
  "dependencies": {},
  "devDependencies": {}
}

2. If you haven't done this already, update the .gitignore file to exclude the node_modules folder which will soon be populated by the npm (Node Package Manager). We also ignore the wwwroot\lib folder, as this folder will be populated automatically.

node_modules/
**/wwwroot/lib/

# other entries here

3. Install the libraries your application uses, by using the npm cli.

C:\Projects\MyApp\WebApplication.AspNetCore>
npm i jquery
npm i bootstrap
npm i bootstrap-icons

This will update your package.json file, and will download the respective libraries in the node_modules folder.

{
  "name": "my-web-application",
  "version": "1.0.0",
  "scripts": {},
  "dependencies": {
    "bootstrap": "^5.3.8",
    "bootstrap-icons": "^1.13.1",
    "jquery": "^3.7.1"
  }
}

node_modules folder

Files inside node_modules are not accessible to the .NET server

By default, .NET web applications expose only the files inside the wwwroot folder. Having the libraries (jQuery, Bootstrap, bootstrap-icons etc.) in the node_modules folder does not make them accessible for the web server yet.

However, copying the files into the wwwroot folder, would make them accessible:

http://localhost:5168/node_modules/jquery/dist/jquery.min.js - 404 NotFound
http://localhost:5168/jquery/dist/jquery.min.js              - 200 OK

If we can automate copying the files from node_modules to the wwwroot folder, we can fully benefit from the npm package manager for our .NET web application.

Copy files from node_modules into wwwroot

I used Gulp to automate copying files from node_modules folder into wwwroot\lib folder, thus making them accessible by the .NET web server (and implicitly for the browsers).

Beside helping with copying the files, Gulp can also be used to minify JavaScript files, compile *.scss files, combine and minify multiple .css files and many more automation tasks useful for a web application.

1. Install gulp as a dev dependency:

C:\Projects\MyApp\WebApplication.AspNetCore>
npm install gulp --save-dev
npm install gulp-rename --save-dev

This will update the package.json file:

{
  "name": "my-web-application",
  "version": "1.0.0",
  "scripts": {},
  "dependencies": {
    "bootstrap": "^5.3.8",
    "bootstrap-icons": "^1.13.1",
    "jquery": "^3.7.1"
  },
  "devDependencies": {
    "gulp": "^5.0.1",
    "gulp-rename": "^2.1.0"
  }
}

2. Create a gulpfile.js in your .NET web project:

const gulp = require('gulp');
const path = require('path');
const rename = require('gulp-rename');

gulp.task('copy-lib', function () {
    return gulp
        .src([
            './node_modules/bootstrap/dist/js/bootstrap.bundle.min.js',
            './node_modules/bootstrap/dist/css/bootstrap.min.css',
            './node_modules/jquery/dist/jquery.min.js',
            './node_modules/bootstrap-icons/font/**/*'
        ], { base: '.' })
        .pipe(rename(function (p) {
            const dirname = p.dirname.substring(`node_modules${path.sep}`.length);
            p.dirname = dirname;
        }))
        .pipe(gulp.dest('./wwwroot/lib'));
});

We define a Gulp task copy-lib which iterates over a list of node_modules files (or folders), and copies them into wwwroot/lib/*.

3. Update the package.json file and define a new gulp:copy-lib script action:

{
  "name": "my-web-application",
  "version": "1.0.0",
  "scripts": {
    "gulp:copy-lib": "gulp copy-lib"
  },
  "dependencies": {
    "bootstrap": "^5.3.8",
    "bootstrap-icons": "^1.13.1",
    "jquery": "^3.7.1"
  },
  "devDependencies": {
    "gulp": "^5.0.1",
    "gulp-rename": "^2.1.0"
  }
}

4. Run the gulp:copy-lib script:

C:\Projects\MyApp\WebApplication.AspNetCore>
npm run gulp:copy-lib

> my-web-application@1.0.0 gulp:copy-lib
> gulp copy-lib

[09:29:05] Using gulpfile ~\Projects\MyApp\WebApp.AspNetCore\gulpfile.js
[09:29:05] Starting 'copy-lib'...
[09:29:05] Finished 'copy-lib' after 66 ms

After running gulp:copy-lib task, we can now see that all the files have been copied into wwwwroot\lib folder:

node_modules folder

This means that they are now accessible to the .NET web server:

GET http://localhost:5290/lib/bootstrap/dist/css/bootstrap.min.css - 200 OK node_modules folder

We can now import the library files directly from the wwwroot folder.

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Web app</title>

    <link href="~/lib/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet" />
    <link href="~/lib/bootstrap-icons/font/bootstrap-icons.css" rel="stylesheet" />
</head>
<body>
    <div class="container text-center py-5">
        <h1 class="display-6 py-2">
            .NET + <span class="fw-semibold">npm</span> + <span class="fst-italic fw-semibold text-danger">Gulp</span>
        </h1>

        <button class="btn btn-light" id="btn">
            Bootstrap button <i class="bi bi-gear-fill"></i>
        </button>
    </div>

    <script src="~/lib/jquery/dist/jquery.min.js"></script>
    <script>
        $("#btn").on("click", function() {
            alert("Hello world!");
        });
    </script>
</body>
</html>

.NET + npm + Gulp

Final thoughts

Earlier, I suggested updating the .gitignore to ignore the /**/wwwroot/lib directory. I don't think it is necessary to commit the library files, because they can always be downloaded and copied locally, using npm i followed by npm run gulp:copy-lib actions.

This practice also prevents accidental modifications inside vendor packages and ensures deterministic builds.

Additionally, this template works very well with CI/CD workflow.

name: Publish WebApp (simplified)
jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Install node.js
        uses: actions/setup-node@v3
        env:
          NODE_ENV: localhost

      - name: Prepare wwwroot
        working-directory: ./src/WebApp.AspNetCore
        run: |
          rm -rf wwwroot/lib
          npm install
          gulp --color copy-lib

      - name: Setup .NET
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: 8.0.x

      - name: Build project
        run: |
          dotnet publish ./src/WebApp.AspNetCore/WebApp.AspNetCore.csproj --configuration Release --runtime linux-x64 --no-self-contained --output ./publish
          ls ./publish
          
      - name: Replace wwwroot
        run: |
          rm -rf ./publish/wwwroot
          cp -r ./src/WebApp.AspNetCore/wwwroot ./publish/
          
      # release the artifacts

With this approach, we don't need to manually download and copy library files in our (legacy) .NET web applications anymore. We can simply use Node Package Manager (npm) to manage the user-interface dependencies for our projects.