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

jinatra

v0.7.1

Published

A tiny Fetch-native web framework with concise routing, middleware, sessions, JSX SSR, and native EJS views

Readme

Jinatra

Jinatra is a tiny Fetch-native web framework for Bun, Node, and Cloudflare Workers. It provides a small router, request-scoped helpers, middleware, static files, escaped JSX SSR, and native EJS views.

A minimal app

import { app } from "jinatra";
import { serve } from "jinatra/bun";

const Layout = ({ children }) => <html><body>{children}</body></html>;

get("/", () => <Layout><h1>Jinatra</h1></Layout>);

get("/hello/:name", () => <h1>Hello {params.name}</h1>);

post("/echo", async () => ({ body: await body() }));

serve(app, { port: 3000 });

Configure automatic JSX in tsconfig.json:

{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "jinatra"
  }
}

Importing the root package also exposes the default app's route verbs and request helpers on globalThis, so get, post, params, and body can be used without importing each one. Returning an object automatically creates a JSON response; json() is mainly the request-body parser when called without arguments. Explicit named imports remain available. The package exports jsx-runtime and jsx-dev-runtime; JSX is rendered on the server and never hydrated. A JSX value becomes escaped text/html, strings become text/plain, objects become JSON, and Response values pass through unchanged.

Routing and helpers

import { Jinatra } from 'jinatra';

const app = new Jinatra();

app.get('/users/:id', async () => {
  return { id: params.id, search: query('search') };
});

app.post('/users', async () => {
  const data = await body();
  status(201);
  return data;
});

Methods are get, post, put, patch, delete, any (and the alias all). Parameters are available as params.id or params('id'); c.param('id', 'fallback') accepts a default and missing values return null. Use app.mount('/api', api) to mount another Fetch-compatible app below a prefix. The other request helpers are query, body, form, json, headers, and cookies. Response helpers are status, redirect, html, text, and json.

Handlers may also receive the request context as their first argument (c.req, c.url, c.state, and the equivalent context methods remain available). The context itself provides a compact request surface with accept, preferredType(), path, ip, and secure; the global request helper also supports bracket access to route parameters.

Middleware, errors, and static files

app.use(async (c, next) => {
  const started = performance.now();
  const response = await next();
  const headers = new Headers(response.headers);
  headers.set('x-time', String(performance.now() - started));
  return new Response(response.body, { status: response.status, headers });
});

app.get('/private', requireAuth, handler);
app.notFound((c) => {
  c.status(404);
  return { error: 'missing' };
});
app.onError((error, c) => {
  c.status(500);
  return { error: error.message };
});
app.static('./public');

import { serve } from 'jinatra/bun';
serve(app, { port: 3000 });

static() and the Bun adapter use Bun.file() and include MIME types, HEAD, cache headers, and traversal protection.

JSX

Intrinsic elements, function components, fragments, arrays, nested components, boolean attributes, className, object styles, and nullish children are supported. Text and attributes are escaped by default:

const name = '<script>alert(1)</script>';
const page = <><h1>{name}</h1><input disabled /></>;

Use raw('<strong>trusted</strong>') only for trusted HTML.

EJS views

The Node and Bun adapters natively render EJS templates from the views directory. Use the view helper from a route; the .ejs suffix is optional:

import { view } from 'jinatra';

app.get('/', () => view('home', { title: 'Jinatra' }, 'layout'));

The optional layout receives the rendered template as body and is also looked up in views. You can override views directory with another by serve(app, { views: './templates' }), or new Jinatra({ views: './templates' }).

Cloudflare Workers

app.fetch is directly compatible with Workers:

import { Jinatra } from 'jinatra';

const app = new Jinatra();
app.get('/', () => 'Hello from a Worker');

export default { fetch: app.fetch };

Use worker() when the application has Cron or Queue handlers. The scheduled and queue exports are omitted when there are no corresponding handlers:

const app = new Jinatra();

app.cron('0 * * * *', async (controller, env, ctx) => {
  await env.DB.prepare('...').run();
});

export default app.worker();

scheduled() compares controller.cron with the expression registered by app.cron() and dispatches only the matching handler. Queue consumers are registered by name and receive each message from Cloudflare's batch:

app.queue('jobs', async (message, env, ctx) => {
  await processJob(message.body, env)
})

Queue producers, R2 buckets, D1 databases, and KV namespaces remain ordinary bindings on env. Cron expressions and queue consumer configuration remain deployment configuration in wrangler.toml or wrangler.jsonc; Jinatra only maps events to application code. withAssets() preserves the scheduled and queue exports when adding a Workers Static Assets fallback.

Other Fetch runtimes can use app.fetch(request) directly. The included Node adapter is a small compatibility adapter; the built-in server path is Bun's Bun.serve().

Documentation

The full guide is built with VitePress and deployed to GitHub Pages:

npm run docs:dev       # local docs server
npm run docs:build     # static production build
npm run docs:preview   # preview the production build

Development

npm test
npm run check