@krs2k/simpler
v1.0.2
Published
File-based routing framework for Hono with HTMX support, layouts, middleware, and Zod form parsing
Maintainers
Readme
Simpler
Lightweight SSR framework built on Hono, JSX, and HTMX. File-based routing, nested layouts, middleware, and Zod form validation — zero client-side JS frameworks.
Quick Start
mkdir my-app && cd my-app
npm init -y
npm install simpler hono zod
npm install -D viteCreate your first page:
my-app/
└── src/
└── pages/
└── index.tsx// src/pages/index.tsx
export default function Home() {
return <h1>Hello from Simpler!</h1>;
}Run the dev server:
npx simpler devCLI
| Command | Description |
| ---------------- | ------------------------------------ |
| simpler dev | Start development server with HMR |
| simpler build | Build for production |
| simpler run | Run the production server |
Project Structure
my-app/
├── simpler.config.ts # optional config
├── vite.config.ts # optional (extra Vite plugins like Tailwind)
└── src/
├── htmx.d.ts # optional — HTMX type support (see below)
└── pages/
├── _layout.tsx # root layout
├── _404.tsx # custom 404 page
├── _error.tsx # custom error page
├── _middleware.ts # root middleware
├── index.tsx # → /
├── about.tsx # → /about
└── blog/
├── _layout.tsx # nested layout for /blog/*
├── _middleware.ts # middleware for /blog/*
├── index.tsx # → /blog
└── [slug].tsx # → /blog/:slugPages
Every .tsx file in src/pages/ becomes a route. Files starting with _ are special (layouts, middleware, error pages).
import type { PageProps } from "simpler";
export default function About({ c, params }: PageProps) {
return <h1>About</h1>;
}Page Data
Export a pageData function to load data before rendering:
import type { PageDataFn } from "simpler";
export const pageData: PageDataFn = async ({ params, c }) => ({
meta: { title: "My Page" },
});
export default function Page() {
return <h1>Page with data</h1>;
}Layouts
_layout.tsx files wrap all pages in their directory and subdirectories. They nest automatically.
import type { LayoutProps } from "simpler";
export default function RootLayout({ children, pageData }: LayoutProps) {
return (
<html lang="en">
<head>
<title>{(pageData?.meta as any)?.title ?? "My App"}</title>
<script src="https://unpkg.com/[email protected]"></script>
</head>
<body>{children}</body>
</html>
);
}Middleware
_middleware.ts files apply to all routes in their directory and below. Uses Hono middleware format.
import type { MiddlewareHandler } from "hono";
const middleware: MiddlewareHandler = async (c, next) => {
console.log(`${c.req.method} ${c.req.url}`);
await next();
};
export default middleware;Dynamic Routes
[slug].tsx→:slugparameter[...rest].tsx→ catch-all wildcard
Access params via PageProps:
export default function Post({ params }: PageProps) {
return <h1>Post: {params.slug}</h1>;
}Forms with Zod
Validate FormData using Zod schemas:
import { z } from "zod";
import { parseForm } from "simpler";
import type { PageProps } from "simpler";
const Schema = z.object({
email: z.string().email(),
password: z.string().min(6),
});
export default async function Login({ c }: PageProps) {
if (c.req.method === "POST") {
const result = parseForm(await c.req.formData(), Schema);
if (result.success) return c.redirect("/dashboard");
// result.errors — field-level error messages
}
return <form method="post">...</form>;
}HTMX
Simpler auto-detects HX-Request headers and skips the root layout for HTMX partial responses.
For TypeScript support of hx-* attributes, copy the type declarations into your project:
// src/htmx.d.ts — grab from node_modules/simpler/lib/htmx.d.ts
// or import the type augmentation:
import "simpler/htmx";Async Context
Per-request context using AsyncLocalStorage:
import { createAppContext } from "simpler";
const { useContext, setContext, runWithContext } = createAppContext({
user: null as string | null,
});Configuration
Create an optional simpler.config.ts in the project root:
import { defineConfig } from "simpler";
export default defineConfig({
port: 3000,
basePath: "/",
defaultMeta: {
title: "My App",
description: "Built with Simpler",
},
});Plugins
Extend Simpler with plugins that add routes and middleware. Define them in simpler.config.ts:
import { defineConfig, definePlugin } from "simpler";
const healthCheck = definePlugin({
name: "health-check",
routes: [
{
method: "get",
path: "/api/health",
handler: (c) => c.json({ status: "ok" }),
},
],
});
const timing = definePlugin({
name: "request-timing",
middleware: [
{
handler: async (c, next) => {
const start = performance.now();
await next();
c.header("X-Response-Time", `${(performance.now() - start).toFixed(2)}ms`);
},
},
],
});
export default defineConfig({
plugins: [timing, healthCheck],
});Plugin API
A plugin is an object with a name and optional middleware / routes arrays:
type SimplerPlugin = {
name: string;
middleware?: PluginMiddleware[];
routes?: PluginRoute[];
};Middleware — Hono middleware, optionally scoped to a path (* by default):
{ path?: string; handler: MiddlewareHandler }Routes — Hono handlers with a path and optional HTTP method (all by default):
{ method?: "get" | "post" | "put" | "delete" | "patch" | "all"; path: string; handler: (c) => Response }Plugins can also be factory functions for reusable, configurable extensions:
const auth = (opts: { secret: string }) =>
definePlugin({
name: "auth",
middleware: [{ path: "/admin/*", handler: jwt({ secret: opts.secret }) }],
routes: [{ method: "get", path: "/login", handler: loginPage }],
});
export default defineConfig({
plugins: [auth({ secret: "s3cret" })],
});Plugin middleware and routes are registered before file-based routes, so they can intercept requests.
Vite Plugins
If you need extra Vite plugins (e.g. Tailwind), create a vite.config.ts — it's automatically merged with Simpler's internal config:
import { defineConfig } from "vite";
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
plugins: [tailwindcss()],
});License
MIT
