TanStack / TanStack/router

Start: provide a supported way to unit-test server functions through the pipeline (inputValidator is skipped on direct calls)

Open
#7,507 0 comments 14 reactions 0 assignees View on GitHub

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:

  • __executeServer is 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-start 1.167.x

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.