npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@bioleyl/hxpress

v0.2.0

Published

A type-safe Express + HTMX framework with TSX rendering

Readme

HxPress — Type-safe Express + HTMX Framework with TSX Rendering

HxPress is a lightweight, type-safe framework layer on top of Express.js for building server-rendered HTMX-powered applications. It provides a TSX developer experience — write JSX components that render to HTML on the server, with zero client-side JavaScript and no hydration overhead.

Philosophy

  • Type safety first: Strong TypeScript inference via Zod schemas for validated request bodies.
  • Simplicity: No virtual DOM, no build step for the framework itself, minimal abstractions.
  • HTMX-first: Automatic detection of HX-Request headers to serve full pages or partial fragments.

Table of Contents


Installation

npm install @bioleyl/hxpress express zod

Peer dependencies: express (v4 or v5), zod (v3.22+ or v4).

HxPress publishes both ESM and CommonJS builds with full TypeScript type declarations.


Quick Start

A minimal HxPress application:

import express from "express";
import { createHxpress } from "@bioleyl/hxpress";

const app = express();
const hx = createHxpress(app);

// GET route — renders a full page for normal requests, partial for HTMX.
hx.get("/invoices", {
  onPage: () => (
    <html>
      <body>
        <h1>Invoices</h1>
        <table id="invoice-list"></table>
      </body>
    </html>
  ),
  onPartial: () => <table id="invoice-list">
    <tr><td>Invoice A — $250.00</td></tr>
  </table>,
});

hx.apply(); // Wire routes onto Express
app.listen(3000);

TSX Setup

To use TSX with HxPress, set these TypeScript compiler options:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "jsx": "react-jsx",
    "jsxImportSource": "@bioleyl/hxpress",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  }
}

Notes:

  • Use .tsx for files that contain JSX.
  • HxPress exposes @bioleyl/hxpress/jsx-runtime and @bioleyl/hxpress/jsx-dev-runtime, so no React dependency is required.

TSX Rendering

HxPress provides a lightweight JSX runtime that converts JSX trees to HTML strings on the server. No React dependency, no virtual DOM, no hydration.

JSX Elements

Prefer regular TSX syntax:

const element = (
  <div class="card">
    <h1>Hello World</h1>
  </div>
);

Low-level factory form (rarely needed, mostly for runtime internals):

import { jsx } from "@bioleyl/hxpress";

const element = jsx("div", { class: "card" }, "key", "Hello World");

Components

Components are plain functions that return JSX nodes:

import type { PropsWithChildren } from "@bioleyl/hxpress";

function Card(props: PropsWithChildren<{ title: string }>) {
  return (
    <article class="card">
      <h2>{props.title}</h2>
      {props.children}
    </article>
  );
}

// Usage:
<Card title="My Card">
  <p>Card content goes here.</p>
</Card>

You can also use HxFC<Props> / AsyncHxFC<Props> as shorthand component aliases when you want children included automatically.

Fragments & Arrays

Use fragments to group elements without adding a wrapper DOM node:

<>
  <span />
  <em />
</>

Arrays render their items sequentially. Nested arrays are flattened automatically:

["a", ["b", "c"]] // → abc

Async Components

Components can be async — HxPress detects Promise returns and awaits them:

async function UserProfile(props: { userId: number }) {
  const user = await fetchUserById(props.userId);
  return (
    <div>
      <h1>{user.name}</h1>
    </div>
  );
}

HTML Escaping

All text content and attribute values are automatically escaped to prevent XSS. The escapeHtml and escapeAttr functions are also exported for manual escaping when needed:

import { escapeHtml } from "@bioleyl/hxpress";

<p>{escapeHtml(userInput)}</p>; // Safe against <script> injection

Routing

GET Routes

Register a GET route with onPage (full page) and optionally onPartial (HTMX fragment):

hx.get("/invoices", {
  onPage: () => (
    <html>
      <body>
        <table></table>
      </body>
    </html>
  ),
  onPartial: () => <table></table>,
});

POST Routes (Traditional)

POST routes without validation use the same onPage/onPartial pattern:

hx.post("/submit", {
  onPage: (_ctx) => <form></form>,
  onPartial: (_ctx) => <p>Submitted!</p>,
});

HTMX Rendering Behavior

onPage vs onPartial

HxPress automatically detects whether a request is an HTMX request by checking the HX-Request: true header. When it is, HxPress calls onPartial; otherwise, it calls onPage.

hx.get("/items/:id", {
  // Called for normal browser navigation → full page with layout wrappers
  onPage: (ctx) => <html><body>Item {ctx.params.id}</body></html>,

  // Called when HX-Request header is present → only the partial content
  onPartial: (ctx) => <div class="item-detail">Details for item {ctx.params.id}</div>,
});

Automatic HTMX Detection

The detection checks req.headers["hx-request"] and matches it case-insensitively against "true". This works with both GET and POST routes.


Typed Actions & Validation

HxPress supports action-style POST routes that validate request bodies using Zod schemas, then pass the validated data to an action handler with full TypeScript inference.

Zod Schema Inference

import { z } from "zod";
import { createSchema } from "@bioleyl/hxpress";

const CreateInvoiceSchema = z.object({
  title: z.string().min(1),
  amount: z.number(),
});

// The schema is wrapped to provide explicit _input/_output type markers.

Action Handlers

The action handler receives a context where ctx.body is typed from the Zod schema:

hx.post("/invoices", {
  schema: CreateInvoiceSchema,

  action(ctx) {
    // ctx.body has type { title: string; amount: number } — inferred automatically!
    const invoice = createInvoice({
      title: ctx.body.title,   // ← TypeScript knows this is a string.
      amount: ctx.body.amount, // ← TypeScript knows this is a number.
    });

    return { html: <tr><td>{invoice.id}</td></tr> };
  },
});

Error Handling with onError

When validation fails, the optional onError callback receives the raw (invalid) body and structured errors:

hx.post("/invoices", {
  schema: CreateInvoiceSchema,
  action(ctx) { /* ... */ },
  onError(_ctx, errors) {
    // errors is ValidationError[] with message + path for each issue.
    return { html: <ul>{errors.map(e => <li>{e.message}</li>)}</ul> };
  },
});

HTMX Response Headers

Action handlers can set HTMX response headers via the ActionResponse object:

| Field | Purpose | Header Set | |-------|---------|------------| | html | Rendered HTML body (JSX node or string) | — | | trigger | Client-side event trigger name | HX-Trigger | | redirect | Redirect URL | HX-Retarget, Express redirect |

action(ctx) {
  return { html: <tr>...</tr>, trigger: "invoiceCreated" };
}
// Sets HX-Trigger: invoiceCreated header.

Request Context

Every handler receives a typed context object with the following properties:

| Property | Type | Description | |----------|------|-------------| | ctx.req | express.Request | The raw Express request object | | ctx.res | express.Response | The raw Express response object | | ctx.params | Record<string, string> | Route parameters (e.g., { id: "42" }) | | ctx.query | Record<string, string \| string[]> | Query string key-value pairs | | ctx.body | T (from Zod schema) or undefined | Validated body in action routes | | ctx.validationErrors | ValidationError[] \| undefined | Validation errors for form re-rendering | | ctx.isHxRequest() | boolean | Whether the request has HX-Request: true | | ctx.render(node) | (node) => Promise<void> | Render a JSX node and send it as response |

hx.get("/users/:id", {
  onPage(ctx) {
    console.log(ctx.params.id);       // "42"
    console.log(ctx.query.q ?? "");   // query string value
    return <div>User {ctx.params.id}</div>;
  },
});

Plugin System

HxPress provides a powerful plugin architecture for extending the framework. Plugins can register middleware, routes, extend context per-request, and hook into the rendering pipeline.

Context Extension

Add custom properties to every request's context:

const userPlugin = createPlugin({
  name: "user-plugin",
  extendContext() {
    return { user: { id: "42", name: "Alice" } };
  },
});

// Now ctx.user is available in all handlers.
hx.get("/profile", { onPage: (ctx) => <div>{ctx.user.name}</div> });

Lifecycle Hooks

Transform rendered HTML or inspect the rendering pipeline:

const transformPlugin = createPlugin({
  name: "transform-plugin",
  hooks: {
    // Called before each JSX tree is rendered.
    beforeRender(ctx, node) { console.log("Rendering:", typeof node); },

    // Called after HTML is generated — return a modified string to replace it.
    afterRender(_ctx, html) { return `<html><body>${html}</body></html>`; },
  },
});

Middleware Registration

Register Express middleware that runs before route handlers:

const authPlugin = createPlugin({
  name: "auth-plugin",
  registerMiddleware() {
    return [(req, res, next) => {
      if (!isAuthenticated(req)) { res.status(401).send("Unauthorized"); }
      else { next(); }
    }];
  },
});

Route Registration

Plugins can add fallback routes:

const healthPlugin = createPlugin({
  name: "health-plugin",
  registerRoutes() {
    return [{ path: "/healthz", method: "get" as const, onPage: () => <p>OK</p> }];
  },
});

Registering Plugins

Pass plugins at app creation time:

const hx = createHxpress(app, {
  plugins: [userPlugin, authPlugin, healthPlugin],
});

API Reference

| Export | Type | Description | |--------|------|-------------| | createHxpress(app, options?) | (app) => Hxpress | Create a framework instance attached to an Express app. | | jsx(tag, props?, maybeKey?, ...children) | JsxElement | Low-level JSX factory used by the TSX transform. | | Fragment({ children }) | JsxFragment | Group multiple nodes without adding a wrapper DOM element. | | render(node) | (node) => Promise<string> | Convert a JSX tree to an HTML string. | | escapeHtml(str) | (str) => string | Escape special characters for safe text content inclusion. | | escapeAttr(str) | (str) => string | Escape special characters for safe attribute value inclusion. | | PropsWithChildren<Props> | Props & { children?: HxChildren } | Add optional children to a component props shape. | | HxFC<Props> | (props) => JsxNode | Synchronous component alias with children included in props. | | AsyncHxFC<Props> | (props) => Promise<JsxNode> | Async component alias with children included in props. | | createPlugin(def) | (def) => HxPlugin | Create a typed plugin from a definition object. | | validateRequest(schema) | (schema) => RequestHandler | Express middleware that validates body against a Zod schema. | | createSchema(zodSchema) | (zodSchema) => ValidatedSchema<T> | Wrap a Zod schema with explicit _input/_output type markers. | | createHxContext(req, res) | (req, res) => HxContext | Create a typed request context from Express req/res objects. |


Example Application

A complete working example is included in the repository:

cd examples/basic-app
npm install
npm run dev

The demo implements an invoice management app with:

  • TSX components: InvoiceRow, InvoiceTable, CreateInvoiceForm, CardSection (typed with PropsWithChildren)
  • HTMX behavior: Full pages for navigation, partials for HTMX swaps
  • Typed actions: Zod-validated POST routes with typed ctx.body
  • Plugins: Analytics plugin (context extension + HTML transformation) and logger middleware

License

MIT © bioleyl