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

@tyno/tyno

v2.1.3

Published

A lightweight Node.js HTTP framework with onion model middleware, trie router, SSE, and TypeScript support.

Readme

tyno

A zero-dependency lightweight Node.js HTTP framework written in TypeScript, built on native http module with ES Modules. Combines Koa-style onion middleware with ThinkPHP/Laravel-style request accessors.

import { Tyno } from '@tyno/tyno'

const app = new Tyno()
app.use((req) => `Hello ${req.query('name') || 'World'}`)
app.listen(3000)
import { Tyno } from '@tyno/tyno'
import { Router } from '@tyno/tyno/router'

const app = new Tyno()
const r = new Router()
r.get('/users/:id', (req) => ({ id: req.params.id }))
app.use(r.routes()).listen(3000)

Features

  • Onion middleware: auto-normalized return values, async compression
  • Trie router: static/param :id/regex :id(\d+)/wildcard *, HEAD, group(), fallback()
  • Request accessors: query() / post() / param() / input()
  • File uploads: streaming multipart state-machine parser, buffer/disk dual mode
  • Response builder: static factories json/text/empty/redirect/image, chainable set/type/setStatus/setCookie/attachment
  • Global facades: req/res (aliases) / request (read inbound) / Cache (cache ops)
  • Error system: HttpError / RuntimeError + error middleware + Symbol marker
  • Event system: request / response / response:sent / error / ready
  • Cache: memory/file/Redis drivers, get/set/has/delete/clear/remember
  • Built-in middleware: CORS, async compression, Range static files, request ID
  • Testing: app.inject() without starting a server

Module Structure

Package exports namespace-style, akin to PHP namespaces:

// Main entry
import { Tyno, Response, res, Request, req } from '@tyno/tyno'

// Facades
import { request, Cache } from '@tyno/tyno/facade'

// Middleware
import { cors, compress, serveStatic, requestId } from '@tyno/tyno/middleware'

// Router
import { Router } from '@tyno/tyno/router'

// Errors
import { HttpError, NotFound, RuntimeError } from '@tyno/tyno/errors'

// Cache
import { CacheManager, MemoryDriver } from '@tyno/tyno/cache'

Core Modules

Middleware

Signature: async (req, next?) => unknown.

// Onion model — await next() to enter downstream
app.use(async (req, next) => {
    const start = Date.now()
    const res = await next()
    console.log(`${req.method} ${req.path} ${res.status} ${Date.now() - start}ms`)
    return res
})

// Terminal (no next)
app.use((req) => ({ hello: 'world' }))

// Error middleware (3 args or Symbol marker)
app.use(async (err, req, next) => {
    return Response.json({ error: err.message }, err.status || 500)
})

// Array registration (executed in order)
app.use([logger, auth, compress])
app.middleware([cors, requestId])   // middleware === use alias

Return value auto-normalization:

| Return Type | Conversion | Content-Type | Status | |------------|-----------|-------------|--------| | Response instance | as-is | existing | existing | | string | Response.text(str) | text/plain; charset=utf-8 | 200 | | Buffer | Response.text(buf) | text/plain; charset=utf-8 | 200 | | object (plain) | Response.json(obj) | application/json; charset=utf-8 | 200 | | number / boolean | Response.text(String(v)) | text/plain; charset=utf-8 | 200 | | ReadableStream (has pipe) | streamed output | none (passthrough) | 200 | | AsyncIterable / Generator | SSE stream | text/event-stream | 200 | | null / undefined | Response.empty() | none | 204 | | Image Buffer | Response.image(buf, 'png') | image/png | 200 |

Images require explicit Response.image() — the framework won't guess Buffer type.

Request

Proxy-wrapped IncomingMessage. Built-in props are read-only; custom props stored in DATA_KEY.

// Basic info
req.method          // GET / POST
req.path            // without query string
req.url             // with query string
req.ip              // X-Forwarded-For aware
req.protocol        // http / https
req.secure          // is HTTPS
req.fullUrl         // protocol://host/url
req.httpVersion     // 1.1 / 2.0
req.host            // with port
req.hostname        // without port
req.userAgent       // User-Agent header
req.isAjax()        // X-Requested-With check
req.wantsJSON()     // Accept header check

// Query
req.query()             // → { id: '1', name: 'Alice' }
req.query('id')         // → '1'
req.query('x', 'def')   // → 'def' with default
req.get('id')           // alias
req.getQuery('id')      // alias

// Body (lazy parse, cached)
await req.body()                     // JSON / urlencoded / text auto-detect
await req.post('title')              // single POST field
await req.param('id')                // GET priority, fallback POST
await req.input('id', 0, parseInt)   // with filter

// Headers / Cookies
req.header('authorization')           // case-insensitive
req.cookies                          // URL-decoded
req.getCookie('sessionId')

// File uploads (streaming multipart)
await req.files()               // → { fields, files }
await req.file('avatar')        // → single UploadedFile | null
// UploadedFile: { fieldname, filename, mimetype, filepath, size, buffer? }

Body config:

const app = new Tyno({
    body: {
        limit: '2mb',              // JSON/urlencoded/text max size
        uploadLimit: '20mb',       // file upload max size
        uploadBufferLimit: '512kb',// memory threshold, exceed → stream to disk
        uploadDir: '/tmp',
        keepExtensions: true
    }
})

Response

Construction + static factories + static facade (preset headers/cookies) — one class, three roles.

Aliases: Response (class) = response = res

import { Response, res } from '@tyno/tyno'

// —— Static factories ——
Response.json({ ok: true })          // 200  application/json; charset=utf-8
Response.json(data, 201)             // custom status
Response.text('hello')               // 200  text/plain; charset=utf-8
Response.empty()                     // 204  no Content-Type
Response.empty(201)                  // 201
Response.redirect('/new')            // 302  Location: /new
Response.redirect('/new', 301)       // 301
Response.image(buf)                  // 200  image/png
Response.image(buf, 'jpeg')          // 200  image/jpeg
Response.image(buf, 'image/webp')    // 200  image/webp
Response.image(buf, 'svg', 201)      // 201  image/svg+xml

// —— Instance construction ——
new Response(200, { 'X-Custom': 'yes' }, 'body')
new Response(404, {}, 'Not Found')

// —— Chainable modifiers ——
Response.json({ ok: true })
    .set('X-Request-Id', 'abc')
    .type('json')
    .setStatus(201)
    .setCookie('token', 'xxx', { httpOnly: true, maxAge: 3600, sameSite: 'Lax' })
    .clearCookie('old_session')
    .attachment('report.txt')     // Content-Disposition: attachment

// —— Static facade (presets, final takes priority) ——
app.use(async (req, next) => {
    Response.header('X-Powered-By', 'tyno')
    Response.setCookie('track', 'abc', { httpOnly: true })
    return next()
})

Response output reference:

| Factory | Content-Type | Default Status | Body Type | |---------|-------------|----------------|-----------| | json(data, status?) | application/json; charset=utf-8 | 200 | string (JSON) | | text(data, status?) | text/plain; charset=utf-8 | 200 | string | | empty(status?) | none | 204 | null | | redirect(url, status?) | none (Location header) | 302 | null | | image(data, type?, status?) | image/* | 200 | Buffer \| string | | new Response(s, h, b) | manual | manual | ResponseBody |

| ResponseBody Type | Output Behavior | |-------------------|-----------------| | string | nodeRes.end(body) | | Buffer | nodeRes.end(body) | | NodeJS.ReadableStream | body.pipe(nodeRes) | | AsyncIterable | SSE chunked write | | null | nodeRes.end() |

Router

Trie-based, with param constraints, wildcards, grouping, and fallback.

import { Router } from '@tyno/tyno/router'

const r = new Router({ prefix: '/api' })

// Router-level middleware (supports array, use/middleware are equivalent)
r.use(async (req, next) => {
    req.user = { id: 1 }
    return next()
})
r.middleware([auth, adminCheck])  // array registration

// Method registration
r.get('/users/:id(\\d+)', (req) => ({ id: req.params.id }))
r.head('/health', () => Response.empty(200))
r.post('/users', async (req) => ({ created: await req.body() }))
r.put('/users/:id', async (req) => Response.empty(204))
r.delete('/users/:id', () => Response.empty(204))
r.patch('/users/:id', async (req) => 'ok')
r.all('/any', (req) => `${req.method} matched`)
r.get('/*', (req) => ({ wild: req.params['*'] }))

// Grouping
r.group('/admin', (admin) => {
    admin.get('/dashboard', () => 'Admin Panel')
    admin.get('/users', () => 'User List')
    // equivalent to /api/admin/dashboard, /api/admin/users
})

// Fallback — called when no route matches
r.fallback((req) => Response.json({ error: 'Not Found' }, 404))

app.use(r.routes())

Path patterns:

| Pattern | Meaning | Match | |---------|---------|-------| | users | static segment | /api/users | | :id | param segment | /api/123 | | :id(\d+) | regex constraint | /api/123 | | * | wildcard | /api/a/b/c |

Priority: static > param > wildcard. Methods: get/head/post/put/delete/patch/all.


Errors & Events

Error Handling

import { HttpError, NotFound, RuntimeError, isRuntimeError } from '@tyno/tyno/errors'

// HttpError: 4xx defaults expose=true, 5xx expose=false
throw new NotFound('Resource not found')
throw new HttpError(401, 'Please login')

// RuntimeError: defaults 500, expose=false, with cause chain
throw new RuntimeError('Database connection failed', {
    code: 'DB_FAIL',
    cause: new Error('ECONNREFUSED')
})

Shortcut classes: BadRequest, Unauthorized, Forbidden, NotFound, Conflict, PayloadTooLarge, TooManyRequests, InternalServerError.

Error middleware (3+ args or IS_ERROR_MIDDLEWARE marker, chained in registration order):

import { asErrorMiddleware } from '@tyno/tyno'

app.use(async (err, req, next) => {
    if (isRuntimeError(err)) {
        return Response.json({ error: 'Service unavailable', code: err.code }, 500)
    }
    return Response.json({ error: err.message }, err.status || 500)
})

Event System

Application extends EventEmitter. Full lifecycle:

| Event | Triggers | Args | |-------|---------|------| | request | request arrives, before compose, AsyncLocalStorage ready | (req) | | response | compose completes, before sending | (req, res) | | response:sent | after response sent | (req, res) | | error | error occurs (only with listeners) | (err, req) | | ready | server listening | () |

import { Tyno } from '@tyno/tyno'
const app = new Tyno({ debug: true })

app.on('request',       (req)      => console.log(`→ ${req.method} ${req.path}`))
app.on('response',      (req, res) => console.log(`← ${req.path} ${res.status}`))
app.on('response:sent', (req, res) => console.log(`✓ ${req.path} ${res.status} done`))
app.on('error',         (err, req) => logger.error({ err, path: req.path }))
app.on('ready',         ()         => console.log('Server ready'))

Built-in Middleware

import { cors, compress, serveStatic, requestId } from '@tyno/tyno/middleware'

// CORS
app.use(cors())
app.use(cors({ origin: ['https://a.com'], credentials: true, maxAge: 86400 }))

// Async compression (gzip/deflate), supports streaming, skips images/video
app.use(compress())
app.use(compress({ threshold: 2048, level: 6 }))

// Static files: ETag/304, Range/206, path traversal protection
app.use(serveStatic('./public', { prefix: '/static', maxAge: 3600 }))

// Request ID: auto-generate UUID, inject req.requestId, write response header
app.use(requestId())
app.use(requestId({ readFromHeader: false }))

// Dev error page — auto-attached when debug:true
new Tyno({ debug: true })  // shows stack + cause, JSON/HTML dual format

Cache

Memory / File / Redis drivers, unified get/set/has/delete/clear/remember API.

Alias: Cache (uppercase) = cache (lowercase)

import { Cache } from '@tyno/tyno/facade'

const app = new Tyno({ cache: { driver: 'memory', prefix: 'my:', ttl: 3600 } })

// via app
await app.cache().set('user:1', { name: 'Alice' }, 60)
const user = await app.cache().get('user:1')

// remember — calls factory on cache miss
const config = await app.cache().remember('config', 3600, loadConfig)

// Global facade (inside middleware)
app.use(async () => {
    await Cache.set('key', 'value')
    return Response.json({ ok: true })
})

File driver: { driver: 'file', file: { path: './storage/cache' } }

Redis driver: requires npm install redis, { driver: 'redis', redis: { host, port } }


Other

Initializers

app.initialize(async (app) => { await db.connect() })
app.listen(4567)  // runs all initializers first

HTTPS

app.listen({ port: 443, tls: { key, cert } })

Testing

const res = await app.inject({ method: 'GET', url: '/?name=alice' })
res.status   // 200
res.json()   // { hello: 'alice' }

SSE

async function* gen() { yield 'chunk1'; yield 'chunk2' }
return gen()

API Reference

Application

| Method | Description | |--------|-------------| | new Tyno({ debug?, body?, cache? }) | Create app | | use(fn\|[...fn]) / middleware(fn\|[...fn]) | Register middleware, supports array | | initialize(fn) | Register initializer | | listen(port\|opts) | Start HTTP/HTTPS, emits ready | | close() | Graceful shutdown | | inject(opts) | Test request | | cache() | Cache instance | | on/emit/once | EventEmitter methods |

Router

| Method | Description | |--------|-------------| | new Router({ prefix? }) | Create router | | use(fn\|[...fn]) / middleware(fn\|[...fn]) | Router-level middleware, supports array | | get/head/post/put/delete/patch/all(path, ...h) | Register route | | group(prefix, fn) | Route grouping | | fallback(handler) | No-match handler | | routes() | Return middleware function |

Request

| Alias | Export Source | |-------|--------------| | Request (class) = req | tyno | | request (global facade) | tyno/facade |

| Method | Description | |--------|-------------| | query()/query(k)/get(k) | Query params | | body()/post/param/input | Body accessors | | files()/file(name) | File upload | | header/cookies/getCookie | Request headers |

Response

| Alias | Export Source | |-------|--------------| | Response (class) = response = res | tyno |

| Static Factory | Content-Type | Status | |---------------|-------------|--------| | json(data, status?) | application/json | 200 | | text(data, status?) | text/plain | 200 | | empty(status?) | — | 204 | | redirect(url, status?) | Location header | 302 | | image(data, type?, status?) | image/* | 200 |

| Instance/Static Methods | Description | |------------------------|-------------| | set/setHeader/type/setStatus | Chainable modifiers | | setCookie/clearCookie/attachment | Cookies + download | | get(name) | Read response header | | static header(name, value) | Preset header (facade) | | static setCookie(name, value, options?) | Preset cookie (facade) |

Error Classes

| Class | Description | |-------|-------------| | AppError | Base class | | HttpError(status, msg?, props?) | HTTP error | | RuntimeError(msg, opts?) | Runtime error | | NotFound/Forbidden/... | Shortcut subclasses |

Global Facades

| Export | Source | Description | |--------|--------|-------------| | req | tyno | Request class alias | | res | tyno | Response class alias | | request | tyno/facade | Read current request | | Cache / cache | tyno/facade | Cache operations |


Project Structure

tyno/
├── src/
│   ├── index.ts                # Main barrel
│   ├── application.ts          # Application (EventEmitter)
│   ├── compose.ts              # Onion model + error handling
│   ├── context.ts              # AsyncLocalStorage
│   ├── types.ts                # Type definitions
│   ├── response.ts             # Response class
│   ├── response/sse.ts         # SSEStream
│   ├── request/
│   │   ├── index.ts            # Request class
│   │   ├── body-parser.ts      # BodyParser
│   │   └── multipart-parser.ts # Streaming multipart
│   ├── router/
│   │   ├── index.ts            # Router + group + fallback
│   │   ├── node.ts             # TrieNode
│   │   └── parse-path.ts       # Path parser
│   ├── errors/
│   │   ├── app-error.ts        # AppError
│   │   ├── http-error.ts       # HttpError
│   │   └── runtime-error.ts    # RuntimeError
│   ├── middlewares/
│   │   ├── index.ts            # Subpath barrel
│   │   ├── cors.ts             # CORS
│   │   ├── compress.ts         # Async compression
│   │   ├── static.ts           # Static files + Range
│   │   ├── request-id.ts       # Request ID
│   │   └── error-page.ts       # Dev error page
│   ├── cache/
│   │   ├── index.ts / manager.ts / types.ts
│   │   └── drivers/ (memory / file / redis)
│   ├── facade/                 # Facade subpath barrel
│   ├── request-global.ts       # Global request facade
│   ├── cache-facade.ts         # Global cache facade
│   └── mime.ts
├── example/app.ts
├── test/functional.test.ts
├── scripts/build.mjs           # Dual-format build script
├── package.json
└── tsconfig.json

License

MIT