Choosing a TypeScript API framework in 2026
Updated 2026-08-04 · Disclosure: we build Nifra, one of the options below. The framework-agnostic part - the three levels of "typed" - applies no matter what you pick.
Every framework claims TypeScript support. The claims mean three very different things, and knowing which level you are buying is the whole decision. Level 1: your handlers are typed. Level 2: your client is typed from the server. Level 3: the types are enforced at runtime too. Most production incidents blamed on "TypeScript didn't catch it" are a missing level, not a missing annotation.
The three levels of "typed"
- Level 1 - typed handlers. Express with
@types/express, Koa, most classic frameworks. The compiler checks your server code against itself. Nothing checks the caller. - Level 2 - inferred contract. The client's request and response types derive from the server's types: tRPC, Elysia's Eden, Hono's
hc, Nifra'sclient<typeof app>(), ts-rest. A renamed field breaks the frontend build instead of production. The differentiators inside this level: does it need codegen (ts-rest and OpenAPI generators do; tRPC/Eden/hc/nifra don't), and does it survive non-TypeScript callers (tRPC's RPC shape doesn't map cleanly to plain HTTP; the others stay REST-shaped). - Level 3 - runtime validation. Types are erased at runtime; a public endpoint typed
{ age: number }still receives{ age: "99; DROP TABLE" }unless something checks. Fastify (JSON Schema), Elysia (TypeBox), NestJS (class-validator), and Nifra (Standard Schema - Zod, Valibot, ArkType, or hand-rolled) validate declared schemas at the boundary. tRPC validates if you attach a schema per procedure; Hono via middleware. The question to ask: is validation the default path or a discipline?
The field, scored
| Framework | Typed client | Codegen needed | Runtime validation | Node req/s (our run, validated POST) |
|---|---|---|---|---|
| Nifra | Inferred, REST-shaped | No | Default (Standard Schema) | 59,764 |
| Fastify | No (add OpenAPI + generator) | Yes, for a client | Default (JSON Schema) | 53,442 |
| Elysia | Inferred (Eden) | No | Default (TypeBox) | 44,130 (on Node; stronger on Bun) |
| tRPC | Inferred, RPC-shaped | No | Per-procedure schemas | n/a (rides another server) |
| Hono | Inferred (hc) | No | Opt-in middleware | 32,332 |
| NestJS | No (OpenAPI + generator) | Yes, for a client | Opt-in decorators | (slowest of the set in public benchmarks) |
Throughput from our published harness (oha @ 50 conns, identical semantics; all rows + methodology on benchmarks). tRPC is a contract layer over an HTTP server rather than a server itself, so it has no row - pair its column with whichever host you'd run.
How to choose
- Internal tools, TS on both ends, RPC feel is fine: tRPC remains excellent. Its weakness is the boundary with anything that isn't your TypeScript monorepo - mobile teams, partners, curl.
- Backend-only, maximum battle-testing: Fastify - accept the codegen step if you need a typed client.
- Bun-first backend: Elysia. Everything-runtimes minimal: Hono. (head-to-head.)
- All three levels, REST-shaped, plus a frontend: Nifra - the inferred client extends beyond fetch calls into SSR loaders, pages, and server functions for React/Vue/Svelte/Solid/Preact, validation is the default, and the API surface ships as a live MCP server so AI agents write against your real contract. Fastest of the set in our Node runs, and the same app moves to Bun for ~2x (measured).
The test to run before committing
Whatever you shortlist: rename one response field on the server and see what breaks. If the answer is "nothing until runtime", you are at Level 1 with extra steps. Then POST a malformed body at a typed endpoint. If it reaches your handler, you are missing Level 3. Ten minutes, and it filters the field faster than any comparison table - including this one. (bunx create-nifra my-app if you want to run it on ours first.)