While Node.js is a powerful JavaScript runtime environment, it has its limitations — especially in terms of performance and efficiency. Tools like Neon can enhance those aspects of Node.js applications, leveraging the power of Rust to provide a more streamlined experience.
Similar to WebAssembly (Wasm), Neon allows you to use Rust in your project. However, while Wasm converts your Rust code to Wasm, Neon compiles your Rust code to native code. This means the native code Neon produces runs faster than WebAssembly.
Unlike WebAssembly, Neon allows you to use almost all — if not all — of Rust’s features. This enables you to bring Rust’s benefits into your Node.js project, such as its speed, efficiency, ecosystem, and more.
In this article, we’ll look into what Neon is and how you can use it in your application. Jump ahead:
To follow along with the practical examples demonstrated in this article, you’ll need Node.js and Rust installed. You can check out the code samples below in this GitHub repository.
Neon allows developers to create native bindings for Node.js applications with Rust. Native bindings are libraries compiled to execute directly on your system’s hardware. You can also call them native libraries.
Native bindings operate when a piece of code interacts with them. They are similar to Node modules, but compile to native code.
You can use native libraries to speed up some necessary project processes. Any code interacting with a native library will experience the performance benefits of that library running directly on the system.
Creating a Neon project is simple, but as previously mentioned, you need to have Node.js and Rust installed on your system. Neon depends heavily on these two programming languages.
If you have everything ready, open your terminal and type in this command:
npm init neon my-project
Your terminal should be in the folder where you want your project because this command creates a new folder with your project name — the last string of text without whitespace at the end of the command — as its name. In this case, my-project
would be the project name.
The project folder will have this structure:
. ├── Cargo.toml ├── README.md ├── package.json └── src └── lib.rs
If you’re interested in optimizing a Node project’s structure, check out our guide to best practices for Node.js project architecture. Otherwise, let’s take a closer look at the unique structure of our Neon project.
If you look at the project structure, you’ll notice that the project seems like a combination of Node.js and Rust library projects. This is how all Neon projects are structured.
You’ll write all the Rust bindings in the Rust src/lib.rs
library file, while your JavaScript files can live anywhere in the project folder.
Let’s look at the Rust library file in more detail.
lib.rs
The Rust library file contains the following code:
use neon::prelude::*; fn hello(mut cx: FunctionContext) -> JsResult<JsString> { Ok(cx.string("hello node")) } #[neon::main] fn main(mut cx: ModuleContext) -> NeonResult<()> { cx.export_function("hello", hello)?; Ok(()) }
This code has three components:
hello
that Neon can export into the JavaScript environmentmain
function allowing Neon to export all your functions into the JavaScript environmentNow let’s go into more detail about the hello
and main
functions.
Let’s start with the hello
function’s argument, cx
. cx
is a reference to the JavaScript context through a function. With this argument, you can make function-like interactions and create and initialize values accessible from the JavaScript environment.
In this function, cx
creates a string value of "hello node"
with the type JsString
in the Rust environment.
Now, look at the hello
function’s return type — JsResult<JsString>
. This indicates Neon expects the function to have a JsResult
type when you’re exporting it, so you must wrap the value you want to return in a JsResult
type.
The hello
function has its return value wrapped in Ok
, a variant of Rust’s built-in Result
type. JsResult
implements the Result
type so that the function can accept Ok
just fine.
Next, let’s look at the main
function. This is the function that Neon needs to work with to know which functions to export to JavaScript.
The main
function can have another name, but you need to mark that function with the #[neon::main]
attribute so Neon knows what role the function should play.
Like in the hello
function, cx
is a reference to the JavaScript context. But in the main
function, cx
allows modular-based interactions and exports hello
to the JavaScript environment. You can also create values and export those values using this cx
.
Finally, let’s look at the main
function’s return type: NeonResult<()>
. As our primary Neon function, the main
function must return a NeonResult
type. NeonResult
is an extended version of Rust’s built-in Result
type, which fits better into the function’s role.
Now, let’s interact with the Rust library using JavaScript. Before you do that, you first need to install the JavaScript dependencies with npm i
and compile the Rust library with npm run build
.
Once you’ve done so, you can test it with Node’s interactive shell:
% node Welcome to Node.js v18.16.0. Type ".help" for more information. > const mod = require(".") undefined > mod.hello() 'hello node' >
You can also try it in a normal JavaScript file. Create an index.js
file, copy the code below into it, and run node index.js
:
const mod = require("."); console.log(mod.hello());
In this section, I’ll give a few examples demonstrating what you can do with Neon. I’ll give you the Rust function, the JavaScript interaction, and the outputs.
Having the ability to create objects enables you to build much more complex data structures with your Rust function. This, in turn, increases the scope of what you can do with Rust in your Node.js project.
This example shows how you can create an empty JavaScript object and store string and number data in its fields. Here’s the function in Rust:
fn get_user(mut cx: FunctionContext) -> JsResult<JsObject> { / Create an empty object let obj = cx.empty_object(); // Create values to store in the object let name = cx.string("Chigozie"); let age = cx.number(19); // Store these values in the object obj.set(&mut cx, "name", name)?; obj.set(&mut cx, "age", age)?; Ok(obj) } // `main` function cx.export_function("getUser", get_user)?;
To interact with this Rust function from our Node.js environment, we can use the following JavaScript code:
const mod = require("."); console.log(mod.getUser());
Then, when executed, the output should display the following:
% node index.js { name: 'Chigozie', age: 19 }
Reading and writing files is one of Rust’s standout features, thanks to its emphasis on safety, performance, and expressiveness.
JavaScript is great for web and lightweight applications. However, if you’re building a data-intensive application that requires high performance, Rust’s speed and efficiency can make it more suitable than JavaScript.
The following is an example of file I/O in Rust:
use std::fs::File; use std::io::prelude::*; fn write_file(mut cx: FunctionContext) -> JsResult<JsBoolean> { if let Ok(mut file) = File::create("foo.txt") { if let Ok(()) = file.write_all(b"Hello, world!") { Ok(cx.boolean(true)) } else { Ok(cx.boolean(false)) } } else { Ok(cx.boolean(false)) } } fn read_file(mut cx: FunctionContext) -> JsResult<JsString> { match File::open("foo.txt") { Ok(mut file) => { let mut contents = String::new(); if let Ok(_) = file.read_to_string(&mut contents) { Ok(cx.string(contents)) } else { Ok(cx.string("")) } } Err(_) => Ok(cx.string("")), } } // `main` function cx.export_function("readFile", read_file)?; cx.export_function("writeFile", write_file)?;
Here’s the JavaScript interaction you’d use to bridge the gap between Rust and Node.js to use the above functions in your application:
const mod = require("."); if (mod.writeFile()) { console.log(mod.readFile()); } else { console.log("couldn't write file"); }
After running the script, you should see this output:
% node index.js Hello, world!
Sometimes, you need to pass arguments to your Rust function, and you need your function to handle arguments that it receives from the JavaScript environment.
This example shows how you can make your Rust function handle arguments from JavaScript. Let’s start with the Rust code:
fn add(mut cx: FunctionContext) -> JsResult<JsNumber> { let num1 = cx .argument::<JsNumber>(0)? // Access the first argument .value(&mut cx); let num2 = cx .argument::<JsNumber>(1)? // Access the second argument .value(&mut cx); Ok(cx.number(num1 + num2)) } // `main` function cx.export_function("add", add)?;
Here’s how you’d invoke the Rust function using JavaScript:
console.log(mod.add(1, 3));
Finally, you should get this output:
% node index.js 4
Neon is a powerful tool that dramatically improves Node.js projects with the power and safety of Rust. Not only that, but it also allows you to use all of Rust’s features. I hope this guide has helped you a lot with understanding it.
You can check out the GitHub repo for this tutorial to see the code examples we used. If you have any questions about using Neon as a bridge between Node.js and Rust, feel free to comment them below. Thanks for reading!
Deploying a Node-based web app or website is the easy part. Making sure your Node instance continues to serve resources to your app is where things get tougher. If you’re interested in ensuring requests to the backend or third-party services are successful, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens while a user interacts with your app. Instead of guessing why problems happen, you can aggregate and report on problematic network requests to quickly understand the root cause.
LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. Start monitoring for free.
Nobody wants their design to confuse users. Running a UX audit will uncover all the “oops” moments in your UI and help fix what’s broken and craft UX that “wow”s users.
Explore use cases for using npm vs. npx such as long-term dependency management or temporary tasks and running packages on the fly.
Whether you’re seeking a fresh challenge or simply curious, this guide provides a roadmap to one of the most dynamic transitions in tech.
Bryanne Pashley talks about how she enhances and develops soft skills, such as empathy, within her team.