Skip to content

feat(trpc-server): add trpcFetchHandler preserving router context inference#1848

Open
hkirste wants to merge 3 commits intohonojs:mainfrom
hkirste:feat/trpc-server-fetch-handler
Open

feat(trpc-server): add trpcFetchHandler preserving router context inference#1848
hkirste wants to merge 3 commits intohonojs:mainfrom
hkirste:feat/trpc-server-fetch-handler

Conversation

@hkirste
Copy link
Copy Markdown

@hkirste hkirste commented Apr 9, 2026

trpcServer pins its options to AnyRouter and its createContext return type to Record<string, unknown>, so the router's typed context is erased at the boundary. Minimal repro:

import { initTRPC } from '@trpc/server'
import { Hono } from 'hono'
import { trpcServer } from '@hono/trpc-server'

type AppContext = { userId: string }
const t = initTRPC.context<AppContext>().create()

const appRouter = t.router({
  me: t.procedure.query(({ ctx }) => ctx.userId),
})

new Hono().use(
  '/trpc/*',
  trpcServer({
    router: appRouter,
    // typechecks, even though `userId` is missing:
    createContext: () => ({ somethingElse: 123 }),
  })
)

At runtime ctx.userId in me is undefined. TS has no way to flag it because createContext's return type is Record<string, unknown>, and the handler itself doesn't carry TRouter as a generic.

This PR adds a second export, trpcFetchHandler, a thin wrapper around fetchRequestHandler that keeps the router's context type all the way through to createContext. Required/optional semantics are inherited from tRPC's own CreateContextCallback<inferRouterContext<TRouter>, ...>, so createContext is required when the router uses a typed context and optional otherwise — same rule as fetchRequestHandler.

import { Hono } from 'hono'
import { trpcFetchHandler } from '@hono/trpc-server'
import { appRouter } from './router'

const app = new Hono()

app.use(
  '/trpc/*',
  trpcFetchHandler({
    router: appRouter,
    endpoint: '/trpc',
    createContext: (_opts, c) => ({
      userId: c.req.header('x-user-id') ?? '',
    }),
  })
)

A few things to flag:

  • trpcServer is unchanged. Same runtime behaviour (auto-merge of c.env, endpoint auto-detect from routePath), same callable shape, no @deprecated. Whether to eventually deprecate it is a maintainer call I don't want to pre-empt here.
  • trpcFetchHandler deliberately does not auto-merge c.env. Callers who want c.env in their tRPC context pass it through their own createContext, and that is exactly what lets the router's context type flow through. Auto-merging env is the reason trpcServer's return type had to be Record<string, unknown> in the first place.
  • The body-method Proxy workaround for HonoRequest#bodyCache (needed when an upstream middleware has already consumed c.req.json() and the raw stream is locked) is factored into a shared resolveRequest helper and used by both exports. No behavioural change for trpcServer.
  • Type contracts live in src/trpc-fetch-handler.test-d.ts using expectTypeOf, picked up by vitest's typecheck mode — same convention as packages/zod-openapi. I also added one runtime smoke test in src/trpc-fetch-handler.test.ts that threads a typed context through to a procedure.
  • Changeset is minor. This is an additive public export with no change to the existing type signature or runtime behaviour of trpcServer, so no existing caller should need to do anything.

The author should do the following, if applicable

  • Add tests
  • Run tests
  • yarn changeset at the top of this repo and push the changeset
  • Follow the contribution guide

…erence

Add trpcFetchHandler, a wrapper around fetchRequestHandler that keeps the
router's context type through to createContext. createContext is required
for routers with a typed context and optional otherwise, via the same
CreateContextCallback conditional type fetchRequestHandler itself uses.

trpcServer is unchanged: same runtime behaviour (auto-env merge, endpoint
auto-detect from routePath) and the same callable shape. The body-method
Proxy workaround for Hono's bodyCache is factored into a shared
resolveRequest helper used by both exports.

Type contracts for the new export live in src/trpc-fetch-handler.test-d.ts
and are picked up by vitest's typecheck mode, matching the convention used
in packages/zod-openapi.
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Apr 9, 2026

🦋 Changeset detected

Latest commit: 7f04b90

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@hono/trpc-server Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 9, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 91.92%. Comparing base (7ace4fd) to head (7f04b90).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1848      +/-   ##
==========================================
+ Coverage   91.72%   91.92%   +0.19%     
==========================================
  Files         113      113              
  Lines        3805     3812       +7     
  Branches      964      963       -1     
==========================================
+ Hits         3490     3504      +14     
+ Misses        283      277       -6     
+ Partials       32       31       -1     
Flag Coverage Δ
trpc-server 100.00% <100.00%> (+31.81%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

hkirste and others added 2 commits April 9, 2026 19:19
The POST request exercises the body-method Proxy branch in resolveRequest,
which is only reached for non-GET/HEAD methods.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant