axum_napi_bridge

Crates.ioaxum_napi_bridge
lib.rsaxum_napi_bridge
version0.1.0
created_at2025-09-13 16:02:12.743379+00
updated_at2025-09-13 16:02:12.743379+00
descriptionA bridge to use axum handlers in Node.js
homepagehttps://github.com/Deepthought-Solutions/axum_napi_bridge
repositoryhttps://github.com/Deepthought-Solutions/axum_napi_bridge
max_upload_size
id1837820
size59,676
nka11 (nka11)

documentation

https://docs.rs/axum_napi_bridge

README

Axum N-API Bridge

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.

✅ Production-Ready with Phusion Passenger

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.

How it Works

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.

Getting Started

Prerequisites

Usage

  1. 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"
    
  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);
    
  3. 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"
      }
    }
    
  4. 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.

Example

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}`)

Development

Pre-commit Hook

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:

  • Main library tests
  • Sample app tests
  • Performance benchmarks
  • Phusion Passenger deployment tests (Nginx + Apache)

All tests must pass before commits are allowed.

Development Commands

# 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

Production Deployment with Phusion Passenger

Nginx + Passenger

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}`)

Apache + Passenger

The bridge also works seamlessly with Apache + Passenger for enterprise environments requiring advanced web server features.

Docker Deployment

Pre-built Docker configurations are available in the repository:

  • Dockerfile.passenger - Nginx + Passenger setup
  • Dockerfile.apache - Apache + Passenger setup

Both configurations use official Phusion Passenger base images and are tested in CI/CD.


Commit count: 71

cargo fmt