Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions docs/start/framework/react/guide/server-functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,55 @@ Server functions can return Server Components - server-rendered React components

Handle request cancellation with `AbortSignal` for long-running operations.

### Manual function IDs

TanStack Start gives every server function an internal ID. By default, that ID is generated from the source file and extracted function name.

Add `.id()` when a function needs to keep the same identity across file moves or variable renames:

```tsx
import { createServerFn } from '@tanstack/react-start'

export const getUser = createServerFn({ method: 'GET' })
.id('get-user')
.handler(async () => {
return { id: '123' }
})
```

The value must be a static string literal before `.handler()`. It can contain letters, numbers, `_`, and `-`. Manual IDs must be unique. Duplicate manual IDs fail at compile time. If a generated or plugin-generated ID collides with one, the generated ID gets a suffix such as `_1`.

> [!WARNING]
> IDs are public. They can show up in browser network requests, logs, proxies, and analytics, so keep them short and non-sensitive. Do not include secrets, private file paths, tenant IDs, user IDs, or sensitive business data.

An ID only locates the function. It does not authenticate the caller, authorize the action, validate input, or provide CSRF protection. Put those checks in middleware or in the handler.

A per-function `.id()` takes precedence over `serverFns.generateFunctionId` for that server function.

#### Manual IDs with factories

Set the ID on the server function produced by the factory, not on the reusable factory itself:

```tsx
const authedServerFn = createServerFn().middleware([authMiddleware])

export const getUser = authedServerFn()
.id('get-user')
.handler(async () => {
return { id: '123' }
})

export const updateUser = authedServerFn({ method: 'POST' })
.id('update-user')
.handler(async () => {
return { ok: true }
})
```

Factory calls do not inherit IDs so each function produced by the factory should declare its own ID.

Keep the same `.id()` value to preserve function identity across file moves and variable renames. Keep the same `method` to preserve call compatibility.

### Function ID generation for production build

Server functions are addressed by a generated, stable function ID under the hood. These IDs are embedded into the client/SSR builds and used by the server to locate and import the correct module at runtime.
Expand Down
152 changes: 144 additions & 8 deletions packages/start-client-core/src/createServerFn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,13 @@ export const createServerFn: CreateServerFn<Register> = (options, __opts) => {

const res: ServerFnBuilder<Register, Method, ServerFnStrict> = {
options: resolvedOptions,
id: (id) => {
const newOptions = {
...resolvedOptions,
id,
}
return createServerFn(undefined, newOptions) as any
},
middleware: (middleware) => {
// multiple calls to `middleware()` merge the middlewares with the previously supplied ones
// this is primarily useful for letting users create their own abstractions on top of `createServerFn`
Expand Down Expand Up @@ -216,8 +223,10 @@ export const createServerFn: CreateServerFn<Register> = (options, __opts) => {
},
} as ServerFnBuilder<Register, Method, ServerFnStrict>
const fun = (options?: ServerFnOptions<Method, ServerFnStrict>) => {
const inheritedOptions = { ...resolvedOptions }
delete inheritedOptions.id
const newOptions = {
...resolvedOptions,
...inheritedOptions,
...options,
}
return createServerFn(undefined, newOptions)
Expand Down Expand Up @@ -500,6 +509,7 @@ export type ServerFnBaseOptions<
TInputValidator = unknown,
TStrict extends ServerFnStrict = true,
> = {
id?: string
method: TMethod
strict?: TStrict
middleware?: Constrain<
Expand Down Expand Up @@ -586,6 +596,135 @@ export type AppendMiddlewares<TMiddlewares, TNewMiddlewares> =
: TMiddlewares
: TNewMiddlewares

export interface ServerFnId<
TRegister,
TMethod extends Method,
TMiddlewares,
TInputValidator,
TStrict extends ServerFnStrict,
> {
id: (
id: string,
) => ServerFnAfterId<
TRegister,
TMethod,
TMiddlewares,
TInputValidator,
TStrict
>
}

export interface ServerFnAfterId<
TRegister,
TMethod extends Method,
TMiddlewares,
TInputValidator,
TStrict extends ServerFnStrict,
>
extends
ServerFnWithTypes<
TRegister,
TMethod,
TMiddlewares,
TInputValidator,
undefined,
TStrict
>,
ServerFnMiddlewareAfterId<
TRegister,
TMethod,
TMiddlewares,
TInputValidator,
TStrict
>,
ServerFnValidatorAfterId<TRegister, TMethod, TMiddlewares, TStrict>,
ServerFnHandler<
TRegister,
TMethod,
TMiddlewares,
TInputValidator,
TStrict
> {}

export interface ServerFnMiddlewareAfterId<
TRegister,
TMethod extends Method,
TMiddlewares,
TInputValidator,
TStrict extends ServerFnStrict,
> {
middleware: <const TNewMiddlewares>(
middlewares: Constrain<
TNewMiddlewares,
ReadonlyArray<AnyFunctionMiddleware | AnyRequestMiddleware | AnyServerFn>
>,
) => ServerFnAfterId<
TRegister,
TMethod,
AppendMiddlewares<TMiddlewares, TNewMiddlewares>,
TInputValidator,
TStrict
>
}

export type ValidatorFnAfterId<
TRegister,
TMethod extends Method,
TMiddlewares,
TStrict extends ServerFnStrict,
> = <TInputValidator>(
validator: ConstrainValidator<TRegister, TMethod, TInputValidator, TStrict>,
) => ServerFnAfterIdAfterValidator<
TRegister,
TMethod,
TMiddlewares,
TInputValidator,
TStrict
>

export interface ServerFnValidatorAfterId<
TRegister,
TMethod extends Method,
TMiddlewares,
TStrict extends ServerFnStrict,
> {
validator: ValidatorFnAfterId<TRegister, TMethod, TMiddlewares, TStrict>
// TODO remove upon stable
/** @deprecated Use `validator` instead. */
inputValidator: ValidatorFnAfterId<TRegister, TMethod, TMiddlewares, TStrict>
}

export interface ServerFnAfterIdAfterValidator<
TRegister,
TMethod extends Method,
TMiddlewares,
TInputValidator,
TStrict extends ServerFnStrict,
>
extends
ServerFnWithTypes<
TRegister,
TMethod,
TMiddlewares,
TInputValidator,
undefined,
TStrict
>,
ServerFnMiddlewareAfterId<
TRegister,
TMethod,
TMiddlewares,
TInputValidator,
TStrict
>,
ServerFnHandler<
TRegister,
TMethod,
TMiddlewares,
TInputValidator,
TStrict
> {}

export interface ServerFnMiddleware<
TRegister,
TMethod extends Method,
Expand Down Expand Up @@ -625,6 +764,7 @@ export interface ServerFnAfterMiddleware<
>,
ServerFnMiddleware<TRegister, TMethod, TMiddlewares, undefined, TStrict>,
ServerFnValidator<TRegister, TMethod, TMiddlewares, TStrict>,
ServerFnId<TRegister, TMethod, TMiddlewares, TInputValidator, TStrict>,
ServerFnHandler<
TRegister,
TMethod,
Expand Down Expand Up @@ -696,13 +836,8 @@ export interface ServerFnAfterValidator<
TInputValidator,
TStrict
>,
ServerFnHandler<
TRegister,
TMethod,
TMiddlewares,
TInputValidator,
TStrict
> {}
ServerFnHandler<TRegister, TMethod, TMiddlewares, TInputValidator, TStrict>,
ServerFnId<TRegister, TMethod, TMiddlewares, TInputValidator, TStrict> {}

export interface ServerFnAfterTyper<
TRegister,
Expand Down Expand Up @@ -764,6 +899,7 @@ export interface ServerFnBuilder<
>,
ServerFnMiddleware<TRegister, TMethod, undefined, undefined, TStrict>,
ServerFnValidator<TRegister, TMethod, undefined, TStrict>,
ServerFnId<TRegister, TMethod, undefined, undefined, TStrict>,
ServerFnHandler<TRegister, TMethod, undefined, undefined, TStrict> {
<
TNewMethod extends Method = TMethod,
Expand Down
62 changes: 62 additions & 0 deletions packages/start-client-core/src/tests/createServerFn.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,68 @@ test('createServerFn strict false factory preserves strictness', () => {
>()
})

test('createServerFn id can be set before handler', () => {
const builder = createServerFn().id('get-user')

expectTypeOf(builder).toHaveProperty('handler')
expectTypeOf(builder).toHaveProperty('middleware')
expectTypeOf(builder).toHaveProperty('validator')
expectTypeOf(builder).not.toHaveProperty('id')

const fn = builder.handler(() => ({}))

expectTypeOf(fn).not.toHaveProperty('id')

const builderAfterMiddleware = createServerFn()
.id('list-users')
.middleware([])

expectTypeOf(builderAfterMiddleware).toHaveProperty('handler')
expectTypeOf(builderAfterMiddleware).not.toHaveProperty('id')

const builderWithIdAfterMiddleware = createServerFn()
.middleware([])
.id('update-user')

expectTypeOf(builderWithIdAfterMiddleware).toHaveProperty('handler')
expectTypeOf(builderWithIdAfterMiddleware).not.toHaveProperty('id')

const builderWithIdAfterValidator = createServerFn()
.validator((input: { id: string }) => input)
.id('delete-user')

expectTypeOf(builderWithIdAfterValidator).toHaveProperty('handler')
expectTypeOf(builderWithIdAfterValidator).not.toHaveProperty('id')
})

test('createServerFn factory children set their own ids', () => {
const middleware = createMiddleware({ type: 'function' }).server(
({ next }) => {
return next({ context: { user: 'alice' } as const })
},
)

const createAuthedServerFn = createServerFn({ method: 'POST' }).middleware([
middleware,
])

const builder = createAuthedServerFn().id('save-user')

expectTypeOf(builder).toHaveProperty('handler')
expectTypeOf(builder).not.toHaveProperty('id')

builder.handler((options) => {
expectTypeOf(options).toEqualTypeOf<{
context: {
readonly user: 'alice'
}
data: undefined
method: 'POST'
serverFnMeta: ServerFnMeta
}>()
})
})

test('createServerFn strict input false can validate function', () => {
const fn = createServerFn({ strict: { input: false } })
.validator((input: { func: () => 'input' }) => ({
Expand Down
Loading
Loading