@carlos-tzin/tzin
v1.0.8
Published
Contract-first TypeScript framework. Types that scale, realtime channels with presence, and an MCP server for every API.
Maintainers
Readme
tzin
Contract-first TypeScript framework. Types that scale. Realtime built in. AI-native from day one.
tzin— from Nahuatl -tzin, an honorific suffix for what is valued and beloved. A pact between client and server, declared once.
Status: stable (1.0.x). The core works end-to-end (see Benchmarks); 1.0.0 shipped the stable API, and 1.0.2 adds test coverage gates, a wider CI matrix and a public type-clean surface.
npm install @carlos-tzin/tzinOr scaffold a new project:
npx create-tzin my-appDocumentation
Hosted site: tzin.tzinny.com
- API Reference — all exports, types, and options
- Architecture Guide — request pipeline, design decisions, internals
- Deployment Guide — Docker, Cloudflare Workers, multi-node clustering
- Roadmap — future features
Why another framework?
The TypeScript backend landscape is crowded — and still leaves real gaps:
| Gap | Evidence | |---|---| | Type inference collapses at scale | Hono issues #2399, #3869; chained route builders force one giant expression per app | | No architecture in lightweight frameworks | Hono issue #4121 | | Extractors don't exist in TS | Only Rust's Axum gets handler input right | | OpenAPI is a bolt-on | Every TS framework translates its own schema DSL to JSON Schema at runtime or via codegen | | AI-native toolchain | One framework ships an MCP server; the window is closing |
tzin's answer: declare a contract once, get everything else for free.
import { t } from '@carlos-tzin/tzin'
import { contract, impl, createApp, listen } from '@carlos-tzin/tzin'
const getUser = contract({
method: 'GET',
path: '/users/:id',
params: t.Object({ id: t.String() }),
responses: {
200: t.Object({ id: t.String(), name: t.String(), tags: t.Array(t.String()) }),
404: t.Object({ error: t.String() }),
},
})
export const getUserRoute = impl(getUser, async ({ params }) => {
const user = await findUser(params.id)
if (!user) throw new HttpError(404, 'user not found')
return { status: 200, body: user }
})
const app = createApp([getUserRoute])
listen(app, 3000)From that single declaration:
- Handler input is extracted, not guessed:
{ params }exists because you declared it; addquery,body,headersorcookiesto the contract and they appear, fully typed and validated per request. - The compiler enforces your responses: returning a shape that doesn't match the
declared
200body is a type error. ThrownHttpErrors map to their status. - OpenAPI 3.1 is free: contracts are JSON Schema (TypeBox), so
generateOpenApi(routes)needs no translation layer.
Features
Core
| Feature | Import | Description |
|---|---|---|
| Contracts | @carlos-tzin/tzin | Type-safe API contracts |
| Routes | @carlos-tzin/tzin | Bind contracts to handlers |
| Middleware | @carlos-tzin/tzin | Onion-style middleware |
| DI | @carlos-tzin/tzin | Typed dependency injection |
Runtime
| Feature | Import | Description |
|---|---|---|
| Node server | @carlos-tzin/tzin | listen(app, port) |
| Bun server | @carlos-tzin/tzin | serveBun(app, port) |
| Workers | @carlos-tzin/tzin | toWorker(app) |
| Dev server | CLI | tzin dev with hot reload |
Realtime
| Feature | Import | Description |
|---|---|---|
| Channels | @carlos-tzin/tzin | SSE + POST channels |
| Presence | @carlos-tzin/tzin | Phoenix-style presence |
| WebSocket | @carlos-tzin/tzin/ws | Native WS channels |
| Browser client | @carlos-tzin/tzin/client-browser | Zero-dep browser client |
AI-Native
| Feature | Import | Description |
|---|---|---|
| MCP Server | @carlos-tzin/tzin | Streamable HTTP + stdio |
| OpenAPI | @carlos-tzin/tzin | Auto-generated from contracts |
| LLMs.txt | @carlos-tzin/tzin | AI-readable API index |
Database
| Feature | Import | Description |
|---|---|---|
| Models | @carlos-tzin/tzin/db | Type-safe ORM |
| Query builder | @carlos-tzin/tzin/db | Chainable queries |
| Store adapters | @carlos-tzin/tzin/db | Swappable backends |
Auth
| Feature | Import | Description |
|---|---|---|
| Bearer auth | @carlos-tzin/tzin/auth | JWT validation |
| Optional auth | @carlos-tzin/tzin/auth | Non-strict JWT |
| API key | @carlos-tzin/tzin/auth | Header-based auth |
| JWT utils | @carlos-tzin/tzin/auth | sign/verify |
Jobs
| Feature | Import | Description |
|---|---|---|
| Define jobs | @carlos-tzin/tzin/jobs | Background tasks |
| Enqueue | @carlos-tzin/tzin/jobs | Job queue |
| Retry | @carlos-tzin/tzin/jobs | Auto-retry on failure |
Logging
| Feature | Import | Description |
|---|---|---|
| Logger | @carlos-tzin/tzin/log | Structured logging |
| Child loggers | @carlos-tzin/tzin/log | Scoped context |
| Pretty/JSON | @carlos-tzin/tzin/log | Configurable output |
Testing
| Feature | Import | Description |
|---|---|---|
| Test client | @carlos-tzin/tzin/test | API testing |
| Schema validation | @carlos-tzin/tzin/test | Type assertions |
| Mock data | @carlos-tzin/tzin/test | Generate mocks |
Quick Start
1. Scaffold
npx create-tzin my-api
cd my-api2. Define a contract
// src/routes/users.ts
import { t, contract, impl } from '@carlos-tzin/tzin'
export const getUser = contract({
method: 'GET',
path: '/users/:id',
params: t.Object({ id: t.String() }),
responses: {
200: t.Object({ id: t.String(), name: t.String() }),
404: t.Object({ error: t.String() }),
},
})
export const getUserRoute = impl(getUser, async ({ params }) => {
return { status: 200, body: { id: params.id, name: 'Ada' } }
})3. Create the app
// src/app.ts
import { createApp } from '@carlos-tzin/tzin'
import { getUserRoute } from './routes/users.js'
export const app = createApp([getUserRoute], {
openapi: true,
mcp: true,
meta: { title: 'My API', version: '1.0.0' },
})4. Run
npm run dev # tzin dev -> dev server at http://localhost:3000
npm run build # tzin build -> production buildThe tzin binary ships with the package, so these scripts work after npm install. Without
a local install, run it through npm exec:
npx --package @carlos-tzin/tzin tzin devCLI
tzin dev [--port N] # dev server with hot reload
tzin build # build for production
tzin deploy --target node|workers [--pack] [--docker <tag> [--push]] # deploy
tzin generate route <name> # scaffold a route
tzin generate middleware <name> # scaffold middleware
tzin generate test <name> # scaffold a testBenchmarks
100 endpoints with distinct schemas, checked by tsc --extendedDiagnostics:
| N routes | tzin: types | Hono: types | tzin: instantiations | Hono: instantiations | |---|---|---|---|---| | 20 | 4,851 | 72,368 | 22,472 | 156,054 | | 100 | 15,779 | 111,952 | 93,128 | 240k | | 300 | 43,099 | 210,912 | 270k | 590k |
tzin grows strictly linearly (~130 types/endpoint).
Runtime throughput
| Framework | req/s | p99 | |---|---|---| | raw node:http (floor) | ~42k | 2–3ms | | hono | ~34–36k | 3ms | | tzin | ~30–31k | 6ms | | express | ~15k | 7ms |
Examples
| Example | Runtime | What it shows |
|---|---|---|
| examples/node-demo.ts | Node | Minimal HTTP server with contracts |
| examples/bun-demo.ts | Bun | Bun.serve with validation |
| examples/todo-api.ts | Node | Full CRUD: auth middleware, DI, OpenAPI, MCP |
| examples/mcp-demo.ts | Node | MCP server over stdio |
| examples/ws-demo.ts | Bun | WebSocket channels with presence |
| examples/workers-quickstart/ | Workers | Copy-paste wrangler project: DO + channels + presence |
| examples/workers-quickstart.md | Workers | Same project, explained file-by-file |
Design principles
- Contract-first, flat registry. A route is data (
contract({...})), not a link in a builder chain. Inference cost stays O(1) per endpoint. - JSON Schema native. TypeBox schemas double as OpenAPI with zero conversion.
- Web Standards runtime.
Requestin,Responseout; adapters stay thin. - Extractors over magic. Handlers receive exactly what the contract declares.
- Errors are control flow. Throw typed errors; the response union already knows.
- Light DI without ceremony.
provide(key, value)at app level seeds typed singletons into every request's context — handlers justctx.require(db).
Development
npm install
npm test # vitest — runtime + end-to-end client + type assertions
npm run typecheck # strict tsc across src/test/bench fixturesLicense
MIT — see LICENSE.
