node-api-dotnet: a Node API surface for calling .NET in-process
Advanced interoperability between .NET and JavaScript in the same process.
At a glance
- What is it?
- node-api-dotnet is a Microsoft project that lets a JavaScript application load .NET assemblies and call .NET APIs in-process, and lets a .NET application load JavaScript packages and call JavaScript in-process, both across the Node API so no recompilation is needed per runtime version. It is marked Public Preview, which is the most important fact in its README.
- Who is it for?
- Adopt node-api-dotnet if you have an existing .NET codebase and an existing JavaScript codebase and you want them in the same process, with the Node API as the stable boundary and no recompilation per Node version. Do not adopt it for a production service that cannot tolerate API churn, because the README marks it Public Preview with known limitations around the edges and possible minor breaking API changes.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 1 day ago.
- What is it written in?
- Mainly C#, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Two runtimes in one process
node-api-dotnet enables advanced interoperability between .NET and JavaScript in the same process, and the two directions are stated as separate capabilities.
From JavaScript, you can load .NET assemblies and call .NET APIs in-process. From .NET, you can load JavaScript packages and call JavaScript APIs in-process. Neither direction is a network call or a subprocess, which is the point: the boundary is a function call inside one address space.
That framing matters for what kind of project this is. A .NET application that wants to reuse a JavaScript library, or a Node application that wants to call into an existing .NET service library, is the use case. Both sides keep their own language and their own toolchain, and the interop layer is what lets them share.
The README also lists what the interop supports beyond plain function calls: it is described as high performance, it supports TypeScript type definitions generation, async through tasks and promises, and streams. The type definitions generation is the detail that makes it usable from TypeScript rather than only from JavaScript, since the whole point of the Node API is that a JavaScript consumer wants types.
This is a Microsoft project, developed under a Microsoft Open Source Code of Conduct, and contributions require agreement to the Contributor License Agreement.
Why the Node API is the interoperability boundary
The design decision that makes this practical is the choice of Node API as the bridge rather than a bespoke interface.
Node API is the stable binary interface that Node.js exposes to native addons, and using it means the interop layer is not tied to a particular Node version. The README states that it is compatible with any Node.js version, without recompiling, or with another JavaScript runtime that supports Node API.
That is a substantial difference from most cross language bridges, which compile against a specific V8 or JavaScriptCore version and need rebuilding when the host moves. Here, the host is whatever JavaScript runtime you have, and the .NET side loads into that process. The cost of that portability is the usual cost of a stable ABI: the interface is limited to what the Node API can express, so advanced .NET features that do not map onto a JavaScript call do not travel.
The alternative approaches each give up something different. Embedding a JavaScript engine in .NET directly gives more control over the engine and the objects you expose, at the cost of owning the embedding and the lifetime rules yourself. Spawning a child process for each call keeps the two worlds separate but throws away the shared memory and the in-process benefit. Choosing Node API means accepting a narrower, more portable interface in exchange for not writing an engine binding at all.
The minimal examples and the shape of the API
The README gives one minimal example per direction, and the JavaScript side is short enough to read in full:
// JavaScript
const Console = require('node-api-dotnet').System.Console;
Console.WriteLine('Hello from .NET!'); // JS writes to the .NET console APIThe .NET assembly is reachable as a namespace on the required module, and the type is then used as if it were a local .NET type. The comment is the point of the example: JavaScript writes to the .NET console API.
The other direction is more involved, and the shape of it is the useful information. You declare an interface for the JavaScript side, in this case a simple IConsole with a Log method taking a string, then create a NodejsPlatform for a given libnode path and call CreateEnvironment on it. You get a nodejs handle, run a block of work with nodejs.Run, and inside that block you import the JavaScript console object with nodejs.Import of your interface type, specifying the global scope and the name console. Then you call the method as if it were a local .NET object.
So the .NET side describes the JavaScript API it wants as a C# interface, and the runtime binds to it. That is a familiar shape for anyone who has written an interface and expected something else to implement it, and it is why the example can be written without a build step or a code generation pass in the snippet.
npm package or NuGet package, chosen by scenario
The packaging decision follows the direction of the call, and the README points at a scenarios page rather than picking one for you.
The NPM package is node-api-dotnet, and the NuGet package is Microsoft.JavaScript.NodeApi. A JavaScript application that loads .NET assemblies is an npm consumer, and a .NET application that loads JavaScript packages is a NuGet consumer. The packages and releases page holds the details, and the scenario is what determines which one you take.
This packaging choice has a practical consequence that is easy to miss. A .NET project that wants to expose an API to a JavaScript application does not install the NuGet package for its own use; the JavaScript application installs the npm package, and the .NET assembly it loads has to be available to that application at runtime. The interop is therefore a deployment arrangement, not just a reference, and the assembly loading is part of what you have to get right in a shipped product rather than a demo.
The documentation site, published at microsoft.github.io/node-api-dotnet, is where the getting started guidance, the feature details and the API reference live, and it is the place to check the current shape of the API before committing to it.
Public Preview and what the edge limitations mean
The status line in the README is the most consequential sentence in the document: this project is in Public Preview, most functionality works well, though there are some known limitations around the edges, and there may still be minor breaking API changes.
Three things follow from that wording, and they are worth separating.
First, the preview is about the API surface, not about the code quality. The two minimal examples work because the interesting parts of the interop, calling across the boundary, are the well trodden path. The unknown territory is at the edges, which in practice means the unusual cases: unusual object lifetimes, values that do not map cleanly onto the Node API, error propagation in unusual shapes, and the interaction with the JavaScript garbage collector and the .NET finalizer.
Second, minor breaking API changes are possible. For a project that pins a version and upgrades on a schedule, that is a cost to plan for. For a project that wants to try the approach in a side branch, it is not.
Third, there is no version to pin in this repository. The repository publishes no GitHub releases, and the versioning that matters is in the two packages, so the packages and releases page is the reference for what is current.
The example set as a map of the interop surface
The examples directory is the best catalogue of what the project actually supports, and it is longer than the README's feature list would suggest.
There are several .NET shaped examples, including dotnet-dynamic, dotnet-dynamic-classlib and dotnet-module, which cover calling into .NET from JavaScript with dynamic objects, with a class library, and in a module layout. On the JavaScript side there are aot-module and aot-npm-package, which cover the direction where JavaScript is packaged and loaded, including an ahead of time compilation path.
Then there are the host integration examples, and they are the interesting ones because they show the interop surviving contact with real frameworks. There is an electron example, which is the case most people care about since an Electron main process is a .NET capable host in practice. There is a jsdom example, a hermes-engine example for the React Native JavaScript engine, a semantic-kernel example, and three Windows stack examples, winappsdk, winui-fluid and wpf, which show the bridge inside desktop application frameworks.
Reading those examples is a better evaluation than reading the feature list, because each one is a worked answer to a question an adopter will have, such as whether the interop survives an Electron main process or a WPF application.
Where node-api-dotnet is the wrong tool
Three cases point elsewhere.
The first is a deployment that cannot add a native component. node-api-dotnet is a native, in-process bridge. If your JavaScript runs in a browser tab, in a sandboxed worker, or on a platform where you cannot ship a native addon, none of this applies, and a web API between services is the only option. The README's own framing, interoperability in the same process, excludes those environments by definition.
The second is a case where a service boundary is what you actually want. Two services talking over HTTP can be deployed independently, scaled separately, and debugged with ordinary network tooling. In-process interop trades that for latency and simplicity, which is the right trade for a desktop application and the wrong one for two independently deployed services that happen to need to talk.
The third is a .NET application that only needs to call one JavaScript library. The same example code shows how much machinery sits underneath: a platform object, an environment, an import of a typed handle, and a run block. If the library has a .NET binding, or a WebAssembly build, or a REST interface, using one of those avoids the interop layer entirely.
One more consideration is the preview status itself. The licence is MIT and the code is available, but a Public Preview with possible breaking changes and known edge limitations is a reasonable thing to spike in a branch and a poor thing to put between your users and a production release without a plan for the upgrades.
Editorial conclusion
Adopt node-api-dotnet if you have an existing .NET codebase and an existing JavaScript codebase and you want them in the same process, with the Node API as the stable boundary and no recompilation per Node version. Do not adopt it for a production service that cannot tolerate API churn, because the README marks it Public Preview with known limitations around the edges and possible minor breaking API changes. Verify three things first: which direction you need, since the npm and NuGet packages are chosen by scenario rather than by preference; whether the .NET and JavaScript components you want to bridge can actually be loaded on the runtime you target, because the host has to exist before the bridge does; and which release you pin, since the repository publishes no GitHub releases and the packages are the versioning surface. The licence is MIT, the last push was on 2026-10-16, and contributions require agreement to the Contributor License Agreement.
Frequently asked questions
What does node-api-dotnet let you do?
It enables interoperability between .NET and JavaScript in the same process. A JavaScript application can load .NET assemblies and call .NET APIs in-process, and a .NET application can load JavaScript packages and call JavaScript APIs in-process.
How does node-api-dotnet avoid being tied to one Node.js version?
It is built on the Node API, the stable binary interface for native addons, so the README states it is compatible with any Node.js version without recompiling, or with another JavaScript runtime that supports Node API.
Which packages do I install?
The NPM package is node-api-dotnet and the NuGet package is Microsoft.JavaScript.NodeApi. Which one you need depends on the scenario, and the README links to the scenarios page and to the packages and releases documentation for the details.
Is node-api-dotnet ready for production use?
It is marked Public Preview. The README says most functionality works well, though there are some known limitations around the edges, and there may still be minor breaking API changes.
How does a .NET application call into JavaScript?
You declare a C# interface for the JavaScript API you want, create a NodejsPlatform for a libnode path, call CreateEnvironment on it, and inside a nodejs.Run block import the object with nodejs.Import of your interface type, for example nodejs.Import<IConsole>("global", "console"), then call its methods.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/microsoft-node-api-dotnet)Was this useful?