Skip to Content
BlogTypeScript Backend Frameworks Compared

TypeScript Backend Frameworks Compared

REST Semantics, Validation, Next.js, RPC, LLM Tools, and Local Call

The TypeScript backend ecosystem has fragmented into ~30 frameworks competing for the same problem space. This article evaluates them on six concrete axes that matter for production systems: REST URL semantics, Next.js compatibility, validation library, typed RPC client, LLM tool derivation, and validated in-process call.

This is not an exhaustive feature list — it’s a focused comparison of one specific design pattern: the single-source-of-truth procedure thesis, where one typed declaration auto-derives an HTTP endpoint, a typed client, an LLM tool, and an in-process call. Vovk.ts is built around this thesis. The natural question is: which other frameworks deliver on the same promise, and where does each draw the line?


TL;DR

  • Four TypeScript frameworks deliver one declaration → REST + RPC client + LLM tool + validated local call: Vovk.ts, Nestia, oRPC, Igniter.js.
  • Of those four, three mount on Next.js App Router — Vovk, oRPC, Igniter.js — while Nestia is NestJS-only.
  • Vovk.ts is a thin layer over Next.js Route Handlers — it owns the procedure declaration and tooling, inheriting the Next.js ecosystem for auth, deployment, MCP transport (mcp-handler), and so on. Among the SSOT four, it has the deepest substrate integration (per-segment serverless functions with independent maxDuration / runtime, on-disk schema artifacts in .vovk-schema/) plus first-party Python and Rust client codegen (🧪 experimental).
  • True validated local-call bypass — calling a procedure in-process with its validation — is ecosystem-rare. Most “test without HTTP” stories (Hono app.request, Fastify inject, Elysia app.handle) are HTTP simulation, not bypass.
  • MCP server transport: only Igniter.js bundles one (@igniter-js/adapter-mcp-server). Vovk and oRPC delegate to mcp-handler / @modelcontextprotocol/sdk.
  • Standard Schema (Zod / Valibot / ArkType) is the emerging consensus. class-validator / TypeBox / Effect Schema persist where the framework predates the spec.

What the columns mean

REST semantics

Does the framework produce RESTful URLs (GET /users/123) or RPC envelopes (POST /trpc/users.getById?batch=1)? This is the most contentious column because frameworks blur the line. We mark ✅ Native only when URLs are resource-style with method + path + path-param semantics. 🔌 Plugin when REST URLs are opt-in via a separate package (e.g. tRPC’s trpc-to-openapi). ⚠️ when there’s a split surface (RPC default, REST as a separate concept). when URLs are RPC envelopes.

Next.js compatibility (App Router)

Does this mount on Next.js App Router? ✅ Native = designed for Next.js. 🔌 Adapter = official adapter package or documented mounting pattern. ⚠️ = possible via custom-server workarounds. = incompatible (own runtime / is a Next.js alternative).

Validation

Which validation library does the framework use? ✅ Standard Schema = accepts any Standard Schema  implementation (Zod / Valibot / ArkType / Effect Schema). 🔌 = one library only (Zod, TypeBox, class-validator, VineJS, typia). = no built-in validation.

Typed RPC client

A type-safe client that calls your endpoints. ✅ Native = first-party client. Sub-modes: pure inference (no codegen, TS type flows), codegen (build step), OpenAPI roundtrip (spec → external generator). = no typed client.

LLM tool derivation

Auto-derived { name, description, parameters, execute } tool objects from existing procedures. ✅ Native = same procedure declaration becomes a tool. 🔌 = via first-party package. OpenAPI bridge = only through OpenAPI doc + external generator. = no auto-derivation (write @Tool methods manually).

Local call

Calling a procedure in-process, without HTTP, with full input validation. ✅ True bypass = no Request construction, no network, validation runs. HTTP simulation = framework constructs a Request and runs the middleware stack (e.g. Hono app.request, Fastify inject, Elysia app.handle). Plain function = the handler can be called as a function but skips validation. = none.


Master table

Framework names link to their detailed breakdown below.

FrameworkREST semanticsNext.js (App Router)ValidationTyped RPC clientLLM toolLocal call
Vovk.ts✅ Native✅ Native✅ Standard Schema✅ CodegenderiveTools.fn() bypass
Nestia✅ Native❌ NestJS-only🔌 typia✅ AST codegentypia.llm + @agentica⚠️ controller skip
oRPC✅ Native (OpenAPIHandler)🔌 Adapter✅ Standard Schema✅ Pure inference@orpc/ai-sdk.callable()
Igniter.js✅ Native✅ Built-in adapter✅ Standard Schema@igniter-js/calleradapter-mcp-server✅ Native
Effect Platform✅ Native (HttpApi)🔌 toWebHandler🔌 Effect Schema✅ HttpApiClient⚠️ parallel to HttpApi✅ Effect-runtime
Encore.ts✅ Native❌ Own runtime🔌 TS typesencore gen client❌ dev-tool MCP only✅ service-to-service
FeathersJS⚠️ CRUD-mapped⚠️ Custom server🔌 @feathersjs/schemafeathers-clientapp.service()
Elysia✅ Native⚠️ Bun-optimized🔌 TypeBox✅ Eden Treaty🔌 @8monkey/elysia-mcp⚠️ HTTP simulation
TanStack Start⚠️ split (server fns RPC + API Routes REST)❌ Next.js alternative✅ Standard Schema✅ Native✅ direct fn call
tRPC🔌 trpc-to-openapi🔌 Adapter✅ Standard Schema✅ Pure inferencecreateCaller
ts-rest✅ Native🔌 @ts-rest/next✅ Standard Schema✅ Pure inference
Hono✅ Native🔌 hono/vercel adapter✅ Standard Schema (via validator pkgs)hc pure inference⚠️ app.request simulation
NestJS (vanilla)✅ Native⚠️ Custom server🔌 class-validator🔌 OpenAPI roundtrip🔌 @rekog/mcp-nest (manual)⚠️ skips pipes
AdonisJS + Tuyau✅ Native❌ Own runtime🔌 VineJS✅ Tuyau codegen🔌 @jrmc/adonis-mcp (manual)⚠️ HttpContext fabrication
Fastify✅ Native⚠️ Custom server🔌 JSON Schema / TypeBox / Zod❌ OpenAPI roundtrip only🔌 fastify-mcp (transport)⚠️ inject (simulation)
next-safe-action❌ RSC RPC✅ Native✅ Standard Schema⚠️ React hooks only✅ direct import
zsa❌ RSC RPC✅ Native🔌 Zod⚠️ React-only✅ direct import
mcp-handler— (not a framework)✅ Native🔌 Zod❌ manual registration
Blitz.js❌ JSON-RPC envelope⚠️ Pages legacy🔌 Zod✅ build-time rewrite✅ resolver call
Wasp⚠️ split (operations RPC + api REST)❌ Own DSL❌ BYO✅ codegen✅ operations call
Next.js raw Route Handlers✅ Native✅ Native❌ BYO⚠️ forge Request
Tier C frameworks

On the LLM-tool column: none of the four 4/4 SSOT frameworks (Vovk, Nestia, oRPC, Igniter.js) ships an end-to-end MCP server transport. Vovk’s deriveTools() produces tool objects with a ToModelOutput.MCP formatter; the transport is delegated to mcp-handler. oRPC’s @orpc/ai-sdk produces ai-sdk tool definitions; MCP server requires @modelcontextprotocol/sdk directly. Nestia’s @agentica runtime is comprehensive but again does not bundle an MCP HTTP transport. Only Igniter.js packages its own MCP server end-to-end via @igniter-js/adapter-mcp-server.


Beyond the six axes: where the Tier S frameworks differentiate

Several secondary capabilities meaningfully differentiate the four frameworks that hit 4/4 SSOT:

Bonus axisVovk.tsNestiaoRPCIgniter.js
Multi-language clients (Python, Rust)✅ first-party (🧪 experimental)❌ TS onlycommunity via OpenAPI roundtrip❌ TS only
Schema artifacts persisted on disk.vovk-schema/*.json❌ SDK files, no schema artifact❌ pure inference❌ pure inference
Per-segment serverless function config✅ independent maxDuration / runtimen/a (NestJS)❌ single handler❌ single handler
Static segments (build-time JSON content)
First-party MCP server transport❌ delegates to mcp-handler❌ delegates❌ delegatesadapter-mcp-server
Streaming as part of procedure declaration✅ JSON Lines✅ Event Iterator (SSE)⚠️
Typed errors flow to clientpartial via HttpException✅ explicit .errors()

Vovk leads on four of these (multi-language clients, schema persistence, segments-as-functions, static segments); ties oRPC on streaming; loses to Igniter.js on first-party MCP server transport and loses to oRPC on typed-error ergonomics. oRPC chose runtime portability — Vovk chose Next.js depth. Both are valid trade-offs, but a buyer who has already committed to Next.js should weight the four Vovk-leading rows heavily, because every one of them requires the Next.js substrate Vovk targets. Sources: vovk.dev , orpc.dev/docs/event-iterator , nestia.io , github.com/felipebarcelospro/igniter-js .


Which framework when

If your project matches one of these descriptions, that’s the framework to start with.

You’re building…Pick
Next.js App Router app + REST endpoints + AI tools, all from one declarationVovk.ts
Next.js App Router app, but want a pure-inference client with no codegen stepoRPC with OpenAPIHandler
Already invested in NestJS, want SSOT for AI toolsNestia (@nestia/core + @agentica)
Next.js Server Actions only, no external clientnext-safe-action
Bun-first, no Next.jsElysia + Eden Treaty
Hono / Fastify / Cloudflare Workers stack, runtime-agnosticoRPC with RPCHandler (or Hono + hand-rolled MCP)
Need WebSockets or duplex traffic todayNone of the SSOT four; pick Hono or Fastify
Want backend as a separate platform (own runtime, own CI)Encore.ts or Convex
Want the strongest contract-first REST + Zod story without a typed local callts-rest

Tier S — 4/4 SSOT (one declaration → all four artifacts)

Vovk.ts

Version: vovk@3.2.2. Requires Next.js 15+, Node 22+. Source: vovk.dev .

REST semantics

Procedures decorated with @get('{id}'), @post(), @put(), @patch(), @del() mount as RESTful endpoints with path-param syntax.

src/modules/user/UserController.ts
import { procedure, prefix, get } from 'vovk'; import { z } from 'zod'; @prefix('users') export default class UserController { @get('{id}') static getUser = procedure({ params: z.object({ id: z.uuid() }), output: z.object({ id: z.string(), name: z.string() }), }).handle(async (req, { id }) => { return { id, name: 'Alice' }; }); }

Source: vovk.dev/procedure 

curl http://localhost:3000/api/users/0190ad59-c3e3-71e0-bbaf-2c4c4f2f8a3f

Next.js compatibility

Native. Vovk targets Next.js App Router exclusively — controllers mount into segments (catch-all routes) via initSegment. No standalone server.

src/app/api/[[...vovk]]/route.ts
import { initSegment } from 'vovk'; import UserController from '@/modules/user/UserController'; const controllers = { UserRPC: UserController }; export type Controllers = typeof controllers; export const { GET, POST, PUT, DELETE, PATCH } = initSegment({ controllers });

Source: vovk.dev/segment 

Validation

Standard Schema . Any library implementing both Standard Schema and Standard JSON Schema (Zod 4, Valibot, ArkType) works in procedure({ params, query, body, output }).

Source: vovk.dev/procedure#procedure 

Typed RPC client

Codegen via vovk generate (or vovk dev watcher) emits vovk-client — a TypeScript module mirroring controllers, same call signature as .fn().

import { UserRPC } from 'vovk-client'; const user = await UserRPC.getUser({ params: { id: '0190ad59-c3e3-71e0-bbaf-2c4c4f2f8a3f' }, });

Source: vovk.dev/typescript 

LLM tool derivation

Native, first-party. deriveTools() converts any controller or RPC module into { name, description, parameters, execute } tool objects compatible with Vercel AI SDK out of the box. The ToModelOutput.MCP formatter shapes execute-output for MCP transports.

import { deriveTools } from 'vovk'; import UserController from '@/modules/user/UserController'; const { tools, toolsByName } = deriveTools({ modules: { UserController } }); // tools: [{ name, description, parameters, execute }, ...]

Source: vovk.dev/tools 

The actual MCP server transport is delegated to mcp-handler (Vercel’s package). Same delegation pattern as oRPC’s AI SDK integration.

Local call

True bypass via .fn(). No Request construction, no network — full input validation runs.

const user = await UserController.getUser.fn({ params: { id: '0190ad59-c3e3-71e0-bbaf-2c4c4f2f8a3f' }, });

Source: vovk.dev/fn 

Used in server components, server actions, SSR/PPR, and AI tool execution.

In a Next.js App Router app, validated bypass matters in three concrete places: (a) React Server Components fetching data without HTTP overhead; (b) Server Actions composing existing procedures without re-serializing; (c) AI tools executing in-process so token streams don’t round-trip the network. The same procedure declaration serves all three plus its HTTP endpoint.

Notable Vovk-specific capabilities

Beyond the six-axis rubric, Vovk ships a few capabilities that other Tier S frameworks don’t match. All verifiable on vovk.dev :

  • Per-segment serverless function config. Each segment compiles to its own Next.js catch-all route, which becomes its own serverless function — with its own maxDuration, runtime (e.g., 'edge'), and bundle. Most competitors mount as a single handler.
  • Multi-language client codegen. First-party vovk-python and vovk-rust emit typed clients in Python and Rust from the same .vovk-schema. 🧪 experimental, but no competitor on this list emits non-TS clients first-party.
  • Schema artifacts on disk. .vovk-schema/*.json is a build artifact, not just an inferred type. CI tools, codegen pipelines, and AI agents can read the API surface without running the dev server.
  • Static segments. Compile-time-generated JSON for content that doesn’t change at runtime (OpenAPI specs, historical data). Served as a static file.
  • decorate() escape hatch. decorate(get('{id}'), procedure(...)) produces identical runtime and types without requiring experimentalDecorators in tsconfig.json — for projects that can’t enable the legacy TS decorator flag.

Streaming (JSON Lines via iteration validator) is a first-class procedure-level capability; oRPC has parity via its Event Iterator  (SSE).

Verdict

4/4 SSOT. Vovk.ts is a thin layer over Next.js App Router Route Handlers — it owns the procedure declaration, codegen, and tooling, and inherits the Next.js ecosystem for everything else (auth, deployment, ISR/PPR, mcp-handler for the MCP transport, next-ws for WebSocket, etc.). That positioning matters for evaluating the trade-offs honestly: many things missing from Vovk are missing from Next.js Route Handlers, not from Vovk specifically.

Trade-offs that are Vovk-specific (you wouldn’t hit these by writing raw Route Handlers):

  • Tied to Next.js App Router. Not a portable library. Services (plain classes with no framework imports) transfer; controllers, decorators, and procedure() definitions don’t. Among the four 4/4 SSOT frameworks, Vovk is the only Next.js-exclusive one: Nestia is NestJS-only, Igniter.js has a Next.js adapter but isn’t Next.js-exclusive, oRPC is runtime-agnostic.
  • Codegen step required. Pure-inference frameworks (tRPC, oRPC, Hono hc, Eden Treaty) don’t need a build step; Vovk does. vovk dev must run alongside next dev in development. The on-disk artifact (.vovk-schema/*.json) has real value — CI tools, AI agents, multi-language clients all consume it — but it’s a workflow tax pure-inference frameworks avoid.
  • Ecosystem size. Smaller community than tRPC, NestJS, Hono. Fewer Stack Overflow answers, fewer third-party plugins, less battle-tested edge-case coverage. (Major-version numbers — Vovk 3.x vs tRPC 11.x — reflect different semver philosophies and aren’t a meaningful comparison; weekly downloads and contributor counts are.)
  • Decorator-based idiomatic API. @get('{id}')-style decorators require "experimentalDecorators": true in tsconfig.json. Next.js’s toolchain throws on TC39 Stage 3 decorator syntax — not a Vovk bug, but it constrains your tsconfig. decorate() is the escape hatch for projects that can’t enable the legacy flag.
  • deriveTools tool-count ceiling. Very large APIs hit per-conversation LLM tool-list limits. The roadmap includes a vector-search-gated router option to scale this; today you expose a subset of procedures or split agents per domain.
  • Python/Rust clients are experimental (🧪). Stable for typical CRUD; expect rough edges on advanced patterns until they leave 🧪.

Constraints inherited from the Next.js Route Handler substrate — not Vovk choices, but worth knowing if you’re picking the stack: no WebSockets / persistent connections (use next-ws or an external service), no first-party MCP server transport (use mcp-handler — the Next.js-canonical pattern; oRPC and Nestia delegate identically), shared cold start with page renders within a segment, no GraphQL on the same handler surface. Detailed in Architectural Trade-offs.


Nestia

Version: @nestia/core@11.2.0. Built on NestJS 11. Requires typia compile-time transformer. Source: nestia.io .

REST semantics

Native via @core.TypedRoute.Get('/:id')-style decorators that mirror NestJS routing.

import core from "@nestia/core"; import { Controller } from "@nestjs/common"; import typia, { tags } from "typia"; @Controller("bbs/articles/:section") export class BbsArticlesController { @core.TypedRoute.Put(":id") public async update( @core.TypedParam("section") section: string, @core.TypedParam("id") id: string & tags.Format<"uuid">, @core.TypedBody() input: IBbsArticle.IStore, ): Promise<IBbsArticle> { // ... } }

Source: nestia.io/docs/sdk/ 

Next.js compatibility

NestJS only. Nestia requires NestFactory.create(); no Next.js adapter exists.

Validation

🔌 typia. TypeScript types act as runtime validators via compile-time transformation. Not Standard Schema — different paradigm (types-as-validators). Validators are synthesized from your TS interfaces at build time, with near-zero runtime cost.

Typed RPC client

✅ AST-direct codegen via @nestia/sdkno OpenAPI roundtrip.

npx nestia sdk

Generated SDK shape:

import api from "./api"; const output = await api.functional.bbs.articles.update( connection, section, id, input, ); if (output.success) { const article: IBbsArticle = output.data; }

Source: nestia.io/docs/sdk/ 

LLM tool derivation

✅ via typia.llm.application<App>() and @agentica/core. The same TypeScript controller methods become function-calling schemas. Auto-derived from controller types, not from separate @Tool definitions. Source: nestia.io .

Local call

⚠️ NestJS controllers can be instantiated and methods called as functions, but @core.TypedRoute validation runs at the HTTP transport layer — direct controller calls skip it unless you wire validation manually. The recommended path is calling underlying service methods.

Verdict

4/4 SSOT (with caveats). The architectural proof-point that procedure-as-SSOT works. NestJS-only. typia instead of Standard Schema. If you’re already on NestJS, this is the obvious choice.


oRPC

Versions: @orpc/server@1.14.2, @orpc/client@1.14.2, @orpc/openapi@1.14.2, @orpc/ai-sdk@1.14.2. Runtime-agnostic. Source: orpc.dev .

REST semantics

oRPC ships two handlers: RPCHandler (proprietary envelope behind a prefix) and OpenAPIHandler (REST URLs from .route({ method, path })). For REST semantics, use OpenAPIHandler and define routes explicitly.

import { os } from '@orpc/server'; import * as z from 'zod'; export const getPlanet = os .route({ method: 'GET', path: '/planets/{id}' }) .input(z.object({ id: z.coerce.number() })) .handler(async ({ input }) => ({ id: input.id, name: 'Earth' }));
curl http://localhost:3000/api/planets/1

Source: orpc.dev/docs/openapi/routing 

Next.js compatibility

🔌 Adapter. Mount RPCHandler or OpenAPIHandler from @orpc/server/fetch on a catch-all route. No separate @orpc/next package — the adapter is the documented pattern using the framework-agnostic fetch handler.

app/rpc/[[...rest]]/route.ts
import { RPCHandler } from '@orpc/server/fetch'; import { router } from '@/router'; const handler = new RPCHandler(router); async function handleRequest(request: Request) { const { response } = await handler.handle(request, { prefix: '/rpc', context: {}, }); return response ?? new Response('Not found', { status: 404 }); } export const HEAD = handleRequest; export const GET = handleRequest; export const POST = handleRequest; export const PUT = handleRequest; export const PATCH = handleRequest; export const DELETE = handleRequest;

Source: orpc.dev/docs/adapters/next . Note: the example above mounts RPCHandler (oRPC’s own envelope); swap to OpenAPIHandler from @orpc/openapi/fetch for the REST-semantic path.

Validation

Standard Schema  — Zod (@orpc/zod), Valibot (@orpc/valibot), ArkType (@orpc/arktype), or any Standard Schema implementation. Source: orpc.dev/docs/getting-started .

Typed RPC client

Pure TS inference — no codegen. createORPCClient<typeof router>(link).

import { createORPCClient } from '@orpc/client'; import { RPCLink } from '@orpc/client/fetch'; const link = new RPCLink({ url: 'http://localhost:3000/rpc', headers: () => ({ authorization: 'Bearer token' }), }); const client = createORPCClient<typeof router>(link); const planet = await client.planet.find({ id: 1 });

Source: orpc.dev/docs/client/client-side 

LLM tool derivation

@orpc/ai-sdk provides createTool() and implementTool() for Vercel AI SDK conversion. There is no first-party @orpc/mcp package — MCP exposure goes through the OpenAPI spec via a third-party generator.

import { createTool, AI_SDK_TOOL_META_SYMBOL } from '@orpc/ai-sdk'; import { os } from '@orpc/server'; import * as z from 'zod'; const getWeatherProcedure = os .meta({ [AI_SDK_TOOL_META_SYMBOL]: { title: 'Get Weather' } }) .route({ summary: 'Get the weather in a location' }) .input(z.object({ location: z.string() })) .handler(async ({ input }) => ({ location: input.location, temperature: 72 })); const getWeatherTool = createTool(getWeatherProcedure, { context: {} });

Source: orpc.dev/docs/integrations/ai-sdk 

Local call

.callable(), call(), createRouterClient — all true bypass with validation. .actionable() for Next.js Server Actions.

// .callable() turns a procedure into a plain async function const getProcedure = os .input(z.object({ id: z.string() })) .handler(async ({ input }) => ({ id: input.id })) .callable({ context: {} }); const result = await getProcedure({ id: '123' }); // createRouterClient — typed client that bypasses HTTP const client = createRouterClient(router, { context: {} }); const result2 = await client.planet.find({ id: 1 });

Source: orpc.dev/docs/client/server-side 

Server Action variant:

'use server'; import { os } from '@orpc/server'; import * as z from 'zod'; export const ping = os .input(z.object({ name: z.string() })) .handler(async ({ input }) => `Hello, ${input.name}`) .actionable({ context: async () => ({}) });

Source: orpc.dev/docs/server-action 

Verdict

4/4 SSOT. Closest existential competitor to Vovk on this rubric. Router-builder pattern vs Vovk’s decorator/controller pattern. Runtime-portable (Next.js, Hono, Fastify, Express, Lambda) vs Vovk’s Next.js-only. No first-party MCP server transport — same delegation pattern as Vovk.


Igniter.js

Versions: @igniter-js/core@0.3.40, @igniter-js/adapter-mcp-server@0.3.7. Pre-1.0 — moving fast, ecosystem is thin. Source: igniterjs.com .

REST semantics

Native. Controllers expose queries (read) and mutations (write) at canonical REST URLs.

Next.js compatibility

✅ Built into core via nextRouteHandlerAdapter exported from @igniter-js/core — no separate adapter package needed.

Validation

Standard Schema (Zod most commonly).

Typed RPC client

@igniter-js/caller — type-safe HTTP client with retries, cache, interceptors.

LLM tool derivation

The only TS framework besides Vovk and Nestia with a first-party packaged MCP server transport. @igniter-js/adapter-mcp-server auto-derives MCP tools from the router queries/mutations. No separate @Tool definitions required.

import { IgniterMcpServer } from '@igniter-js/adapter-mcp-server'; import { AppRouter } from '@/igniter.router'; const { handler } = IgniterMcpServer .create() .router(AppRouter) .withServerInfo({ name: 'Igniter.js MCP Server', version: '1.0.0', }) .withInstructions("Use the available tools to manage users and products in the Acme Corporation API.") .build(); export const GET = handler; export const POST = handler;

Per the package README: “The adapter will automatically expose all of your Igniter.js API actions as MCP tools without requiring separate definitions.” Source: github.com/felipebarcelospro/igniter-js — adapter-mcp-server README .

Local call

✅ Native. Procedures and actions callable in-process.

Verdict

4/4 SSOT on paper. Most “AI-native” TS framework after Vovk and Nestia for the MCP sub-feature — it’s the only one that bundles a packaged MCP server adapter. Caveats: pre-1.0 (0.3.x), small ecosystem, less battle-tested than Vovk / oRPC / NestJS-Nestia.


Tier A — 3/4 SSOT (one structural gap)

Effect Platform

Versions: @effect/platform@0.96.1, @effect/ai@0.35.0. Source: effect.website .

REST semantics

Native via HttpApi / HttpApiGroup / HttpApiEndpoint. Method + path + path-param schema.

Next.js compatibility

🔌 Adapter. HttpApiBuilder.toWebHandler(...) returns a Web handler that mounts on a Next.js App Router catch-all route.

Validation

🔌 Effect Schema (now effect/Schema). Not Standard Schema by default — uses Effect’s own schema language tightly coupled with the runtime.

Typed RPC client

✅ Native via HttpApiClient.make(MyApi, { baseUrl }). Pure TS inference from the same HttpApi declaration that drives the server.

LLM tool derivation

⚠️ @effect/ai provides Tool.make, Toolkit.make, Toolkit.toLayer. No Toolkit.fromHttpApi auto-derivation as of v0.35.0. Tools share schemas with HttpApi endpoints but require separate definitions — parallel to HttpApi, not auto-derived.

Local call

✅ HTTP handlers are Effects. Run them directly via Effect.runPromise with the appropriate Layer. (toWebHandler itself is HTTP simulation; the true bypass is at the Effect-runtime level.)

Verdict

3/4. Excellent SSOT for REST + client + local call from one HttpApi declaration. AI tool layer parallels HttpApi rather than auto-deriving from it. The biggest commitment is Effect-runtime buy-in.


Encore.ts

Version: encore.dev@1.57.1. Source: encore.dev/docs/ts .

REST semantics

Native.

import { api } from "encore.dev/api"; interface PingParams { name: string } interface PingResponse { message: string } export const ping = api( { method: "POST", path: "/ping/:name", expose: true }, async (p: PingParams): Promise<PingResponse> => ({ message: `Hello ${p.name}!` }), );

Source: encore.dev/docs/ts/primitives/services-and-apis 

Next.js compatibility

❌ Encore is its own platform/runtime, not a library. Not mountable on Next.js.

Validation

🔌 TS types via Rust-based parser at build time. Brand types like MinLen, IsEmail for constraints. No external validator library involved.

Typed RPC client

✅ Codegen via encore gen client <app> --output=./client.ts. Supports TypeScript, JavaScript, Go, OpenAPI.

encore gen client hello-a8bc --output=./client.ts
import { client } from './client'; const response = await client.email.Send(params);

Internal service-to-service calls go via ~encore/clients and are typed:

import { ping } from '~encore/clients'; const result = await ping({ name: 'world' });

Source: encore.dev/docs/ts/cli/client-generation 

LLM tool derivation

Encore’s encore mcp start is a dev-tooling MCP that exposes app metadata (services, schemas, traces) to coding agents like Cursor. It does NOT convert user-defined api() endpoints into runtime LLM tools for end-user agents. Source: encore.dev/docs/ts/cli/mcp .

Local call

✅ Service-to-service calls are typed and feel local. In production these are HTTP between deployed services; in development they’re in-process.

Verdict

3/4 (REST + client + local call all excellent). LLM-tool column is structurally missing despite “AI-native” branding — that branding refers to dev-tool MCP, not runtime API-as-tool.


FeathersJS

Version: Feathers v5 (Dove). Source: feathersjs.com .

REST semantics

⚠️ Service methods (find, get, create, update, patch, remove) map automatically to REST verbs and URLs. Custom methods don’t auto-get REST URLs — they require explicit route configuration.

class UserService { async find(params: Params) { return [] } async get(id: Id, params: Params) { /* ... */ } async create(data: any, params: Params) { /* ... */ } async update(id: NullableId, data: any, params: Params) { /* ... */ } async patch(id: NullableId, data: any, params: Params) { /* ... */ } async remove(id: NullableId, params: Params) { /* ... */ } } const app = feathers(); app.use('users', new UserService());

Source: feathersjs.com/api/services.html 

Next.js compatibility

⚠️ Custom server only. Feathers runs on Koa or Express.

Validation

🔌 @feathersjs/schema — Feathers’ own schema builder; can wrap Zod-compatible schemas.

Typed RPC client

feathers-client. Services are callable identically locally and remotely — that’s Feathers’ core SSOT thesis.

LLM tool derivation

❌ None first-party. OpenAPI bridge only via feathers-swagger.

Local call

✅ True service call: app.service('users').get(id). Hooks (Feathers’ middleware) run; validation runs if configured.

const myService = app.service('users'); const items = await myService.find(); const item = await app.service('users').get(1);

Source: feathersjs.com/api/services.html 

Verdict

3/4 within CRUD. Historical precedent for service-as-SSOT, but CRUD-shaped — custom verbs are second-class. Mature; Node-classic stack. The closest pre-Vovk SSOT analogue in the TS ecosystem.


Elysia

Version: elysia@1.4.28. Optimized for Bun, also runs on Node.js. Source: elysiajs.com .

REST semantics

Native.

import { Elysia, t } from 'elysia'; const app = new Elysia() .get('/users/:id', ({ params: { id } }) => ({ id, name: 'Alice' }), { params: t.Object({ id: t.String() }), }) .listen(3000); export type App = typeof app;

Source: elysiajs.com/essential/route.html 

Next.js compatibility

⚠️ Possible — Elysia 1.x runs on Node.js (per docs: “multiple runtime support but optimized for Bun”). Mounting on Next.js App Router works but isn’t the documented or idiomatic path. Source: elysiajs.com/quick-start.html .

Validation

🔌 TypeBox via elysia.t. JSON Schema-shaped at runtime.

Typed RPC client

✅ Eden Treaty — pure TS inference, no codegen.

import { treaty } from '@elysia/eden'; import type { App } from './server'; const app = treaty<App>('localhost:3000'); const { data, error } = await app.users({ id: '1' }).get();

Source: elysiajs.com/eden/treaty/overview.html 

LLM tool derivation

🔌 @8monkey/elysia-mcp (community) — auto-derives MCP tools from Elysia routes using TypeBox schemas. Niche. Source: npmjs.com/package/@8monkey/elysia-mcp .

Local call

⚠️ app.handle(new Request(...)) is HTTP simulation, not validated bypass.

Verdict

3/4 in Bun world. Outside Bun, it’s a Hono-class framework with a dedicated MCP plugin. Next.js compatibility is technical, not idiomatic.


TanStack Start

Version: @tanstack/react-start@1.167.65 (v1 stable since March 2026). Source: tanstack.com/start .

REST semantics

⚠️ Split. Server functions (createServerFn) are RPC-shaped — they compile to HTTP POSTs at framework-managed paths, NOT RESTful URLs. For true REST, use separate API Routes (file-based, /api/...).

import { createServerFn } from '@tanstack/react-start'; import { z } from 'zod'; const UserSchema = z.object({ name: z.string().min(1), age: z.number().min(0) }); export const createUser = createServerFn({ method: 'POST' }) .inputValidator(UserSchema) .handler(async ({ data }) => `Created user: ${data.name}, age ${data.age}`);

Per the docs: “the build process replaces server function implementations with RPC stubs in client bundles. When invoked from client code, calls become network requests to the server RPC endpoint.” Source: tanstack.com/start/latest/docs/framework/react/quick-start .

Next.js compatibility

❌ TanStack Start is its own full-stack meta-framework — a Next.js alternative, not a library you mount on Next.js.

Validation

✅ Standard Schema. .inputValidator() accepts Zod, Valibot, ArkType.

Typed RPC client

✅ Native — server functions imported on the client are rewritten at build time to fetch calls. Pure inference, no separate codegen step.

import { useServerFn } from '@tanstack/react-start'; function UserForm() { const createNewUser = useServerFn(createUser); const { mutate } = useMutation({ mutationFn: () => createNewUser({ data: { name: 'John', age: 30 } }), }); }

LLM tool derivation

❌ None.

Local call

✅ Server-fn-to-server-fn calls are direct function calls — validation runs through .inputValidator.

Verdict

3/4 within its own ecosystem. Strongest validator-first server-function story outside Vovk/oRPC. REST and AI-tool stories are the structural gaps.


Tier B — 2/4 SSOT

tRPC

Versions: @trpc/server@11.17.0, trpc-to-openapi@3.2.0 (community fork). Official @trpc/openapi@11.17-alpha exists. Source: trpc.io .

REST semantics

🔌 Plugin and opt-in per procedure. Vanilla tRPC v11 routes go through /trpc/<procedure>?batch=1&input=... — RPC envelope over batched POST. For REST URLs, add .meta({ openapi }) to procedures and serve via trpc-to-openapi:

const router = t.router({ getUser: t.procedure .meta({ openapi: { method: 'GET', path: '/users/{id}' } }) .input(z.object({ id: z.string() })) .output(z.object({ id: z.string(), name: z.string() })) .query(({ input }) => ({ id: input.id, name: 'Alice' })), });

Each procedure you want exposed needs the .meta(). Procedures without it are not REST-exposed. Source: npmjs.com/package/trpc-to-openapi .

Next.js compatibility

🔌 Adapter. App Router uses the fetch adapter:

app/api/trpc/[trpc]/route.ts
import { fetchRequestHandler } from '@trpc/server/adapters/fetch'; import { appRouter } from '@/server/router'; const handler = (req: Request) => fetchRequestHandler({ endpoint: '/api/trpc', req, router: appRouter, createContext: () => ({}), }); export { handler as GET, handler as POST };

URL pattern: /api/trpc/<procedure> (RPC envelope, not REST — see column 1). Source: trpc.io/docs/server/adapters/nextjs .

Validation

Standard Schema (Zod, Valibot, ArkType, custom).

Typed RPC client

@trpc/client — pure TS inference, fetch-based.

LLM tool derivation

trpc-mcp exists but is abandoned (24 downloads/month at time of writing). Practical path: trpc-to-openapi → OpenAPI doc → external MCP generator. Not first-party.

Local call

createCallerFactory — true bypass, full validation runs.

const t = initTRPC.context<Context>().create(); const { createCallerFactory, router } = t; const appRouter = router({ post: router({ add: publicProcedure .input(z.object({ title: z.string().min(2) })) .mutation((opts) => { // procedure logic }), }), }); const createCaller = createCallerFactory(appRouter); const caller = createCaller({ /* context */ }); const result = await caller.post.add({ title: 'Example' });

Note: the docs warn that createCaller “should not be used to call procedures from within other procedures” — it recreates context and re-runs middleware. Extract shared logic into plain functions. Source: trpc.io/docs/server/server-side-calls .

Verdict

2/4 native (RPC + local call). REST is plugin-and-opt-in. LLM tool requires the OpenAPI bridge. The most popular RPC framework, structurally weaker on REST than common perception.


ts-rest

Version: @ts-rest/core@3.52.1. Source: ts-rest.com .

REST semantics

Native, contract-first.

import { initContract } from '@ts-rest/core'; import { z } from 'zod'; const c = initContract(); export const contract = c.router({ getPosts: { method: 'GET', path: '/posts', query: z.object({ skip: z.number(), take: z.number() }), responses: { 200: c.type<Post[]>() }, headers: z.object({ 'x-pagination-page': z.coerce.number().optional() }), }, });

Server implementation binds handlers to contract entries:

const router = s.router(contract, { getPosts: async ({ query }) => ({ status: 200, body: await prisma.post.findMany({ skip: query.skip, take: query.take }), }), });

Source: github.com/ts-rest/ts-rest README 

Next.js compatibility

🔌 Adapter @ts-rest/next v3.52. Source: npmjs.com/package/@ts-rest/next .

Validation

Standard Schema (Zod, Valibot, ArkType).

Typed RPC client

initClient(contract, ...) — pure TS inference.

const result = await client.getPosts({ headers: { 'x-pagination-page': 1 }, query: { skip: 0, take: 10 }, });

Source: github.com/ts-rest/ts-rest README 

LLM tool derivation

❌ None first-party. @ts-rest/open-api → external MCP generator.

Local call

❌ No validating caller. Handlers are plain functions you can import, but no first-class validated-bypass API.

Verdict

2/4. Excellent contract → REST + client SSOT. Punts on local and AI.


Hono

Versions: hono@4.12.18, @hono/mcp@0.2.5. Source: hono.dev .

REST semantics

Native.

import { Hono } from 'hono'; import { zValidator } from '@hono/zod-validator'; import { z } from 'zod'; const app = new Hono(); const route = app.post( '/posts', zValidator('form', z.object({ title: z.string(), body: z.string() })), (c) => c.json({ ok: true, message: 'Created!' }, 201), ); export type AppType = typeof route;

Source: hono.dev/docs/guides/rpc 

Next.js compatibility

🔌 Adapter. app/api/[[...route]]/route.ts:

app/api/[[...route]]/route.ts
import { Hono } from 'hono'; import { handle } from 'hono/vercel'; const app = new Hono().basePath('/api'); app.get('/hello', (c) => c.json({ message: 'Hello Next.js!' })); export const GET = handle(app); export const POST = handle(app);

Source: hono.dev/docs/getting-started/nextjs 

Validation

Standard Schema via @hono/zod-validator, @hono/valibot-validator, @hono/arktype-validator, @hono/typebox-validator. Per-validator package — but the surface is consistent.

Typed RPC client

hc<typeof app>(url) — pure TS inference, fetch-based, no codegen.

import { hc } from 'hono/client'; import type { AppType } from './server'; const client = hc<AppType>('http://localhost:8787/'); const res = await client.posts.$post({ form: { title: 'Hello', body: 'Hono is a cool project' }, }); if (res.ok) { const data = await res.json(); console.log(data.message); }

Source: hono.dev/docs/guides/rpc 

LLM tool derivation

@hono/mcp v0.3.0 is HTTP Streamable transport for an externally-instantiated McpServer — not auto-derivation. You write tools manually against @modelcontextprotocol/sdk. Community hono-mcp claims describe()-based exposure but is not first-party. The MCP team itself also publishes an official adapter, @modelcontextprotocol/hono@2.0.0-alpha.2 (currently pre-alpha), with the same transport-not-derivation shape. Sources: npmjs.com/package/@hono/mcp , github.com/modelcontextprotocol/typescript-sdk .

Local call

⚠️ app.request(req) is HTTP simulation (constructs Request, runs middleware) — not a validated bypass. Mostly used for testing.

Verdict

2/4. Strong REST + typed client (one of the best inferred-client stories in the ecosystem). No SSOT for AI tools, no true validated local-call.


NestJS

Version: @nestjs/core@11.1.19 (vanilla NestJS, without Nestia). Source: docs.nestjs.com .

REST semantics

Native via class decorators.

import { Controller, Get, Param } from '@nestjs/common'; @Controller('users') export class UsersController { @Get(':id') findOne(@Param('id') id: string) { return { id, name: 'Alice' }; } }

Source: docs.nestjs.com/controllers 

Next.js compatibility

⚠️ Custom server only. The historical Pages Router custom-server pattern is fragile on App Router. In practice, NestJS runs as its own process and you’d reverse-proxy from Next.

Validation

🔌 class-validator + class-transformer (DTO pattern). Not Standard Schema.

import { IsEmail, IsString } from 'class-validator'; export class CreateUserDto { @IsString() name: string; @IsEmail() email: string; }

Source: docs.nestjs.com/techniques/validation 

Typed RPC client

🔌 OpenAPI roundtrip. Generate OpenAPI via @nestjs/swagger, then run @hey-api/openapi-ts or NSwag for client codegen. No first-party typed client.

LLM tool derivation

🔌 Community packages (@rekog/mcp-nest, @nestjs-mcp/server). These typically require separate @Tool methods — not auto-derived from existing controllers. Pattern is “write a @Tool createUser() that internally calls the same service as @Post('/users')” — manual mirroring.

Local call

⚠️ app.get(UsersController).findOne(id) skips ValidationPipe and guards unless you wire them manually. Plain method call, not validated bypass.

Verdict

1.5/4. Mature, enterprise-grade. Each SSOT pillar requires an additional package or workaround. Nestia is the SSOT path within the NestJS ecosystem.


AdonisJS

Versions: AdonisJS v6 / @tuyau/core (typed client add-on). Source: adonisjs.com .

REST semantics

Native.

router.get('/posts/:id', ({ params }) => { return `This is post with id ${params.id}`; }); router.get('/posts/:id/comments/:commentId', ({ params }) => { console.log(params.id, params.commentId); });

Source: docs.adonisjs.com/guides/basics/routing 

Next.js compatibility

❌ AdonisJS is its own runtime (HTTP server, lifecycle, DI container).

Validation

🔌 VineJS — AdonisJS’s own validation library.

Typed RPC client

✅ Tuyau — codegen-based.

import { createTuyau } from '@tuyau/core/client'; import { registry } from '@my-app/backend/registry'; export const client = createTuyau({ baseUrl: 'http://localhost:3333', registry, headers: { Accept: 'application/json' }, }); async function handleCreatePost() { const post = await client.api.posts.store({ body: { title: 'My first blog post', content: 'This is the content of the blog post', published: true, }, }); }

The body shape is inferred from your VineJS validator. Source: docs.adonisjs.com/guides/frontend/api-client .

LLM tool derivation

🔌 @jrmc/adonis-mcp (community). Separate @Tool methods, not auto-derived.

Local call

⚠️ HttpContext fabrication required. No first-class validated bypass.

Verdict

2/4. Strong typed client and REST. Weaker on local call and AI tools.


Fastify

Version: fastify@5.8.5. Source: fastify.dev .

REST semantics

Native.

import Fastify from 'fastify'; const fastify = Fastify(); fastify.get('/users/:id', { schema: { params: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'], }, response: { 200: { type: 'object', properties: { id: { type: 'string' }, name: { type: 'string' } }, }, }, }, }, async (request, reply) => { const { id } = request.params as { id: string }; return { id, name: 'Alice' }; });

Source: fastify.dev/docs/latest/Reference/Validation-and-Serialization 

Next.js compatibility

⚠️ Custom server only.

Validation

🔌 JSON Schema natively. Standard Schema via type providers: @fastify/type-provider-zod (Zod), @fastify/type-provider-typebox (TypeBox).

Typed RPC client

❌ Codegen via @fastify/swagger → external OpenAPI generator (e.g. openapi-fetch). No first-party typed client.

LLM tool derivation

🔌 Multiple community packages (fastify-mcp, fastify-mcp-server) — all manual tool registration, not route-to-tool auto-derivation. The MCP team also publishes pre-alpha official adapters (@modelcontextprotocol/express, @modelcontextprotocol/node) that target Node-family servers including Fastify behind a Node HTTP listener; still transport-only.

Local call

⚠️ fastify.inject({ method, url, payload }) — HTTP simulation, not bypass. Mature for testing.

Verdict

1.5/4. Best JSON Schema story in the ecosystem; great performance. No native typed RPC, no SSOT AI tools.


next-safe-action

Version: next-safe-action@8.5.2. Source: next-safe-action.dev .

REST semantics

Server Actions are RSC RPC, not REST. They POST to React’s RSC endpoint with serialized payloads. Not curl-able with a normal URL.

Next.js compatibility

✅ Native (Next.js-specific).

Validation

Standard Schema (Zod, Valibot, ArkType via .inputSchema()).

import { createSafeActionClient } from "next-safe-action"; export const actionClient = createSafeActionClient(); export const loginUser = actionClient .inputSchema(loginSchema) .action(async ({ parsedInput: { username, password } }) => { const user = await verifyCredentials(username, password); return { id: user.id, name: user.name }; });

Source: next-safe-action.dev/docs/getting-started 

Typed RPC client

⚠️ React hooks only (useAction). No general-purpose client.

LLM tool derivation

❌ None.

Local call

✅ Native — actions are async functions; you import and call them directly with validation:

const result = await loginUser({ username, password });

Verdict

2/4 within RSC scope. The cleanest Server Action wrapper. RSC-RPC by design — REST is a non-goal.


zsa

Version: zsa@0.6.0. Source: zsa.vercel.app.

Similar shape to next-safe-action: RSC RPC (not REST), Zod-only validation, React-hook client, direct call works (validated), no LLM tool derivation.

Verdict: 2/4 within RSC scope. Smaller and more focused than next-safe-action.


mcp-handler

Version: mcp-handler@1.1.0. Source: github.com/vercel/mcp-adapter .

This is not a framework — it’s Vercel’s MCP HTTP Streamable transport adapter for Next.js. You manually register tools against an McpServer instance from @modelcontextprotocol/sdk:

app/api/[transport]/route.ts
import { createMcpHandler } from "mcp-handler"; import { z } from "zod"; const handler = createMcpHandler( (server) => { server.registerTool( "roll_dice", { title: "Roll Dice", description: "Roll a dice with a specified number of sides.", inputSchema: { sides: z.number().int().min(2) }, }, async ({ sides }) => { const value = 1 + Math.floor(Math.random() * sides); return { content: [{ type: "text", text: `🎲 You rolled a ${value}!` }], }; }, ); }, {}, { basePath: "/api", maxDuration: 60, verboseLogs: true }, ); export { handler as GET, handler as POST };

Source: github.com/vercel/mcp-adapter 

Manual tool registration only. Does NOT auto-derive from existing route handlers, server actions, or any other endpoint surface. This is the transport layer Vovk’s deriveTools() output plugs into (and oRPC users would also wire into).


Blitz.js

Package: @blitzjs/rpc. Source: blitzjs.com .

@blitzjs/rpc resolvers are RPC-over-POST through a single /api/rpc/[[...blitz]] endpoint. Not REST. Build-time fetch rewrite gives type-safe imports on the client. Resolvers callable directly server-side (validation via resolver.pipe(resolver.zod(Schema), ...)).

Verdict: 1.5/4. Original “zero-API” RPC; structurally anti-REST.


Wasp

Package: wasp / wasp-lang. Source: wasp.sh .

Operations declared in main.wasp DSL produce typed RPC client + local-callable Node functions. For REST, you author a separate api declaration with explicit httpRoute — parallel surface, not auto-derived from operations.

Verdict: 2/4. DSL lock-in is the opposite tradeoff from “just TypeScript.”


Next.js raw Route Handlers

App Router file-based REST. Native to Next.js.

app/users/[id]/route.ts
export async function GET( request: Request, { params }: { params: Promise<{ id: string }> }, ) { const { id } = await params; return Response.json({ id, name: 'Alice' }); }

Source: nextjs.org/docs/app/api-reference/file-conventions/route 

No built-in validation, no typed RPC client, no LLM tool derivation, no validated local-call mechanism. Pure REST, nothing else from the SSOT rubric.

Verdict: 1/4. Pure REST. Everything else is BYO — and BYO at every cell is what Vovk and oRPC are built to absorb.


Tier C frameworks

REST endpoints work; little else from the SSOT rubric. Listed for completeness; not detailed individually because the verdict is the same: ~1/4, REST works, everything else is BYO or unsupported.

FrameworkOne-line summary
next-rest-frameworkAdds OpenAPI emission to Next.js Route Handlers. No typed client, no LLM tools.
next-zod-route, typed-route-handler, next-openapi-route-handlerSmall Zod-validation wrappers around Route Handlers. No client, no AI, no local-call.
ConvexBaaS with TypeScript backend functions and its own runtime. Mounts on Next.js via convex/nextjs helpers, not as a library. Convex MCP exists but is dev-tooling (queries the data layer), not API-as-tool. ~1/4 on this rubric.
H3 + Nitro (UnJS)H3 is the HTTP toolkit, Nitro is the server engine; both deploy anywhere (Node/Bun/CF/Vercel/Deno/Lambda) but Nitro IS a Next.js alternative server engine. Hono-class on REST; no first-party typed RPC client.
tsoaOpenAPI-from-TypeScript-controllers for Express/Koa/Hapi. No App Router adapter, no MCP, no first-party typed client. ~1.5/4. Active but Express-era stack.
ExpressVanilla. No type-flow, no SSOT. Custom-server-only on Next.js.
KoaSame shape as Express.
routing-controllersNestJS-like decorators on Express. OpenAPI roundtrip only. Maintenance-mode.
LoopBack 4Heavy enterprise, OpenAPI-first. Own runtime, not Next.js compatible.
MoleculerMicroservices broker; moleculer-web exposes REST. Stringly-typed broker.call() is the local-call.
Sails.js / Marble.js / Total.jsNiche, mostly legacy.
ZodiosContract-first REST + Zod client. Unmaintained since 2023 — listed only to dismiss.
SvelteKit / SolidStart / Qwik City / Astro / Remix (React Router 7) / Nuxt / FreshAll Next.js alternatives, not libraries. Each has its own server-function story; none mounts on Next.js.
RedwoodJSGraphQL-first. REST via functions/; services local-callable but not auto-derived.
Inngest / Trigger.dev / Vercel Workflows / Temporal / Restate / DBOSDurable-execution / background-job platforms — adjacent category, not HTTP backend frameworks. Out of scope for this rubric; mentioned to disambiguate.

What the comparison reveals

1. Only four TypeScript frameworks hit 4/4 SSOT

Vovk.ts, Nestia, oRPC, Igniter.js. Of these, only Vovk, oRPC, and Igniter.js mount on Next.js App Router; Nestia is NestJS-only.

2. oRPC is the closest existential competitor

Same SSOT thesis, comparable maturity (1.14.x). Concrete differences a buyer can audit:

  • Multi-language client codegen. Vovk emits Python and Rust clients first-party (🧪 experimental); oRPC is TS-only — non-TS clients require an OpenAPI roundtrip through a third-party generator.
  • Schema artifacts on disk. Vovk persists .vovk-schema/*.json build artifacts that CI tools and AI agents can consume offline; oRPC types live only in TypeScript inference.
  • Per-segment serverless function config. Each Vovk segment compiles to its own Next.js catch-all → its own Vercel function with independent maxDuration and runtime; oRPC mounts as a single handler regardless of how the router is structured.
  • First-class MCP formatter in deriveTools. Vovk’s ToModelOutput.MCP shapes execute-output for MCP transports; oRPC’s @orpc/ai-sdk produces ai-sdk tool definitions but routes MCP through @modelcontextprotocol/sdk directly — orpc.dev/docs/integrations/ai-sdk .
  • Streaming: parity. Vovk’s JSON Lines and oRPC’s Event Iterator  are roughly equivalent first-class streaming primitives — neither leads here.
  • Typed errors: oRPC ahead. oRPC has explicit .errors() declarations that flow to the client. Vovk’s error story (HttpException, client rethrow) is more implicit. This is one of the few axes where oRPC has a clean lead.
  • Ergonomics: a question of taste. Decorator/controller (Vovk) vs router-builder (oRPC). Vovk targets Next.js App Router exclusively; oRPC mounts on Next.js, Hono, Fastify, Express, Lambda via separate adapters.

3. Igniter.js is the only TS framework packaging its own MCP server transport

@igniter-js/adapter-mcp-server builds the actual MCP server end-to-end. Strictly more first-party than Vovk on this single sub-feature. Counterweights: pre-1.0 maturity (0.3.x), thin ecosystem, less battle-tested. Source: github.com/felipebarcelospro/igniter-js/tree/main/packages/adapter-mcp-server .

4. tRPC’s REST story is structurally weaker than perception suggests

Vanilla tRPC v11 is non-REST-semantic. REST requires per-procedure .meta({ openapi }) opt-in via trpc-to-openapi (community fork) or @trpc/openapi@11.17-alpha. tRPC and Blitz remain RPC-first by design — that’s a feature, not a bug, but it’s not REST. Sources: trpc.io/docs/server/adapters/nextjs , npmjs.com/package/trpc-to-openapi .

5. True validated local-call bypass is genuinely rare

Most frameworks’ “test without HTTP” stories — Hono app.request, Fastify inject, Elysia app.handle, H3 app.fetch — are HTTP simulation: they construct a Request and run middleware. True validated bypasses: Vovk .fn(), oRPC .callable() / call() / createRouterClient, tRPC createCaller, Nestia (in-process via typia). That’s the entire list. Sources: vovk.dev/fn , orpc.dev/docs/client/server-side , trpc.io/docs/server/server-side-calls .

6. MCP-from-routes auto-derivation is the rarest column

First-party native: Vovk (deriveTools), Nestia (typia.llm + @agentica), Igniter.js (adapter-mcp-server). Plugin via package: oRPC (@orpc/ai-sdk), Elysia (@8monkey/elysia-mcp). Everywhere else — Hono, Fastify, NestJS vanilla, AdonisJS, Express — you’re hand-writing tools against @modelcontextprotocol/sdk or chaining openapi-mcp-generator. Sources: vovk.dev/tools , orpc.dev/docs/integrations/ai-sdk , npmjs.com/package/@8monkey/elysia-mcp .

7. “AI-native” branding doesn’t always mean “API-as-tool”

Encore’s encore mcp start, Nuxt’s nuxt-mcp, and Next.js’s devtools MCP are all dev-tooling MCPs — they expose framework state to coding agents during development. None converts production endpoints into runtime LLM tools. Read the branding carefully. Source: encore.dev/docs/ts/cli/mcp .

8. WebMCP is a parallel client-side dimension

This article’s MCP analysis is server-side: a procedure becomes a tool an agent calls over HTTP/stdio. WebMCP  — a W3C Draft Community Group Report (2026-02-10) , shipping behind a feature flag in Chrome 146 stable (since 2026-03-10) and expected in Edge within 1–2 Chromium releases — flips that: the browser exposes tools via navigator.modelContext, so an agent inherits the user’s authenticated session and DOM context rather than calling the backend.

The spec is still moving: provideContext() was removed on 2026-03-05 and unregisterTool(name) was removed on 2026-04-23 in favor of AbortSignal-driven unregistration. The polyfill keeps both functional with deprecation warnings, but anyone integrating now should expect more API churn before the W3C draft stabilizes.

The npm ecosystem already includes @mcp-b/webmcp-polyfill and @mcp-b/webmcp-types (polyfill + TS types for the spec), @webmcp-auto-ui/core (polyfill + Streamable HTTP client), @mcp-b/webmcp-local-relay (iframe/localhost bridge), auto-webmcp (form-to-tool wrapper), and a third-party orpc-webmcp demo that introspects oRPC routers and registers procedures as browser tools.

This doesn’t replace server-side MCP — agents still need server tools for anything stateful that lives outside the browser tab — but it changes what “AI-tool derivation from a procedure” can mean. None of the four 4/4 SSOT frameworks ships a first-party WebMCP bridge yet; for procedure-as-SSOT frameworks, this is the next plausible derivation target.

9. Standard Schema is the emerging consensus

Frameworks that accept any Standard Schema implementation (Vovk, oRPC, tRPC, ts-rest, Hono via validator packages, TanStack Start, next-safe-action) have the lightest validation lock-in. NestJS/Nestia (class-validator/typia), Encore (TS types), Effect (Effect Schema), Fastify (JSON Schema), AdonisJS (VineJS) each pin to one library — sometimes for good reasons (typia’s compile-time performance, Effect’s runtime integration), but it’s a coupling. Source: standardschema.dev .


Methodology

This comparison is authored by the maintainer of Vovk.ts . The six-axis rubric was fixed before per-framework analysis began, and every claim about every other framework is sourced inline against that framework’s official docs, npm, or package README. If you find a Vovk-vs-competitor claim that doesn’t hold against the linked source, the article is wrong — please open an issue.

Column rubric:

  • ✅ Native — feature is built-in to the framework’s core
  • 🔌 Plugin — feature available via first-party or commonly-used add-on package
  • ⚠️ — works with caveats (HTTP simulation, missing validation, split surface)
  • — not available

Last updated on