arpon-express-inertia-bridge
v1.1.1
Published
Express middleware bridge for Inertia.js with Vite (React + Vue adapter mode)
Maintainers
Readme
arpon-express-inertia-bridge
Express middleware for Inertia.js + Vite that lets you build server-driven React or Vue apps without a separate API layer.
This package adds res.inertia() and res.share() to Express responses, handles Inertia HTML/JSON responses, supports SSR fallback behavior, and provides Laravel-style Vite helpers for EJS templates.
Why use this package
- Works with Express 4/5, React and Vue 3
- Supports first-page HTML render + Inertia XHR JSON responses
- Built-in head/title handling for better SEO output
- Works with EJS root views, custom template files, or custom template functions
- Supports Vite dev server and production manifest modes
Installation
npm i arpon-express-inertia-bridge express ejsInstall your frontend stack as usual:
# React
npm i @inertiajs/react react react-dom
# Vue 3
npm i @inertiajs/vue3 vueQuick start (zero config)
1. Create server.js
import express from "express";
import inertiaExpress from "arpon-express-inertia-bridge";
const app = express();
app.set("view engine", "ejs");
app.use(inertiaExpress());
app.get("/", async (_req, res) => {
await res.inertia("Home", { title: "Dashboard" });
});
app.listen(3000, () => console.log("http://localhost:3000"));2. Create views/base.ejs
<!doctype html>
<html lang="en">
<head>
<x-inertia::head><title>My App</title></x-inertia::head>
<%- viteReactRefresh %>
<%- vite("resources/js/app.js") %>
</head>
<body>
<x-inertia::app />
</body>
</html>3. Run your app
node server.jsOpen http://localhost:3000.
Vite setup (React example)
Use this when creating a fresh app with this middleware.
1. Install frontend tooling
npm i @inertiajs/react react react-dom
npm i -D vite @vitejs/plugin-react nodemon concurrently cross-env2. Create vite.config.js
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
publicDir: false,
build: {
manifest: true,
rollupOptions: {
input: "resources/js/app.jsx"
}
}
});3. Create resources/js/app.jsx
import { createInertiaApp } from "@inertiajs/react";
import { createRoot } from "react-dom/client";
createInertiaApp({
resolve: (name) => {
const pages = import.meta.glob("./Pages/**/*.jsx", { eager: true });
return pages[`./Pages/${name}.jsx`];
},
setup({ el, App, props }) {
createRoot(el).render(<App {...props} />);
}
});4. Add scripts in package.json
{
"scripts": {
"dev": "concurrently \"npm run dev:vite\" \"npm run dev:server\"",
"dev:vite": "vite",
"dev:server": "nodemon server.js",
"build": "vite build",
"start": "cross-env NODE_ENV=production node server.js"
}
}5. Run
npm run devFor production:
npm run build
npm run startProduction setup
If your Express app serves built frontend assets, add static serving for dist in production:
import path from "node:path";
import express from "express";
const app = express();
const isProd = process.env.NODE_ENV === "production";
if (isProd) {
app.use(express.static(path.join(process.cwd(), "dist")));
}Build and run in production mode:
npm run build
npm run startExpress response helpers
res.inertia(component, props?)- Render an Inertia pageres.share(props)- Share request-scoped props across pages in that requestres.inertiaLocation(url)- Send 409 +X-Inertia-Locationfor full redirects
res.shareis the only sharing helper exposed by this package.
API: inertiaExpress(options)
| Option | Type | Default | Notes |
| --- | --- | --- | --- |
| rootId | string | "app" | Root element id |
| version | string \| () => Promise<string> | undefined | Inertia asset version |
| sharedProps | object \| ({ req, res }) => object | undefined | Global shared props |
| head | string \| string[] \| ({ page, req, res }) => ... | undefined | Custom head tags |
| headStrategy | "auto" \| "head" \| "headFromPage" \| resolver | "auto" | Head merge strategy |
| title | string \| ({ title, page, req, res }) => string | undefined | Final title override |
| headFromPage | boolean \| { pagesPath?: string } | true | Read <Head> from page files |
| vite.adapter | "react" \| "vue" | auto | Force adapter mode |
| vite.devServerUrl | string | http://localhost:5173 (non-production) | Vite dev server |
| vite.devStyleEntry | string | undefined | Extra stylesheet entry in dev |
| vite.manifestPath | string | auto resolve | Enable manifest mode |
| vite.assetsBase | string | "/" | Prefix production asset URLs |
| ssr.url | string | http://127.0.0.1:13714 | Inertia SSR endpoint |
| rootView | string | "base" | Express view name |
| viewData | object \| ({ page, req, res }) => object | undefined | Extra EJS locals |
| templatePath | string | undefined | Render from file path (.ejs or html-like) |
| template | (ctx) => string | undefined | Fully custom html renderer |
| diagnostics | boolean | false | Logs config diagnostics |
EJS locals available in root view
page,rootIdhead,ssrHead,inertiaHeadssrBody,inertiaBodyinertia(the rendered root app html)viteReactRefreshvite(entryPath?)- values returned from
viewData
Head and SEO behavior
- Handles Inertia
<Head>output and keeps title tags consistent - Supports server-side
headresolver plus page-derived head (headFromPage) titleresolver can enforce global title format (prefix/suffix/override)- Blade-style
<x-inertia::head>and<x-inertia::app>placeholders are supported in EJS
SSR behavior
- Calls configured SSR endpoint when available
- If SSR is unreachable or returns empty body, it gracefully falls back to client render
- Preserves Inertia protocol behavior for XHR requests and version mismatch redirects
Production publish and build
npm test
npm run build
npm pack --dry-runRequirements
- Node.js
>= 18 - Express app configured with a view engine (for root view rendering)
MIT License
