This library provides a macro to easily expose an Axum web server, written in Rust, to a Node.js application using NAPI-rs.
This allows you to write high-performance, memory-safe web services in Rust and seamlessly integrate them into a Node.js environment.
axum_napi_bridge is fully tested and compatible with Phusion Passenger, the industry-standard application server for production Node.js deployments. The library supports both:
- Nginx + Passenger - High-performance reverse proxy with automatic process management
- Apache + Passenger - Full-featured web server with enterprise-grade features
All Passenger configurations are thoroughly tested in our CI/CD pipeline using Docker containers to ensure production reliability.
The library provides a macro, napi_axum_bridge!, which generates the necessary N-API boilerplate to bridge your Axum Router to Node.js. It creates an exported function handle_request that can be called from JavaScript with request details.
-
Set up your
Cargo.tomlYou will need to add this library and its dependencies to your
Cargo.toml. Because the bridge uses macros from several other crates, you need to include them as direct dependencies in your project.[package] name = "my-axum-app" version = "0.1.0" edition = "2021" [lib] crate-type = ["cdylib"] [dependencies] axum = "0.8.4" tokio = { version = "1", features = ["full"] } # The bridge library axum_napi_bridge = { git = "https://github.com/your-repo/axum-napi-bridge" } # Or use a path dependency # Dependencies required by the bridge's macros serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" napi = { version = "3.0.0", features = ["tokio_rt", "serde-json"] } napi-derive = "3.0.0" [build-dependencies] napi-build = "2"
-
Use the macro in your Rust code
In your
src/lib.rs(or another source file likesrc/bridge.rs), define a function that returns your AxumRouter, and then pass it to thenapi_axum_bridge!macro.use axum::routing::get; use axum_napi_bridge::napi_axum_bridge; fn my_app() -> axum::Router { axum::Router::new() .route("/", get(|| async { "Hello from my app!" })) .route("/foo", get(|| async { "This is the /foo route." })) } // This generates the bridge code napi_axum_bridge!(my_app);
-
Set up your
package.jsonYou will need a
package.jsonto manage the build process.{ "name": "my-axum-app", "version": "1.0.0", "main": "index.js", "scripts": { "build": "napi build --release" }, "dependencies": { "@napi-rs/cli": "^3.0.0" } } -
Build and run
npm install npm run build
You can now
requirethe generated.nodefile in your JavaScript code and use thehandleRequestfunction.
For a working example, you can create the following files in a new project.
[package]
name = "example-axum-app"
version = "0.1.0"
edition = "2021"
build = "build.rs"
[lib]
crate-type = ["cdylib"]
path = "src/bridge.rs"
[dependencies]
axum = "0.8.4"
tokio = { version = "1", features = ["full"] }
axum_napi_bridge = "0.1.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
napi = { version = "3.0.0", features = ["tokio_rt", "serde-json"] }
napi-derive = "3.0.0"
[build-dependencies]
napi-build = "2"use axum::routing::get;
use axum_napi_bridge::napi_axum_bridge;
use std::time::Duration;
use tokio::time::sleep;
fn my_app() -> axum::Router {
axum::Router::new()
.route("/", get(|| async { "Hello from the example app!" }))
.route("/test", get(|| async { "This is a test route." }).post(|| async { "POST response from test route." }))
.route("/concurrent-test", get(|| async {
sleep(Duration::from_millis(50)).await;
"Concurrent test route."
}))
}
napi_axum_bridge!(my_app);extern crate napi_build;
fn main() {
napi_build::setup();
}{
"name": "example-axum-app",
"version": "1.0.0",
"description": "An example Axum app using the axum-napi-bridge.",
"main": "index.js",
"type": "module",
"scripts": {
"build": "napi build --release",
"test": "playwright test tests/bridge.spec.ts",
"test:e2e": "playwright test tests/example.spec.mjs",
"test:passenger": "playwright test --config=playwright.passenger.config.mjs",
"test:passenger:apache": "playwright test --config=playwright.apache.config.mjs",
"bench": "npx tsc benchmark/bridge_bench.ts --outDir benchmark --target es2022 --module es2022 --esModuleInterop --skipLibCheck --moduleResolution node && node benchmark/bridge_bench.js"
},
"dependencies": {
"@napi-rs/cli": "^3.1.5"
},
"devDependencies": {
"@playwright/test": "^1.55.0",
"tinybench": "^2.4.0",
"typescript": "^5.2.2"
}
}import { test, expect } from '@playwright/test'
import { handleRequest } from '../index.js'
test('GET /', async () => {
const response = await handleRequest('GET', '/', null, null)
const parsed = JSON.parse(response)
expect(parsed.status).toBe(200)
expect(parsed.body).toBe('Hello from the example app!')
})
test('GET /test', async () => {
const response = await handleRequest('GET', '/test', null, null)
const parsed = JSON.parse(response)
expect(parsed.status).toBe(200)
expect(parsed.body).toBe('This is a test route.')
})
test('POST /test', async () => {
const response = await handleRequest('POST', '/test', null, null)
const parsed = JSON.parse(response)
expect(parsed.status).toBe(200)
expect(parsed.body).toBe('POST response from test route.')
})import { handleRequest } from './index.js'
import { createServer } from 'http'
const server = createServer(async (req, res) => {
try {
const body = await new Promise<string | null>((resolve) => {
let data = ''
req.on('data', (chunk) => (data += chunk))
req.on('end', () => resolve(data || null))
})
const result = await handleRequest(req.method!, req.url!, req.headers, body)
const response = JSON.parse(result)
res.writeHead(response.status, response.headers)
res.end(response.body)
} catch (error) {
res.writeHead(500, { 'Content-Type': 'text/plain' })
res.end('Internal Server Error')
}
})
server.listen(process.env.PORT || 3000)
console.log(`Server listening on port ${process.env.PORT || 3000}`)To ensure code quality and prevent issues, install the pre-commit hook that runs all tests before commits:
npm run install-hookThe hook automatically runs:
- Main library tests
- Sample app tests
- Performance benchmarks
- Phusion Passenger deployment tests (Nginx + Apache)
All tests must pass before commits are allowed.
# Install pre-commit hook
npm run install-hook
# Build the library
npm run build
# Run tests
npm test
# Format code
npm run format
# Lint code
npm run lint
# Run benchmarks
npm run benchThe bridge is fully compatible with Nginx + Passenger for high-performance production deployments:
// server.ts
import { handleRequest } from './index.js'
import { createServer } from 'http'
const server = createServer(async (req, res) => {
try {
const body = await new Promise<string | null>((resolve) => {
let data = ''
req.on('data', (chunk) => (data += chunk))
req.on('end', () => resolve(data || null))
})
const result = await handleRequest(req.method!, req.url!, req.headers, body)
const response = JSON.parse(result)
res.writeHead(response.status, response.headers)
res.end(response.body)
} catch (error) {
res.writeHead(500, { 'Content-Type': 'text/plain' })
res.end('Internal Server Error')
}
})
server.listen(process.env.PORT || 3000)
console.log(`Server listening on port ${process.env.PORT || 3000}`)The bridge also works seamlessly with Apache + Passenger for enterprise environments requiring advanced web server features.
Pre-built Docker configurations are available in the repository:
Dockerfile.passenger- Nginx + Passenger setupDockerfile.apache- Apache + Passenger setup
Both configurations use official Phusion Passenger base images and are tested in CI/CD.