Server-Funktionen ermöglichen es Ihnen, Logik zu definieren, die fast überall (sogar auf dem Client) aufgerufen werden kann, aber **nur** auf dem Server ausgeführt wird. Tatsächlich unterscheiden sie sich nicht wesentlich von einer API-Route, aber mit einigen wichtigen Unterschieden:
Sie ähneln jedoch regulären API-Routen insofern, als dass
Wie unterscheiden sich Server-Funktionen von "Solid Server Functions"?
- TanStack Server-Funktionen sind nicht an ein bestimmtes Frontend-Framework gebunden und können mit jedem Frontend-Framework oder auch ohne eines verwendet werden.
- TanStack Server-Funktionen basieren auf Standard-HTTP-Anfragen und können beliebig oft aufgerufen werden, ohne von seriellen Ausführung-Engpässen betroffen zu sein.
Server-Funktionen können überall in Ihrer Anwendung definiert werden, müssen aber auf der obersten Ebene einer Datei definiert werden. Sie können in Ihrer gesamten Anwendung aufgerufen werden, einschließlich Loadern, Hooks usw. Traditionell ist dieses Muster als Remote Procedure Call (RPC) bekannt, aber aufgrund der isomeren Natur dieser Funktionen bezeichnen wir sie als Server-Funktionen.
Server-Funktionen können Middleware verwenden, um Logik, Kontext, gängige Operationen, Voraussetzungen und vieles mehr zu teilen. Um mehr über die Middleware von Server-Funktionen zu erfahren, lesen Sie bitte die Informationen dazu im Middleware-Leitfaden.
Wir möchten dem tRPC-Team für die Inspiration des Server-Funktionsdesigns von TanStack Start und die Anleitung bei der Implementierung danken. Wir lieben (und empfehlen) die Verwendung von tRPC für API-Routen so sehr, dass wir darauf bestanden haben, dass Server-Funktionen die gleiche erstklassige Behandlung und Entwicklererfahrung erhalten. Vielen Dank!
Server-Funktionen werden mit der Funktion createServerFn aus dem Paket @tanstack/solid-start definiert. Diese Funktion nimmt ein optionales options-Argument entgegen, um Konfigurationen wie die HTTP-Methode und den Antworttyp festzulegen, und ermöglicht es Ihnen, vom Ergebnis abgeleitet Dinge wie den Body der Server-Funktion, Eingabevalidierung, Middleware usw. zu definieren. Hier ist ein einfaches Beispiel:
// getServerTime.ts
import { createServerFn } from '@tanstack/solid-start'
export const getServerTime = createServerFn().handler(async () => {
// Wait for 1 second
await new Promise((resolve) => setTimeout(resolve, 1000))
// Return the current time
return new Date().toISOString()
})
// getServerTime.ts
import { createServerFn } from '@tanstack/solid-start'
export const getServerTime = createServerFn().handler(async () => {
// Wait for 1 second
await new Promise((resolve) => setTimeout(resolve, 1000))
// Return the current time
return new Date().toISOString()
})
Beim Erstellen einer Server-Funktion können Sie Konfigurationsoptionen angeben, um ihr Verhalten anzupassen.
import { createServerFn } from '@tanstack/solid-start'
export const getData = createServerFn({
method: 'GET', // HTTP method to use
response: 'data', // Response handling mode
}).handler(async () => {
// Function implementation
})
import { createServerFn } from '@tanstack/solid-start'
export const getData = createServerFn({
method: 'GET', // HTTP method to use
response: 'data', // Response handling mode
}).handler(async () => {
// Function implementation
})
method
Gibt die HTTP-Methode für die Server-Funktionsanfrage an.
method?: 'GET' | 'POST'
method?: 'GET' | 'POST'
Standardmäßig verwenden Server-Funktionen GET, wenn nichts anderes angegeben ist.
response
Steuert, wie Antworten verarbeitet und zurückgegeben werden.
response?: 'data' | 'full' | 'raw'
response?: 'data' | 'full' | 'raw'
Server-Funktionen akzeptieren einen einzelnen Parameter, der verschiedene Typen haben kann:
Hier ist ein Beispiel für eine Server-Funktion, die einen einfachen String-Parameter akzeptiert.
import { createServerFn } from '@tanstack/solid-start'
export const greet = createServerFn({
method: 'GET',
})
.validator((data: string) => data)
.handler(async (ctx) => {
return `Hello, ${ctx.data}!`
})
greet({
data: 'John',
})
import { createServerFn } from '@tanstack/solid-start'
export const greet = createServerFn({
method: 'GET',
})
.validator((data: string) => data)
.handler(async (ctx) => {
return `Hello, ${ctx.data}!`
})
greet({
data: 'John',
})
Server-Funktionen können so konfiguriert werden, dass sie ihre Eingabedaten zur Laufzeit validieren und gleichzeitig Typsicherheit hinzufügen. Dies ist nützlich, um sicherzustellen, dass die Eingabe den richtigen Typ hat, bevor die Server-Funktion ausgeführt wird, und um freundlichere Fehlermeldungen bereitzustellen.
Dies geschieht mit der Methode validator. Sie akzeptiert jede Eingabe, die an die Server-Funktion übergeben wird. Der Wert (und Typ), den Sie aus dieser Funktion zurückgeben, wird zur Eingabe, die an den eigentlichen Handler der Server-Funktion übergeben wird.
Validatoren integrieren sich auch nahtlos mit externen Validatoren, wenn Sie etwas wie Zod verwenden möchten.
Hier ist ein einfaches Beispiel für eine Server-Funktion, die den Eingabeparameter validiert.
import { createServerFn } from '@tanstack/solid-start'
type Person = {
name: string
}
export const greet = createServerFn({ method: 'GET' })
.validator((person: unknown): Person => {
if (typeof person !== 'object' || person === null) {
throw new Error('Person must be an object')
}
if ('name' in person && typeof person.name !== 'string') {
throw new Error('Person.name must be a string')
}
return person as Person
})
.handler(async ({ data }) => {
return `Hello, ${data.name}!`
})
import { createServerFn } from '@tanstack/solid-start'
type Person = {
name: string
}
export const greet = createServerFn({ method: 'GET' })
.validator((person: unknown): Person => {
if (typeof person !== 'object' || person === null) {
throw new Error('Person must be an object')
}
if ('name' in person && typeof person.name !== 'string') {
throw new Error('Person.name must be a string')
}
return person as Person
})
.handler(async ({ data }) => {
return `Hello, ${data.name}!`
})
Validierungsbibliotheken wie Zod können wie folgt verwendet werden:
import { createServerFn } from '@tanstack/solid-start'
import { z } from 'zod'
const Person = z.object({
name: z.string(),
})
export const greet = createServerFn({ method: 'GET' })
.validator((person: unknown) => {
return Person.parse(person)
})
.handler(async (ctx) => {
return `Hello, ${ctx.data.name}!`
})
greet({
data: {
name: 'John',
},
})
import { createServerFn } from '@tanstack/solid-start'
import { z } from 'zod'
const Person = z.object({
name: z.string(),
})
export const greet = createServerFn({ method: 'GET' })
.validator((person: unknown) => {
return Person.parse(person)
})
.handler(async (ctx) => {
return `Hello, ${ctx.data.name}!`
})
greet({
data: {
name: 'John',
},
})
Da Server-Funktionen die Netzwerkgrenze überschreiten, ist es wichtig sicherzustellen, dass die an sie übergebenen Daten nicht nur den richtigen Typ haben, sondern auch zur Laufzeit validiert werden. Dies ist besonders wichtig, wenn es um Benutzereingaben geht, da diese unvorhersehbar sein können. Um sicherzustellen, dass Entwickler ihre E/A-Daten validieren, sind Typen von der Validierung abhängig. Der Rückgabetyp der Funktion validator ist die Eingabe für den Handler der Server-Funktion.
import { createServerFn } from '@tanstack/solid-start'
type Person = {
name: string
}
export const greet = createServerFn({ method: 'GET' })
.validator((person: unknown): Person => {
if (typeof person !== 'object' || person === null) {
throw new Error('Person must be an object')
}
if ('name' in person && typeof person.name !== 'string') {
throw new Error('Person.name must be a string')
}
return person as Person
})
.handler(
async ({
data, // Person
}) => {
return `Hello, ${data.name}!`
},
)
function test() {
greet({ data: { name: 'John' } }) // OK
greet({ data: { name: 123 } }) // Error: Argument of type '{ name: number; }' is not assignable to parameter of type 'Person'.
}
import { createServerFn } from '@tanstack/solid-start'
type Person = {
name: string
}
export const greet = createServerFn({ method: 'GET' })
.validator((person: unknown): Person => {
if (typeof person !== 'object' || person === null) {
throw new Error('Person must be an object')
}
if ('name' in person && typeof person.name !== 'string') {
throw new Error('Person.name must be a string')
}
return person as Person
})
.handler(
async ({
data, // Person
}) => {
return `Hello, ${data.name}!`
},
)
function test() {
greet({ data: { name: 'John' } }) // OK
greet({ data: { name: 123 } }) // Error: Argument of type '{ name: number; }' is not assignable to parameter of type 'Person'.
}
Server-Funktionen leiten ihre Eingabe- und Ausgabetypen aus der Eingabe der Funktion validator und dem Rückgabewert der Funktion handler ab. Tatsächlich kann der von Ihnen definierte validator eigene separate Ein-/Ausgabetypen haben, was nützlich sein kann, wenn Ihr Validator Transformationen an den Eingabedaten durchführt.
Um dies zu veranschaulichen, werfen wir einen Blick auf ein Beispiel mit der Validierungsbibliothek zod.
import { createServerFn } from '@tanstack/solid-start'
import { z } from 'zod'
const transactionSchema = z.object({
amount: z.string().transform((val) => parseInt(val, 10)),
})
const createTransaction = createServerFn()
.validator(transactionSchema)
.handler(({ data }) => {
return data.amount // Returns a number
})
createTransaction({
data: {
amount: '123', // Accepts a string
},
})
import { createServerFn } from '@tanstack/solid-start'
import { z } from 'zod'
const transactionSchema = z.object({
amount: z.string().transform((val) => parseInt(val, 10)),
})
const createTransaction = createServerFn()
.validator(transactionSchema)
.handler(({ data }) => {
return data.amount // Returns a number
})
createTransaction({
data: {
amount: '123', // Accepts a string
},
})
Obwohl wir dringend empfehlen, eine Validierungsbibliothek zur Validierung Ihrer Netzwerk-E/A-Daten zu verwenden, möchten Sie vielleicht aus irgendeinem Grund Ihre Daten **nicht** validieren, aber trotzdem Typsicherheit haben. Um dies zu tun, stellen Sie Typinformationen für die Server-Funktion über eine Identitätsfunktion als validator bereit, die die Eingabe und/oder Ausgabe auf die korrekten Typen typisiert.
import { createServerFn } from '@tanstack/solid-start'
type Person = {
name: string
}
export const greet = createServerFn({ method: 'GET' })
.validator((d: Person) => d)
.handler(async (ctx) => {
return `Hello, ${ctx.data.name}!`
})
greet({
data: {
name: 'John',
},
})
import { createServerFn } from '@tanstack/solid-start'
type Person = {
name: string
}
export const greet = createServerFn({ method: 'GET' })
.validator((d: Person) => d)
.handler(async (ctx) => {
return `Hello, ${ctx.data.name}!`
})
greet({
data: {
name: 'John',
},
})
Server-Funktionen können JSON-serialisierbare Objekte als Parameter akzeptieren. Dies ist nützlich, um komplexe Datenstrukturen an den Server zu übergeben.
import { createServerFn } from '@tanstack/solid-start'
type Person = {
name: string
age: number
}
export const greet = createServerFn({ method: 'GET' })
.validator((data: Person) => data)
.handler(async ({ data }) => {
return `Hello, ${data.name}! You are ${data.age} years old.`
})
greet({
data: {
name: 'John',
age: 34,
},
})
import { createServerFn } from '@tanstack/solid-start'
type Person = {
name: string
age: number
}
export const greet = createServerFn({ method: 'GET' })
.validator((data: Person) => data)
.handler(async ({ data }) => {
return `Hello, ${data.name}! You are ${data.age} years old.`
})
greet({
data: {
name: 'John',
age: 34,
},
})
Server-Funktionen können FormData-Objekte als Parameter akzeptieren.
import { createServerFn } from '@tanstack/solid-start'
export const greetUser = createServerFn({ method: 'POST' })
.validator((data) => {
if (!(data instanceof FormData)) {
throw new Error('Invalid form data')
}
const name = data.get('name')
const age = data.get('age')
if (!name || !age) {
throw new Error('Name and age are required')
}
return {
name: name.toString(),
age: parseInt(age.toString(), 10),
}
})
.handler(async ({ data: { name, age } }) => {
return `Hello, ${name}! You are ${age} years old.`
})
// Usage
function Test() {
return (
<form
onSubmit={async (event) => {
event.preventDefault()
const formData = new FormData(event.currentTarget)
const response = await greetUser({ data: formData })
console.log(response)
}}
>
<input name="name" />
<input name="age" />
<button type="submit">Submit</button>
</form>
)
}
import { createServerFn } from '@tanstack/solid-start'
export const greetUser = createServerFn({ method: 'POST' })
.validator((data) => {
if (!(data instanceof FormData)) {
throw new Error('Invalid form data')
}
const name = data.get('name')
const age = data.get('age')
if (!name || !age) {
throw new Error('Name and age are required')
}
return {
name: name.toString(),
age: parseInt(age.toString(), 10),
}
})
.handler(async ({ data: { name, age } }) => {
return `Hello, ${name}! You are ${age} years old.`
})
// Usage
function Test() {
return (
<form
onSubmit={async (event) => {
event.preventDefault()
const formData = new FormData(event.currentTarget)
const response = await greetUser({ data: formData })
console.log(response)
}}
>
<input name="name" />
<input name="age" />
<button type="submit">Submit</button>
</form>
)
}
Zusätzlich zu dem einzelnen Parameter, den Server-Funktionen akzeptieren, können Sie auch auf den serverseitigen Request-Kontext innerhalb jeder Server-Funktion zugreifen, indem Sie Dienstprogramme von @tanstack/solid-start/server verwenden. Intern verwenden wir das h3-Paket von Unjs, um plattformübergreifende HTTP-Anfragen durchzuführen.
Es stehen viele Kontextfunktionen zur Verfügung, z. B. für:
Eine vollständige Liste der verfügbaren Kontextfunktionen finden Sie unter allen verfügbaren h3-Methoden oder inspizieren Sie den @tanstack/start-server-core Quellcode.
Hier sind zunächst einige Beispiele:
Verwenden wir die Funktion getWebRequest, um von innerhalb einer Server-Funktion auf die Anfrage selbst zuzugreifen.
import { createServerFn } from '@tanstack/solid-start'
import { getWebRequest } from '@tanstack/solid-start/server'
export const getServerTime = createServerFn({ method: 'GET' }).handler(
async () => {
const request = getWebRequest()
console.log(request.method) // GET
console.log(request.headers.get('User-Agent')) // Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3
},
)
import { createServerFn } from '@tanstack/solid-start'
import { getWebRequest } from '@tanstack/solid-start/server'
export const getServerTime = createServerFn({ method: 'GET' }).handler(
async () => {
const request = getWebRequest()
console.log(request.method) // GET
console.log(request.headers.get('User-Agent')) // Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3
},
)
Verwenden Sie die Funktion getHeaders, um von innerhalb einer Server-Funktion auf alle Header zuzugreifen.
import { createServerFn } from '@tanstack/solid-start'
import { getHeaders } from '@tanstack/solid-start/server'
export const getServerTime = createServerFn({ method: 'GET' }).handler(
async () => {
console.log(getHeaders())
// {
// "accept": "*/*",
// "accept-encoding": "gzip, deflate, br",
// "accept-language": "en-US,en;q=0.9",
// "connection": "keep-alive",
// "host": "localhost:3000",
// ...
// }
},
)
import { createServerFn } from '@tanstack/solid-start'
import { getHeaders } from '@tanstack/solid-start/server'
export const getServerTime = createServerFn({ method: 'GET' }).handler(
async () => {
console.log(getHeaders())
// {
// "accept": "*/*",
// "accept-encoding": "gzip, deflate, br",
// "accept-language": "en-US,en;q=0.9",
// "connection": "keep-alive",
// "host": "localhost:3000",
// ...
// }
},
)
Sie können auch einzelne Header mit der Funktion getHeader aufrufen.
import { createServerFn } from '@tanstack/solid-start'
import { getHeader } from '@tanstack/solid-start/server'
export const getServerTime = createServerFn({ method: 'GET' }).handler(
async () => {
console.log(getHeader('User-Agent')) // Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3
},
)
import { createServerFn } from '@tanstack/solid-start'
import { getHeader } from '@tanstack/solid-start/server'
export const getServerTime = createServerFn({ method: 'GET' }).handler(
async () => {
console.log(getHeader('User-Agent')) // Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3
},
)
Server-Funktionen können verschiedene Arten von Werten zurückgeben:
Um primitive oder JSON-serialisierbare Objekte zurückzugeben, geben Sie einfach den Wert aus der Server-Funktion zurück.
import { createServerFn } from '@tanstack/solid-start'
export const getServerTime = createServerFn({ method: 'GET' }).handler(
async () => {
return new Date().toISOString()
},
)
export const getServerData = createServerFn({ method: 'GET' }).handler(
async () => {
return {
message: 'Hello, World!',
}
},
)
import { createServerFn } from '@tanstack/solid-start'
export const getServerTime = createServerFn({ method: 'GET' }).handler(
async () => {
return new Date().toISOString()
},
)
export const getServerData = createServerFn({ method: 'GET' }).handler(
async () => {
return {
message: 'Hello, World!',
}
},
)
Standardmäßig gehen Server-Funktionen davon aus, dass jedes Nicht-Response-Objekt entweder ein primitiver oder ein JSON-serialisierbarer Wert ist.
Um mit benutzerdefinierten Headern zu antworten, können Sie die Funktion setHeader verwenden.
import { createServerFn } from '@tanstack/solid-start'
import { setHeader } from '@tanstack/solid-start/server'
export const getServerTime = createServerFn({ method: 'GET' }).handler(
async () => {
setHeader('X-Custom-Header', 'value')
return new Date().toISOString()
},
)
import { createServerFn } from '@tanstack/solid-start'
import { setHeader } from '@tanstack/solid-start/server'
export const getServerTime = createServerFn({ method: 'GET' }).handler(
async () => {
setHeader('X-Custom-Header', 'value')
return new Date().toISOString()
},
)
Um mit einem benutzerdefinierten Statuscode zu antworten, können Sie die Funktion setResponseStatus verwenden.
import { createServerFn } from '@tanstack/solid-start'
import { setResponseStatus } from '@tanstack/solid-start/server'
export const getServerTime = createServerFn({ method: 'GET' }).handler(
async () => {
setResponseStatus(201)
return new Date().toISOString()
},
)
import { createServerFn } from '@tanstack/solid-start'
import { setResponseStatus } from '@tanstack/solid-start/server'
export const getServerTime = createServerFn({ method: 'GET' }).handler(
async () => {
setResponseStatus(201)
return new Date().toISOString()
},
)
Um ein rohes Response-Objekt zurückzugeben, geben Sie ein Response-Objekt aus der Server-Funktion zurück und setzen Sie response: 'raw'.
import { createServerFn } from '@tanstack/solid-start'
export const getServerTime = createServerFn({
method: 'GET',
response: 'raw',
}).handler(async () => {
// Read a file from s3
return fetch('https://example.com/time.txt')
})
import { createServerFn } from '@tanstack/solid-start'
export const getServerTime = createServerFn({
method: 'GET',
response: 'raw',
}).handler(async () => {
// Read a file from s3
return fetch('https://example.com/time.txt')
})
Die Option `response: 'raw'` ermöglicht auch gestreamte Antworten und vieles mehr.
import { createServerFn } from '@tanstack/solid-start'
export const streamEvents = createServerFn({
method: 'GET',
response: 'raw',
}).handler(async ({ signal }) => {
// Create a ReadableStream to send chunks of data
const stream = new ReadableStream({
async start(controller) {
// Send initial response immediately
controller.enqueue(new TextEncoder().encode('Connection established\n'))
let count = 0
const interval = setInterval(() => {
// Check if the client disconnected
if (signal.aborted) {
clearInterval(interval)
controller.close()
return
}
// Send a data chunk
controller.enqueue(
new TextEncoder().encode(
`Event ${++count}: ${new Date().toISOString()}\n`,
),
)
// End after 10 events
if (count >= 10) {
clearInterval(interval)
controller.close()
}
}, 1000)
// Ensure we clean up if the request is aborted
signal.addEventListener('abort', () => {
clearInterval(interval)
controller.close()
})
},
})
// Return a streaming response
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
},
})
})
import { createServerFn } from '@tanstack/solid-start'
export const streamEvents = createServerFn({
method: 'GET',
response: 'raw',
}).handler(async ({ signal }) => {
// Create a ReadableStream to send chunks of data
const stream = new ReadableStream({
async start(controller) {
// Send initial response immediately
controller.enqueue(new TextEncoder().encode('Connection established\n'))
let count = 0
const interval = setInterval(() => {
// Check if the client disconnected
if (signal.aborted) {
clearInterval(interval)
controller.close()
return
}
// Send a data chunk
controller.enqueue(
new TextEncoder().encode(
`Event ${++count}: ${new Date().toISOString()}\n`,
),
)
// End after 10 events
if (count >= 10) {
clearInterval(interval)
controller.close()
}
}, 1000)
// Ensure we clean up if the request is aborted
signal.addEventListener('abort', () => {
clearInterval(interval)
controller.close()
})
},
})
// Return a streaming response
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
},
})
})
Die Option response: 'raw' ist besonders nützlich für:
Abgesehen von speziellen redirect- und notFound-Fehlern können Server-Funktionen beliebige benutzerdefinierte Fehler auslösen. Diese Fehler werden serialisiert und als JSON-Antwort mit einem 500er-Statuscode an den Client gesendet.
import { createServerFn } from '@tanstack/solid-start'
export const doStuff = createServerFn({ method: 'GET' }).handler(async () => {
throw new Error('Something went wrong!')
})
// Usage
function Test() {
try {
await doStuff()
} catch (error) {
console.error(error)
// {
// message: "Something went wrong!",
// stack: "Error: Something went wrong!\n at doStuff (file:///path/to/file.ts:3:3)"
// }
}
}
import { createServerFn } from '@tanstack/solid-start'
export const doStuff = createServerFn({ method: 'GET' }).handler(async () => {
throw new Error('Something went wrong!')
})
// Usage
function Test() {
try {
await doStuff()
} catch (error) {
console.error(error)
// {
// message: "Something went wrong!",
// stack: "Error: Something went wrong!\n at doStuff (file:///path/to/file.ts:3:3)"
// }
}
}
Auf dem Client können Server-Funktionsaufrufe über ein AbortSignal abgebrochen werden. Auf dem Server benachrichtigt ein AbortSignal, wenn die Anfrage vor Abschluss der Ausführung geschlossen wurde.
import { createServerFn } from '@tanstack/solid-start'
export const abortableServerFn = createServerFn().handler(
async ({ signal }) => {
return new Promise<string>((resolve, reject) => {
if (signal.aborted) {
return reject(new Error('Aborted before start'))
}
const timerId = setTimeout(() => {
console.log('server function finished')
resolve('server function result')
}, 1000)
const onAbort = () => {
clearTimeout(timerId)
console.log('server function aborted')
reject(new Error('Aborted'))
}
signal.addEventListener('abort', onAbort, { once: true })
})
},
)
// Usage
function Test() {
const controller = new AbortController()
const serverFnPromise = abortableServerFn({
signal: controller.signal,
})
await new Promise((resolve) => setTimeout(resolve, 500))
controller.abort()
try {
const serverFnResult = await serverFnPromise
console.log(serverFnResult) // should never get here
} catch (error) {
console.error(error) // "signal is aborted without reason"
}
}
import { createServerFn } from '@tanstack/solid-start'
export const abortableServerFn = createServerFn().handler(
async ({ signal }) => {
return new Promise<string>((resolve, reject) => {
if (signal.aborted) {
return reject(new Error('Aborted before start'))
}
const timerId = setTimeout(() => {
console.log('server function finished')
resolve('server function result')
}, 1000)
const onAbort = () => {
clearTimeout(timerId)
console.log('server function aborted')
reject(new Error('Aborted'))
}
signal.addEventListener('abort', onAbort, { once: true })
})
},
)
// Usage
function Test() {
const controller = new AbortController()
const serverFnPromise = abortableServerFn({
signal: controller.signal,
})
await new Promise((resolve) => setTimeout(resolve, 500))
controller.abort()
try {
const serverFnResult = await serverFnPromise
console.log(serverFnResult) // should never get here
} catch (error) {
console.error(error) // "signal is aborted without reason"
}
}
Server-Funktionen können normal aus Route-loaders, beforeLoads oder anderen Router-gesteuerten APIs aufgerufen werden. Diese APIs sind ausgestattet, um Fehler, Weiterleitungen und nicht gefundene Elemente, die von Server-Funktionen ausgelöst werden, automatisch zu verarbeiten.
import { getServerTime } from './getServerTime'
export const Route = createFileRoute('/time')({
loader: async () => {
const time = await getServerTime()
return {
time,
}
},
})
import { getServerTime } from './getServerTime'
export const Route = createFileRoute('/time')({
loader: async () => {
const time = await getServerTime()
return {
time,
}
},
})
Server-Funktionen können redirects oder notFounds auslösen, und obwohl nicht erforderlich, wird empfohlen, diese Fehler abzufangen und entsprechend zu behandeln. Um dies zu erleichtern, exportiert das Paket @tanstack/solid-start einen Hook useServerFn, der verwendet werden kann, um Server-Funktionen an Komponenten und Hooks zu binden.
import { useServerFn } from '@tanstack/solid-start'
import { useQuery } from '@tanstack/solid-query'
import { getServerTime } from './getServerTime'
export function Time() {
const getTime = useServerFn(getServerTime)
const timeQuery = useQuery({
queryKey: 'time',
queryFn: () => getTime(),
})
}
import { useServerFn } from '@tanstack/solid-start'
import { useQuery } from '@tanstack/solid-query'
import { getServerTime } from './getServerTime'
export function Time() {
const getTime = useServerFn(getServerTime)
const timeQuery = useQuery({
queryKey: 'time',
queryFn: () => getTime(),
})
}
Bei der Verwendung von Server-Funktionen beachten Sie, dass Weiterleitungen und nicht gefundene Elemente, die sie auslösen, nur automatisch behandelt werden, wenn sie von
verwendet werden. Für andere Verwendungspunkte müssen Sie diese Fälle manuell behandeln.
Server-Funktionen können einen redirect-Fehler auslösen, um den Benutzer zu einer anderen URL weiterzuleiten. Dies ist nützlich für die Behandlung von Authentifizierung, Autorisierung oder anderen Szenarien, in denen Sie den Benutzer auf eine andere Seite weiterleiten müssen.
Um eine Weiterleitung auszulösen, können Sie die Funktion redirect verwenden, die aus dem Paket @tanstack/solid-router exportiert wird.
import { redirect } from '@tanstack/solid-router'
import { createServerFn } from '@tanstack/solid-start'
export const doStuff = createServerFn({ method: 'GET' }).handler(async () => {
// Redirect the user to the home page
throw redirect({
to: '/',
})
})
import { redirect } from '@tanstack/solid-router'
import { createServerFn } from '@tanstack/solid-start'
export const doStuff = createServerFn({ method: 'GET' }).handler(async () => {
// Redirect the user to the home page
throw redirect({
to: '/',
})
})
Weiterleitungen können alle gleichen Optionen wie router.navigate, useNavigate() und <Link>-Komponenten nutzen. Sie können also auch gerne Folgendes übergeben:
Weiterleitungen können auch den Statuscode der Antwort festlegen, indem sie eine status-Option übergeben.
import { redirect } from '@tanstack/solid-router'
import { createServerFn } from '@tanstack/solid-start'
export const doStuff = createServerFn({ method: 'GET' }).handler(async () => {
// Redirect the user to the home page with a 301 status code
throw redirect({
to: '/',
status: 301,
})
})
import { redirect } from '@tanstack/solid-router'
import { createServerFn } from '@tanstack/solid-start'
export const doStuff = createServerFn({ method: 'GET' }).handler(async () => {
// Redirect the user to the home page with a 301 status code
throw redirect({
to: '/',
status: 301,
})
})
Sie können auch zu einem externen Ziel weiterleiten, indem Sie href verwenden.
import { redirect } from '@tanstack/solid-router'
import { createServerFn } from '@tanstack/solid-start'
export const auth = createServerFn({ method: 'GET' }).handler(async () => {
// Redirect the user to the auth provider
throw redirect({
href: 'https://authprovider.com/login',
})
})
import { redirect } from '@tanstack/solid-router'
import { createServerFn } from '@tanstack/solid-start'
export const auth = createServerFn({ method: 'GET' }).handler(async () => {
// Redirect the user to the auth provider
throw redirect({
href: 'https://authprovider.com/login',
})
})
⚠️ Verwenden Sie **nicht** die Funktion sendRedirect von @tanstack/solid-start/server, um Soft-Redirects aus Server-Funktionen zu senden. Dies sendet den Redirect über den Location-Header und erzwingt eine vollständige Seiten-Hard-Navigation auf dem Client.
Sie können auch benutzerdefinierte Header bei einer Weiterleitung festlegen, indem Sie eine headers-Option übergeben.
import { redirect } from '@tanstack/solid-router'
import { createServerFn } from '@tanstack/solid-start'
export const doStuff = createServerFn({ method: 'GET' }).handler(async () => {
// Redirect the user to the home page with a custom header
throw redirect({
to: '/',
headers: {
'X-Custom-Header': 'value',
},
})
})
import { redirect } from '@tanstack/solid-router'
import { createServerFn } from '@tanstack/solid-start'
export const doStuff = createServerFn({ method: 'GET' }).handler(async () => {
// Redirect the user to the home page with a custom header
throw redirect({
to: '/',
headers: {
'X-Custom-Header': 'value',
},
})
})
Beim Aufruf einer Server-Funktion aus einem Route-Lebenszyklus loader oder beforeLoad kann ein spezieller notFound-Fehler ausgelöst werden, um dem Router anzuzeigen, dass die angeforderte Ressource nicht gefunden wurde. Dies ist nützlicher als ein einfacher 404-Statuscode, da es Ihnen ermöglicht, eine benutzerdefinierte 404-Seite zu rendern oder den Fehler auf benutzerdefinierte Weise zu behandeln. Wenn `notFound` aus einer Server-Funktion außerhalb eines Route-Lebenszyklus ausgelöst wird, wird er nicht automatisch behandelt.
Um `notFound` auszulösen, können Sie die Funktion notFound verwenden, die aus dem Paket @tanstack/solid-router exportiert wird.
import { notFound } from '@tanstack/solid-router'
import { createServerFn } from '@tanstack/solid-start'
const getStuff = createServerFn({ method: 'GET' }).handler(async () => {
// Randomly return a not found error
if (Math.random() < 0.5) {
throw notFound()
}
// Or return some stuff
return {
stuff: 'stuff',
}
})
export const Route = createFileRoute('/stuff')({
loader: async () => {
const stuff = await getStuff()
return {
stuff,
}
},
})
import { notFound } from '@tanstack/solid-router'
import { createServerFn } from '@tanstack/solid-start'
const getStuff = createServerFn({ method: 'GET' }).handler(async () => {
// Randomly return a not found error
if (Math.random() < 0.5) {
throw notFound()
}
// Or return some stuff
return {
stuff: 'stuff',
}
})
export const Route = createFileRoute('/stuff')({
loader: async () => {
const stuff = await getStuff()
return {
stuff,
}
},
})
Not-Found-Fehler sind ein Kernmerkmal von TanStack Router.
Wenn eine Server-Funktion einen Fehler (nicht-redirect/nicht-notFound) auslöst, wird dieser serialisiert und als JSON-Antwort mit einem 500er-Statuscode an den Client gesendet. Dies ist für das Debugging nützlich, aber Sie möchten diese Fehler möglicherweise benutzerfreundlicher behandeln. Dies können Sie tun, indem Sie den Fehler abfangen und in Ihrem Route-Lebenszyklus, Ihrer Komponente oder Ihrem Hook behandeln, wie Sie es normalerweise tun würden.
import { createServerFn } from '@tanstack/solid-start'
export const doStuff = createServerFn({ method: 'GET' }).handler(async () => {
undefined.foo()
})
export const Route = createFileRoute('/stuff')({
loader: async () => {
try {
await doStuff()
} catch (error) {
// Handle the error:
// error === {
// message: "Cannot read property 'foo' of undefined",
// stack: "TypeError: Cannot read property 'foo' of undefined\n at doStuff (file:///path/to/file.ts:3:3)"
}
},
})
import { createServerFn } from '@tanstack/solid-start'
export const doStuff = createServerFn({ method: 'GET' }).handler(async () => {
undefined.foo()
})
export const Route = createFileRoute('/stuff')({
loader: async () => {
try {
await doStuff()
} catch (error) {
// Handle the error:
// error === {
// message: "Cannot read property 'foo' of undefined",
// stack: "TypeError: Cannot read property 'foo' of undefined\n at doStuff (file:///path/to/file.ts:3:3)"
}
},
})
Ohne aktivierte JavaScript gibt es nur eine Möglichkeit, Server-Funktionen auszuführen: durch das Absenden eines Formulars.
Dies geschieht durch Hinzufügen eines form-Elements zur Seite mit dem HTML-Attribut action.
Beachten Sie, dass wir das **HTML**-Attribut action erwähnt haben. Dieses Attribut akzeptiert in HTML nur einen String, wie alle anderen Attribute auch.
Zur Veranschaulichung: Während React 19 die Unterstützung für die Übergabe einer Funktion an action hinzugefügt hat, ist dies eine React-spezifische Funktion und kein Teil des HTML-Standards.
Das Attribut action gibt dem Browser an, wohin die Formulardaten gesendet werden sollen, wenn das Formular abgeschickt wird. In diesem Fall möchten wir die Formulardaten an die Server-Funktion senden.
Um dies zu tun, können wir die Eigenschaft url der Server-Funktion verwenden.
const yourFn = createServerFn({ method: 'POST' })
.validator((formData) => {
if (!(formData instanceof FormData)) {
throw new Error('Invalid form data')
}
const name = formData.get('name')
if (!name) {
throw new Error('Name is required')
}
return name
})
.handler(async ({ data: name }) => {
console.log(name) // 'John'
})
console.info(yourFn.url)
const yourFn = createServerFn({ method: 'POST' })
.validator((formData) => {
if (!(formData instanceof FormData)) {
throw new Error('Invalid form data')
}
const name = formData.get('name')
if (!name) {
throw new Error('Name is required')
}
return name
})
.handler(async ({ data: name }) => {
console.log(name) // 'John'
})
console.info(yourFn.url)
Und dies dem action-Attribut des Formulars übergeben.
function Component() {
return (
<form action={yourFn.url} method="POST">
<input name="name" defaultValue="John" />
<button type="submit">Click me!</button>
</form>
)
}
function Component() {
return (
<form action={yourFn.url} method="POST">
<input name="name" defaultValue="John" />
<button type="submit">Click me!</button>
</form>
)
}
Wenn das Formular abgeschickt wird, wird die Server-Funktion ausgeführt.
Um Argumente an eine Server-Funktion beim Absenden eines Formulars zu übergeben, können Sie das input-Element mit dem Attribut name verwenden, um das Argument an das FormData anzuhängen, das an Ihre Server-Funktion übergeben wird.
const yourFn = createServerFn({ method: 'POST' })
.validator((formData) => {
if (!(formData instanceof FormData)) {
throw new Error('Invalid form data')
}
const age = formData.get('age')
if (!age) {
throw new Error('age is required')
}
return age.toString()
})
.handler(async ({ data: formData }) => {
// `age` will be '123'
const age = formData.get('age')
// ...
})
function Component() {
return (
// We need to tell the server that our data type is `multipart/form-data` by setting the `encType` attribute on the form.
<form action={yourFn.url} method="POST" encType="multipart/form-data">
<input name="age" defaultValue="34" />
<button type="submit">Click me!</button>
</form>
)
}
const yourFn = createServerFn({ method: 'POST' })
.validator((formData) => {
if (!(formData instanceof FormData)) {
throw new Error('Invalid form data')
}
const age = formData.get('age')
if (!age) {
throw new Error('age is required')
}
return age.toString()
})
.handler(async ({ data: formData }) => {
// `age` will be '123'
const age = formData.get('age')
// ...
})
function Component() {
return (
// We need to tell the server that our data type is `multipart/form-data` by setting the `encType` attribute on the form.
<form action={yourFn.url} method="POST" encType="multipart/form-data">
<input name="age" defaultValue="34" />
<button type="submit">Click me!</button>
</form>
)
}
Wenn das Formular abgeschickt wird, wird die Server-Funktion mit den Formulardaten als Argument ausgeführt.
Unabhängig davon, ob JavaScript aktiviert ist oder nicht, gibt die Server-Funktion eine Antwort auf die vom Client ausgehende HTTP-Anfrage zurück.
Wenn JavaScript aktiviert ist, kann diese Antwort als Rückgabewert der Server-Funktion im JavaScript-Code des Clients abgerufen werden.
const yourFn = createServerFn().handler(async () => {
return 'Hello, world!'
})
// `.then` is not available when JavaScript is disabled
yourFn().then(console.log)
const yourFn = createServerFn().handler(async () => {
return 'Hello, world!'
})
// `.then` is not available when JavaScript is disabled
yourFn().then(console.log)
Wenn jedoch JavaScript deaktiviert ist, gibt es keine Möglichkeit, den Rückgabewert der Server-Funktion im JavaScript-Code des Clients abzurufen.
Stattdessen kann die Server-Funktion eine Antwort an den Client liefern, die den Browser anweist, auf eine bestimmte Weise zu navigieren.
In Kombination mit einem loader von TanStack Router können wir eine Erfahrung ähnlich einer Single-Page-Application bieten, auch wenn JavaScript deaktiviert ist; alles, indem wir dem Browser anweisen, die aktuelle Seite mit neuen Daten neu zu laden, die durch den loader geleitet werden.
import * as fs from 'fs'
import { createFileRoute } from '@tanstack/solid-router'
import { createServerFn } from '@tanstack/solid-start'
const filePath = 'count.txt'
async function readCount() {
return parseInt(
await fs.promises.readFile(filePath, 'utf-8').catch(() => '0'),
)
}
const getCount = createServerFn({
method: 'GET',
}).handler(() => {
return readCount()
})
const updateCount = createServerFn({ method: 'POST' })
.validator((formData) => {
if (!(formData instanceof FormData)) {
throw new Error('Invalid form data')
}
const addBy = formData.get('addBy')
if (!addBy) {
throw new Error('addBy is required')
}
return parseInt(addBy.toString())
})
.handler(async ({ data: addByAmount }) => {
const count = await readCount()
await fs.promises.writeFile(filePath, `${count + addByAmount}`)
// Reload the page to trigger the loader again
return new Response('ok', { status: 301, headers: { Location: '/' } })
})
export const Route = createFileRoute('/')({
component: Home,
loader: async () => await getCount(),
})
function Home() {
const state = Route.useLoaderData()
return (
<div>
<form
action={updateCount.url}
method="POST"
encType="multipart/form-data"
>
<input type="number" name="addBy" defaultValue="1" />
<button type="submit">Add</button>
</form>
<pre>{state()}</pre>
</div>
)
}
import * as fs from 'fs'
import { createFileRoute } from '@tanstack/solid-router'
import { createServerFn } from '@tanstack/solid-start'
const filePath = 'count.txt'
async function readCount() {
return parseInt(
await fs.promises.readFile(filePath, 'utf-8').catch(() => '0'),
)
}
const getCount = createServerFn({
method: 'GET',
}).handler(() => {
return readCount()
})
const updateCount = createServerFn({ method: 'POST' })
.validator((formData) => {
if (!(formData instanceof FormData)) {
throw new Error('Invalid form data')
}
const addBy = formData.get('addBy')
if (!addBy) {
throw new Error('addBy is required')
}
return parseInt(addBy.toString())
})
.handler(async ({ data: addByAmount }) => {
const count = await readCount()
await fs.promises.writeFile(filePath, `${count + addByAmount}`)
// Reload the page to trigger the loader again
return new Response('ok', { status: 301, headers: { Location: '/' } })
})
export const Route = createFileRoute('/')({
component: Home,
loader: async () => await getCount(),
})
function Home() {
const state = Route.useLoaderData()
return (
<div>
<form
action={updateCount.url}
method="POST"
encType="multipart/form-data"
>
<input type="number" name="addBy" defaultValue="1" />
<button type="submit">Add</button>
</form>
<pre>{state()}</pre>
</div>
)
}
Bei der Verwendung von Prerendering/Static-Generation können Server-Funktionen auch "statisch" sein, was es ermöglicht, ihre Ergebnisse zur Build-Zeit zu cachen und als statische Assets auszuliefern.
Erfahren Sie alles über dieses Muster auf der Seite Static Server Functions.
Unter der Haube werden Server-Funktionen aus dem Client-Bundle in ein separates Server-Bundle extrahiert. Auf dem Server werden sie wie sie sind ausgeführt, und das Ergebnis wird an den Client zurückgesendet. Auf dem Client leiten Server-Funktionen die Anfrage an den Server weiter, der die Funktion ausführt und das Ergebnis über fetch an den Client zurücksendet.
Der Prozess sieht folgendermaßen aus:
Ihre wöchentliche Dosis JavaScript-Nachrichten. Jeden Montag kostenlos an über 100.000 Entwickler geliefert.