Start: provide a supported way to unit-test server functions through the pipeline (inputValidator is skipped on direct calls)
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 15.1k
- Forks
- 1.9k
- Avg merge
- 1d 20h
- Merged PRs (30d)
- 143
Description
Which project does this relate to?
Start
Describe the problem
There's no supported way to unit-test that a server function's .inputValidator() runs. Calling the fn directly in a Node test (await fn({ data })) silently skips the validator and invokes the handler with raw, unvalidated data.
For apps that rely on .inputValidator(zodSchema) as their wire-level input guard (e.g. NoSQL‑injection hardening, where the schema coerces ids/queries to primitives before they reach the database), this means the exact security boundary we most want to regression-test is the one a unit test can't reach. A test that passes a Mongo operator object as an id "succeeds" (the handler runs against it) instead of being rejected — the opposite of what production does.
This came up while migrating a codebase to the idiomatic .inputValidator(Schema) form and wanting tests that guard against someone reverting a validator back to a type-only (data) => data passthrough.
Root cause
In createServerFn, calling the returned fn runs the middleware chain with env === "client":
// createServerFn.js
return Object.assign(async (opts) => {
const result = await executeMiddleware(resolvedMiddleware, "client", { ... });
...
});
but execValidator only runs on the server branch:
if ("inputValidator" in nextMiddleware.options && nextMiddleware.options.inputValidator && env === "server")
ctx.data = await execValidator(nextMiddleware.options.inputValidator, ctx.data);
So in a Node test there's no client/server transport — the handler runs, but the validator (the "server"-only step) is skipped. Confirmed empirically: a server fn called directly with an operator-object id reaches Model.findOne() with the raw object, whereas through the server pipeline the validator rejects it first.
Current workaround (and why it's not enough)
The only way to exercise the validator today is to drive the "server" path via the fn's internal __executeServer, inside a hand-built start context:
import { runWithStartContext } from '@tanstack/start-storage-context';
function callServerFn(fn, data) {
return runWithStartContext(
{
contextAfterGlobalMiddlewares: {},
request: new Request('http://localhost'),
executedRequestMiddlewares: new Set(),
},
() => fn.__executeServer({ data, context: {} }), // __-prefixed internal
);
}
// → resolves to { result, error }; a validator rejection surfaces as `error`
// and short-circuits the handler.
Problems with relying on this:
__executeServeris an undocumented__-prefixed internal that can change between releases.- The start-context shape is reverse-engineered from the source.
- Global function-middleware isn't applied (also noted by others in the discussion below).
Proposed solution
A supported, exported testing entry — e.g. createServerFnTestCaller() / callServerFn(fn, { data, context }) — that runs the full server pipeline (global middleware + inputValidator + handler) and returns { result, error }, so apps can assert both happy-path and validation-rejection behavior without reaching into internals.
References
- Discussion #2701 — "How to test Server Functions" (unresolved; multiple users describe this as a gap and converge on
runWithStartContext-based workarounds) - Related: #6428 — inputValidator validation errors lose structure during serialization
Platform
- Router / Start Version:
@tanstack/react-start1.167.x
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by reading createServerFn.js and tracing the client and server branches around execValidator, then compare the __executeServer workaround using runWithStartContext. Define a supported testing entry that exercises global middleware, inputValidator, and the handler, and verify that it returns { result, error } for both successful calls and validation rejection.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- api, backend, testing-qa
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100