Crates.io | axum_napi_bridge |
lib.rs | axum_napi_bridge |
version | 0.1.0 |
created_at | 2025-09-13 16:02:12.743379+00 |
updated_at | 2025-09-13 16:02:12.743379+00 |
description | A bridge to use axum handlers in Node.js |
homepage | https://github.com/Deepthought-Solutions/axum_napi_bridge |
repository | https://github.com/Deepthought-Solutions/axum_napi_bridge |
max_upload_size | |
id | 1837820 |
size | 59,676 |
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:
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.toml
You 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 like src/bridge.rs
), define a function that returns your Axum Router
, and then pass it to the napi_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.json
You will need a package.json
to 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 require
the generated .node
file in your JavaScript code and use the handleRequest
function.
For a working example, you can create the following files in a new project.
Cargo.toml
[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"
src/bridge.rs
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);
build.rs
extern crate napi_build;
fn main() {
napi_build::setup();
}
package.json
{
"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"
}
}
tests/bridge.spec.ts
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.')
})
server.ts
(Optional HTTP Server)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-hook
The hook automatically runs:
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 bench
The 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 setupBoth configurations use official Phusion Passenger base images and are tested in CI/CD.