---
id: mutations
track: learn
product: query
locale: en
order: 6
revision: 2
sourceCheckedOn: "2026-08-03"
versionRange: "Start ^1 / Query ^5"
verifiedAgainst: "@tanstack/react-start@1.168.34 + @tanstack/react-query@5.101.4"
contentModel: 2
prerequisites:
  - "Loader + Query lesson completed"
  - "Understand that server input is untrusted"
sourceRefs:
  - "https://tanstack.com/start/latest/docs/framework/react/guide/server-functions"
  - "https://tanstack.com/query/latest/docs/framework/react/guides/mutations"
  - "https://tanstack.com/query/latest/docs/framework/react/guides/updates-from-mutation-responses"
---

# Write back from a Server Function

> Validate input at the network boundary, model write state with a mutation, then immutably update Query cache from the server response.

**Outcome:** Implement a create flow with pending, error, and success states.

## Protect the server boundary first

Client types do not replace runtime validation. A POST Server Function uses validator before the handler to turn unknown data into trusted input; real apps should also authenticate and authorize here.

File: `src/features/issues/issues.ts`

```ts
export const createIssue = createServerFn({ method: 'POST' })
  .validator((input: { title: string }) => {
    const title = input.title.trim()
    if (title.length < 3) throw new Error('Title is too short')
    return { title }
  })
  .handler(async ({ data }) => saveIssue(data))
```

## Update the single source of truth from the response

useMutation owns the write lifecycle. On success, update cache from the complete server response; return a new array instead of pushing into the old cached value.

> **Note:** If the response cannot reconstruct the correct list, invalidate the query and let the server become authoritative again.

File: `src/routes/issues/index.tsx`

```tsx
const queryClient = useQueryClient()
const mutation = useMutation({
  mutationFn: (input: CreateIssueInput) =>
    createIssue({ data: input }),
  onSuccess: (issue) => {
    queryClient.setQueryData<Issue[]>(['issues'], (current = []) => [
      issue,
      ...current,
    ])
  },
})
```

## Practice and verification

### Completion checkpoint

List and detail caches agree after a successful write, while failed writes do not leave unconfirmed data visible.

### Exercise

After creating an issue, seed its detail cache and invalidate only the affected list prefix.

### Verification

Exercise success and failure; success should seed detail data, while failure should preserve the previous cache value.

### Common pitfalls

- Invalidating the entire query cache after every mutation.
- Assuming the server response is identical to the submitted input.

## Official sources

- https://tanstack.com/start/latest/docs/framework/react/guide/server-functions
- https://tanstack.com/query/latest/docs/framework/react/guides/mutations
- https://tanstack.com/query/latest/docs/framework/react/guides/updates-from-mutation-responses
