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

@torajs/tora

v0.1.6

Published

A lightweight web framework with a declarative map() API for Deno, Bun and Node.js

Readme

🐯 Tora

⚠️ Experimental. Tora is a personal framework built to explore the map() idea — a declarative, tree-shaped route API that pre-computes middleware chains at startup. It powers my own projects and evolves alongside them. Expect breaking changes as the design matures. Currently written in TypeScript with opt-in typed handlers — full end-to-end type safety is planned.

Tora (虎) means tiger in Japanese. It is a minimalist and ultrafast web framework built on Web Standards, compatible with Deno, Bun, and Node.js. Its unique declarative map() API pre-computes middleware chains at startup — zero per-request overhead.


Why Tora?

Pre-computed middleware chains

Most frameworks resolve middleware at request time. Express walks a global stack, Koa composes closures lazily. Tora does none of that.

Because map() declares the entire route tree upfront, Tora fully resolves which middlewares apply to each route at registration time. Every route gets a flat, pre-built [...globalMw, ...scopedMw, ...routeMw] array baked in. At request time fetch() does a single linear iteration over that array — no path matching, no closure composition, no runtime middleware resolution. The cost is paid once at startup.

Scoped middleware that reads like your structure

The use key cascades middleware down the tree. The shape of your middleware config mirrors the shape of your API — no separate router.use('/api', auth) calls scattered around.

app.map({
  '/api': {
    use: [auth()],          // applies to everything under /api
    '/admin': {
      use: [adminOnly()],   // stacks on top for /admin routes
      get: dashboard,
    },
  },
})

Composable by default

Route subtrees are plain objects. Split them across files and compose with spread — no framework-specific API needed.

import { usersRoutes } from './routes/users.ts'
import { adminRoutes } from './routes/admin.ts'

app.map({
  '/api': {
    use: [auth()],
    ...usersRoutes,
    '/admin': adminRoutes,
  },
})

Installation

Deno / JSR

import { Tora } from 'jsr:@tora/tora'

Bun / Node.js (npm)

npm install @torajs/tora

Requires Node 18+ or Bun 1+.


Quick start

Deno

import { Tora } from 'jsr:@tora/tora'

const app = new Tora()
app.map({ '/': { get: (ctx) => ctx.text('Hello world') } })

Deno.serve(app.fetch)

Bun

import { Tora } from '@torajs/tora'

const app = new Tora()
app.map({ '/': { get: (ctx) => ctx.text('Hello world') } })

Bun.serve({ fetch: app.fetch })

Node.js

import { Tora } from '@torajs/tora'
import { serve } from '@torajs/tora/node'

const app = new Tora()
app.map({ '/': { get: (ctx) => ctx.text('Hello world') } })

serve(app, 3000)

The map() API

map() is the core of Tora. Your entire API is a single nested object. Keys are path segments, HTTP method names, or use for middleware.

Basic routes

app.map({
  '/': {
    get: (ctx) => ctx.text('Home'),
    post: (ctx) => ctx.json({ created: true }, 201),
  },
  '/about': {
    get: (ctx) => ctx.text('About'),
  },
})

Nested paths

app.map({
  '/api': {
    '/users': {
      get: (ctx) => ctx.text('User list'),   // GET /api/users
      post: (ctx) => ctx.text('Create'),     // POST /api/users
      '/:id': {
        get: (ctx) => ctx.text('One user'),  // GET /api/users/:id
        delete: (ctx) => ctx.text('Delete'), // DELETE /api/users/:id
      },
    },
  },
})

URL parameters

Parameters are prefixed with : and available on ctx.params:

'/:id': {
  get: (ctx) => ctx.json({ id: ctx.params.id }),
}

Supported HTTP methods

get, post, put, delete, patch, head

Multiple map() calls

map() calls are additive — each one registers more routes into the same radix tree. Splitting by domain is a natural way to organise larger apps:

app.map({ '/api/users': usersRoutes })
app.map({ '/api/orders': ordersRoutes })
app.map({ '/api/products': productsRoutes })

Writing middleware

A middleware is a function that receives ctx and next. Call next() to pass control to the next middleware or handler, and use the returned Response to do work after the route has responded.

import type { Context, Next } from 'jsr:@tora/tora'

const myMiddleware = async (ctx: Context, next: Next) => {
  // --- before the handler runs ---
  console.log('incoming:', ctx.req.method, new URL(ctx.req.url).pathname)

  const response = await next() // run the next middleware / handler

  // --- after the handler responds ---
  console.log('outgoing:', response.status)

  return response
}

Register it globally or scope it to a subtree:

app.use(myMiddleware)           // global — runs on every request

app.map({
  '/api': {
    use: [myMiddleware],        // scoped — runs only under /api
    '/users': {
      get: route(myMiddleware, listUsers), // route-level — this route only
    },
  },
})

Execution order

For a request to GET /api/users with the setup above, the full execution order is:

global middlewares → scoped middlewares → route middlewares → handler
       ↑                    ↑                    ↑               ↑
   app.use()           use: [...]           route(mw, ...)    handler fn

The "before" code in each middleware runs top-down, and the "after" code runs bottom-up as the call stack unwinds:

→ globalMw (before)
  → scopedMw (before)
    → routeMw (before)
      → handler
    ← routeMw (after)
  ← scopedMw (after)
← globalMw (after)

Short-circuiting

Return a response without calling next() to stop the chain — useful for auth guards:

const auth = async (ctx: Context, next: Next) => {
  const token = ctx.header('authorization')
  if (!token) return ctx.json({ error: 'Unauthorized' }, 401) // chain stops here
  ctx.set('user', await verifyToken(token))
  return next()
}

Passing data downstream

Use ctx.set() / ctx.get() to share data between middlewares and handlers:

const auth = async (ctx: Context, next: Next) => {
  ctx.set('user', await verifyToken(ctx.header('authorization') ?? ''))
  return next()
}

const getProfile = (ctx: Context) => {
  const user = ctx.get<User>('user')
  return ctx.json(user)
}

Route-level middleware with route()

Use the route() helper to attach middleware to a single route. It enforces at the type level that the last argument is a Handler and all preceding arguments are Middleware — a plain array gives you no such guarantee.

import { Tora, route } from 'jsr:@tora/tora'

app.map({
  '/api/users': {
    post: route(auth, validate(schema), createUser),
    //    ^^^^^ TypeScript enforces: [...Middleware[], Handler]
  },
})

Without route(), a plain array works at runtime but the types are loose — putting the handler in the wrong position is a silent runtime bug. route() makes that a compile error.


Scoped middleware with use

Add a use key at any level. Middleware cascades down to all routes at that level and below.

app.map({
  '/api': {
    use: [auth],
    '/users': {
      get: listUsers,       // auth runs here
      '/:id': {
        get: getUser,       // and here
      },
    },
    '/admin': {
      use: [adminOnly],
      get: adminDashboard,  // auth + adminOnly run here
    },
  },
})

Execution order for GET /api/admin: auth → adminOnly → adminDashboard


Global middleware

app.use() registers middleware that runs on every request, before any route-level middleware:

app.use(requestId(), rateLimiter(), logger(), cors())

Error handling

app.onError() registers error handlers. Called whenever a handler or middleware throws. Multiple handlers chain via next(). If none handle it, a default 500 JSON response is returned.

app.onError((err, ctx, next) => {
  console.error(err)
  return ctx.json({ error: err.message }, err.status ?? 500)
})

Attach a status property to errors for custom HTTP codes:

const err = new Error('Forbidden')
err.status = 403
throw err

Request context (ctx)

Every handler and middleware receives ctx:

| Property / Method | Description | | ----------------------------------- | ------------------------------------------------ | | ctx.req | Raw Request object | | ctx.params | URL parameters e.g. { id: '42' } | | ctx.query | Parsed query string (lazy) e.g. { page: '1' } | | ctx.header(name) | Get a request header | | ctx.cookie(name) | Read a request cookie | | ctx.setCookie(name, value, opts) | Set a response cookie | | ctx.body<T>() | Parse request body as JSON | | ctx.bodyText() | Parse request body as plain text | | ctx.bodyForm() | Parse request body as FormData | | ctx.set(key, value) | Store a value for the lifetime of the request | | ctx.get<T>(key) | Retrieve a stored value | | ctx.text(content, status?) | Return a text response | | ctx.json(data, status?) | Return a JSON response | | ctx.html(content, status?) | Return an HTML response | | ctx.redirect(url, status?) | Return a redirect (default 302) |

Cookies

// Read
const token = ctx.cookie('session')

// Write
ctx.setCookie('session', 'abc123', {
  httpOnly: true,
  secure: true,
  sameSite: 'Lax',
  maxAge: 3600,
  path: '/',
})
return ctx.json({ ok: true })

Bundled middlewares

logger()

Logs method, path, status, and response time to stdout.

import { logger } from 'jsr:@tora/tora/logger'

app.use(logger())
// [2026-04-10T12:00:00.000Z] GET /api/users 200 - 3ms

cors(options?)

Handles CORS headers and preflight OPTIONS requests.

import { cors } from 'jsr:@tora/tora/cors'

app.use(cors({
  origin: ['https://myapp.com', 'https://staging.myapp.com'],
  credentials: true,
  allowHeaders: ['Content-Type', 'Authorization'],
  exposeHeaders: ['X-Request-Id'],
  maxAge: 86400,
}))

Defaults to origin: '*' with no credentials.

rateLimiter(options?)

In-memory token bucket rate limiter. Expired buckets are swept automatically on each window interval so memory stays bounded.

import { rateLimiter } from 'jsr:@tora/tora'

app.use(rateLimiter({
  limit: 100,       // requests per window
  window: 60_000,   // window in ms (default: 1 min)
  keyBy: (ctx) => ctx.header('x-forwarded-for') ?? 'unknown',
}))

Returns 429 Too Many Requests when the limit is exceeded.

requestId()

Stamps each request with a unique ID. Reuses X-Request-Id if already present (e.g. from a proxy). Echoed back in the X-Request-Id response header.

import { requestId } from 'jsr:@tora/tora'

app.use(requestId())

// Access in handlers/middleware:
const id = ctx.get('requestId')

errorHandler()

Structured JSON error responses with console logging.

import { errorHandler } from 'jsr:@tora/tora'

app.onError(errorHandler())
// [ERROR] Something went wrong
// { "error": "Something went wrong" } — 500

Validation

Tora doesn't include a built-in validator — use whatever you prefer. The route() helper makes attaching a validation middleware clean and type-safe:

import { z } from 'zod'
import type { Context, Next } from 'jsr:@tora/tora'

const validate = (schema: z.ZodType) => async (ctx: Context, next: Next) => {
  const result = schema.safeParse(await ctx.body())
  if (!result.success) return ctx.json({ error: result.error.flatten() }, 400)
  ctx.set('body', result.data)
  return next()
}

app.map({
  '/api/users': {
    post: route(validate(z.object({ name: z.string(), age: z.number() })), createUser),
  },
})

Works with Zod, Valibot, TypeBox, or anything else.


Route tree visualiser

Two visualiser functions are available as standalone imports — tree-shakeable and optional.

tree(app) — full chain

Shows the complete middleware → handler chain for every route.

import { tree } from 'jsr:@tora/tora'

console.log(tree(app))
🐯 Tora
/
├── GET: showUrl
└── api
    └── users
        ├── GET: auth → listUsers
        └── POST: auth → validate → createUser

Named functions show their name. Anonymous arrows show as <anonymous> — a useful nudge to name your handlers.

treeCompact(app) — method + count

Shows methods inline with the middleware count in parentheses. Useful for a quick overview of large APIs.

import { treeCompact } from 'jsr:@tora/tora'

console.log(treeCompact(app))
🐯 Tora
/ [GET]
├── api
│   ├── users [GET (1), POST (2)]
│   └── :id [GET (1), DELETE (1)]
└── admin [GET (2)]

Node.js

Use the included adapter to run on Node 18+:

import { Tora } from '@torajs/tora'
import { serve } from '@torajs/tora/node'

const app = new Tora()
app.map({ ... })
serve(app, 3000, () => console.log('http://localhost:3000'))

Or use createNodeAdapter directly if you need the raw http.Server:

import http from 'node:http'
import { createNodeAdapter } from '@torajs/tora/node'

const server = http.createServer(createNodeAdapter(app))
server.listen(3000)

Full example

import { Tora, route, tree } from 'jsr:@tora/tora'
import { logger, cors, rateLimiter, requestId, errorHandler } from 'jsr:@tora/tora'
import type { Context, Next } from 'jsr:@tora/tora'

const app = new Tora()

app.onError(errorHandler())
app.use(requestId(), rateLimiter(), logger(), cors())

const auth = async (ctx: Context, next: Next) => {
  const token = ctx.header('authorization')
  if (!token) return ctx.json({ error: 'Unauthorized' }, 401)
  ctx.set('token', token)
  return next()
}

app.map({
  '/': {
    get: (ctx) => ctx.text('Hello'),
  },
  '/api': {
    use: [auth],
    '/users': {
      get: (ctx) => ctx.json({ users: [] }),
      post: route(auth, (ctx) => ctx.json({ created: true }, 201)),
      '/:id': {
        get: (ctx) => ctx.json({ id: ctx.params.id }),
        delete: (ctx) => ctx.json({ deleted: ctx.params.id }),
      },
    },
  },
})

console.log(tree(app))
Deno.serve(app.fetch)