Dieses Tutorial führt Sie durch den Aufbau einer vollständigen Full-Stack-Anwendung mit TanStack Start. Sie erstellen eine DevJokes-App, in der Benutzer entwicklerbezogene Witze anzeigen und hinzufügen können, und demonstrieren dabei Schlüsselkonzepte von TanStack Start, einschließlich Serverfunktionen, dateibasiertem Datenspeicher und React-Komponenten.
Hier ist eine Demo der App in Aktion
Der vollständige Code für dieses Tutorial ist auf GitHub verfügbar.
Lassen Sie uns zuerst ein neues TanStack Start-Projekt erstellen
pnpx create-start-app devjokes
cd devjokes
pnpx create-start-app devjokes
cd devjokes
Wenn dieses Skript ausgeführt wird, werden Ihnen einige Setup-Fragen gestellt. Sie können die Optionen auswählen, die für Sie passen, oder einfach Enter drücken, um die Standardeinstellungen zu übernehmen.
Optional können Sie ein --add-on Flag übergeben, um Optionen wie Shadcn, Clerk, Convex, TanStack Query usw. zu erhalten.
Nach Abschluss des Setups, installieren Sie die Abhängigkeiten und starten Sie den Entwicklungsserver
pnpm i
pnpm dev
pnpm i
pnpm dev
Für dieses Projekt benötigen wir ein paar zusätzliche Pakete
# Install uuid for generating unique IDs
pnpm add uuid
pnpm add -D @types/uuid
# Install uuid for generating unique IDs
pnpm add uuid
pnpm add -D @types/uuid
An diesem Punkt sollte die Projektstruktur wie folgt aussehen -
/devjokes
├── src/
│ ├── routes/
│ │ ├── __root.tsx # Root layout
│ │ ├── index.tsx # Home page
│ │ ├── demo.start.server-funcs.tsx # Demo server functions
│ │ └── demo.start.api-request.tsx # Demo API request
│ ├── api/ # API endpoints
│ ├── components/ # React components
│ ├── api.ts # API handler.
│ ├── client.tsx # Client entry point
│ ├── router.tsx # Router configuration
│ ├── routeTree.gen.ts # Generated route tree
│ ├── ssr.tsx # Server-side rendering
│ └── styles.css # Global styles
├── public/ # Static assets
├── app.config.ts # TanStack Start configuration
├── package.json # Project dependencies
└── tsconfig.json # TypeScript configuration
/devjokes
├── src/
│ ├── routes/
│ │ ├── __root.tsx # Root layout
│ │ ├── index.tsx # Home page
│ │ ├── demo.start.server-funcs.tsx # Demo server functions
│ │ └── demo.start.api-request.tsx # Demo API request
│ ├── api/ # API endpoints
│ ├── components/ # React components
│ ├── api.ts # API handler.
│ ├── client.tsx # Client entry point
│ ├── router.tsx # Router configuration
│ ├── routeTree.gen.ts # Generated route tree
│ ├── ssr.tsx # Server-side rendering
│ └── styles.css # Global styles
├── public/ # Static assets
├── app.config.ts # TanStack Start configuration
├── package.json # Project dependencies
└── tsconfig.json # TypeScript configuration
Diese Struktur mag auf den ersten Blick überwältigend erscheinen, aber hier sind die wichtigsten Dateien, auf die Sie sich konzentrieren müssen
Sobald Ihr Projekt eingerichtet ist, können Sie Ihre App unter localhost:3000 aufrufen. Sie sollten die Standard-TanStack Start-Willkommensseite sehen.
An diesem Punkt wird Ihre App wie folgt aussehen -

Beginnen wir damit, ein dateibasiertes Speichersystem für unsere Witze zu erstellen.
Lassen Sie uns eine Liste von Witzen einrichten, die wir auf der Seite darstellen können. Erstellen Sie ein data-Verzeichnis in Ihrem Projektstammverzeichnis und eine jokes.json-Datei darin
mkdir -p src/data
touch src/data/jokes.json
mkdir -p src/data
touch src/data/jokes.json
Fügen wir nun einige Beispielwitze in diese Datei ein
[
{
"id": "1",
"question": "Why don't keyboards sleep?",
"answer": "Because they have two shifts"
},
{
"id": "2",
"question": "Are you a RESTful API?",
"answer": "Because you GET my attention, PUT some love, POST the cutest smile, and DELETE my bad day"
},
{
"id": "3",
"question": "I used to know a joke about Java",
"answer": "But I ran out of memory."
},
{
"id": "4",
"question": "Why do Front-End Developers eat lunch alone?",
"answer": "Because, they don't know how to join tables."
},
{
"id": "5",
"question": "I am declaring a war.",
"answer": "var war;"
}
]
[
{
"id": "1",
"question": "Why don't keyboards sleep?",
"answer": "Because they have two shifts"
},
{
"id": "2",
"question": "Are you a RESTful API?",
"answer": "Because you GET my attention, PUT some love, POST the cutest smile, and DELETE my bad day"
},
{
"id": "3",
"question": "I used to know a joke about Java",
"answer": "But I ran out of memory."
},
{
"id": "4",
"question": "Why do Front-End Developers eat lunch alone?",
"answer": "Because, they don't know how to join tables."
},
{
"id": "5",
"question": "I am declaring a war.",
"answer": "var war;"
}
]
Lassen Sie uns eine Datei erstellen, um unsere Datentypen zu definieren. Erstellen Sie eine neue Datei unter src/types/index.ts
// src/types/index.ts
export interface Joke {
id: string
question: string
answer: string
}
export type JokesData = Joke[]
// src/types/index.ts
export interface Joke {
id: string
question: string
answer: string
}
export type JokesData = Joke[]
Lassen Sie uns eine neue Datei src/serverActions/jokesActions.ts erstellen, um eine Serverfunktion für Lese-Schreib-Vorgänge zu erstellen. Wir werden eine Serverfunktion mit createServerFn erstellen.
// src/serverActions/jokesActions.ts
import { createServerFn } from '@tanstack/react-start'
import * as fs from 'node:fs'
import type { JokesData } from '../types'
const JOKES_FILE = 'src/data/jokes.json'
export const getJokes = createServerFn({ method: 'GET' }).handler(async () => {
const jokes = await fs.promises.readFile(JOKES_FILE, 'utf-8')
return JSON.parse(jokes) as JokesData
})
// src/serverActions/jokesActions.ts
import { createServerFn } from '@tanstack/react-start'
import * as fs from 'node:fs'
import type { JokesData } from '../types'
const JOKES_FILE = 'src/data/jokes.json'
export const getJokes = createServerFn({ method: 'GET' }).handler(async () => {
const jokes = await fs.promises.readFile(JOKES_FILE, 'utf-8')
return JSON.parse(jokes) as JokesData
})
In diesem Code verwenden wir createServerFn, um eine Serverfunktion zu erstellen, die Witze aus der JSON-Datei liest. Die Funktion handler ist dort, wo wir das fs-Modul verwenden, um die Datei zu lesen.
Um diese Serverfunktion zu nutzen, können wir sie einfach in unserem Code aufrufen, indem wir TanStack Router verwenden, der bereits mit TanStack Start geliefert wird!
Lassen Sie uns nun eine neue Komponente JokesList erstellen, um die Witze auf der Seite mit einem Hauch von Tailwind-Styling darzustellen.
// src/components/JokesList.tsx
import { Joke } from '../types'
interface JokesListProps {
jokes: Joke[]
}
export function JokesList({ jokes }: JokesListProps) {
if (!jokes || jokes.length === 0) {
return <p className="text-gray-500 italic">No jokes found. Add some!</p>
}
return (
<div className="space-y-4">
<h2 className="text-xl font-semibold">Jokes Collection</h2>
{jokes.map((joke) => (
<div
key={joke.id}
className="bg-white p-4 rounded-lg shadow-md border border-gray-200"
>
<p className="font-bold text-lg mb-2">{joke.question}</p>
<p className="text-gray-700">{joke.answer}</p>
</div>
))}
</div>
)
}
// src/components/JokesList.tsx
import { Joke } from '../types'
interface JokesListProps {
jokes: Joke[]
}
export function JokesList({ jokes }: JokesListProps) {
if (!jokes || jokes.length === 0) {
return <p className="text-gray-500 italic">No jokes found. Add some!</p>
}
return (
<div className="space-y-4">
<h2 className="text-xl font-semibold">Jokes Collection</h2>
{jokes.map((joke) => (
<div
key={joke.id}
className="bg-white p-4 rounded-lg shadow-md border border-gray-200"
>
<p className="font-bold text-lg mb-2">{joke.question}</p>
<p className="text-gray-700">{joke.answer}</p>
</div>
))}
</div>
)
}
Rufen wir nun unsere Serverfunktion in App.jsx mit TanStack Router auf, der bereits mit TanStack Start geliefert wird!
// App.jsx
import { createFileRoute } from '@tanstack/react-router'
import { getJokes } from './serverActions/jokesActions'
import { JokesList } from './JokesList'
export const Route = createFileRoute('/')({
loader: async () => {
// Load jokes data when the route is accessed
return getJokes()
},
component: App,
})
const App = () => {
const jokes = Route.useLoaderData() || []
return (
<div className="p-4 flex flex-col">
<h1 className="text-2xl">DevJokes</h1>
<JokesList jokes={jokes} />
</div>
)
}
// App.jsx
import { createFileRoute } from '@tanstack/react-router'
import { getJokes } from './serverActions/jokesActions'
import { JokesList } from './JokesList'
export const Route = createFileRoute('/')({
loader: async () => {
// Load jokes data when the route is accessed
return getJokes()
},
component: App,
})
const App = () => {
const jokes = Route.useLoaderData() || []
return (
<div className="p-4 flex flex-col">
<h1 className="text-2xl">DevJokes</h1>
<JokesList jokes={jokes} />
</div>
)
}
Wenn die Seite geladen wird, enthält jokes bereits Daten aus der jokes.json-Datei!
Mit etwas Tailwind-Styling sollte die App wie folgt aussehen

Bisher konnten wir erfolgreich aus der Datei lesen! Wir können denselben Ansatz verwenden, um in die jokes.json-Datei zu schreiben, indem wir createServerFunction verwenden.
Es ist an der Zeit, die jokes.json-Datei zu ändern, damit wir neue Witze hinzufügen können. Lassen Sie uns eine weitere Serverfunktion erstellen, diesmal jedoch mit einer POST-Methode, um in dieselbe Datei zu schreiben.
// src/serverActions/jokesActions.ts
import { createServerFn } from '@tanstack/react-start'
import * as fs from 'node:fs'
import { v4 as uuidv4 } from 'uuid' // Add this import
import type { Joke, JokesData } from '../types'
export const addJoke = createServerFn({ method: 'POST' })
.validator((data: { question: string; answer: string }) => {
// Validate input data
if (!data.question || !data.question.trim()) {
throw new Error('Joke question is required')
}
if (!data.answer || !data.answer.trim()) {
throw new Error('Joke answer is required')
}
return data
})
.handler(async ({ data }) => {
try {
// Read the existing jokes from the file
const jokesData = await getJokes()
// Create a new joke with a unique ID
const newJoke: Joke = {
id: uuidv4(),
question: data.question,
answer: data.answer,
}
// Add the new joke to the list
const updatedJokes = [...jokesData, newJoke]
// Write the updated jokes back to the file
await fs.promises.writeFile(
JOKES_FILE,
JSON.stringify(updatedJokes, null, 2),
'utf-8',
)
return newJoke
} catch (error) {
console.error('Failed to add joke:', error)
throw new Error('Failed to add joke')
}
})
// src/serverActions/jokesActions.ts
import { createServerFn } from '@tanstack/react-start'
import * as fs from 'node:fs'
import { v4 as uuidv4 } from 'uuid' // Add this import
import type { Joke, JokesData } from '../types'
export const addJoke = createServerFn({ method: 'POST' })
.validator((data: { question: string; answer: string }) => {
// Validate input data
if (!data.question || !data.question.trim()) {
throw new Error('Joke question is required')
}
if (!data.answer || !data.answer.trim()) {
throw new Error('Joke answer is required')
}
return data
})
.handler(async ({ data }) => {
try {
// Read the existing jokes from the file
const jokesData = await getJokes()
// Create a new joke with a unique ID
const newJoke: Joke = {
id: uuidv4(),
question: data.question,
answer: data.answer,
}
// Add the new joke to the list
const updatedJokes = [...jokesData, newJoke]
// Write the updated jokes back to the file
await fs.promises.writeFile(
JOKES_FILE,
JSON.stringify(updatedJokes, null, 2),
'utf-8',
)
return newJoke
} catch (error) {
console.error('Failed to add joke:', error)
throw new Error('Failed to add joke')
}
})
In diesem Code
Ändern wir nun unsere Startseite, um Witze anzuzeigen und ein Formular zum Hinzufügen neuer Witze bereitzustellen. Erstellen wir eine neue Komponente namens JokeForm.jsx und fügen Sie das folgende Formular hinzu
// src/components/JokeForm.tsx
import { useState } from 'react'
import { useRouter } from '@tanstack/react-router'
import { addJoke } from '../serverActions/jokesActions'
export function JokeForm() {
const router = useRouter()
const [question, setQuestion] = useState('')
const [answer, setAnswer] = useState('')
const [isSubmitting, setIsSubmitting] = useState(false)
const [error, setError] = useState<string | null>(null)
return (
<form onSubmit={handleSubmit} className="flex flex-row gap-2 mb-6">
{error && (
<div className="bg-red-100 text-red-700 p-2 rounded mb-4">{error}</div>
)}
<div className="flex flex-col sm:flex-row gap-4 mb-8">
<input
id="question"
type="text"
placeholder="Enter joke question"
className="w-full p-2 border rounded focus:ring focus:ring-blue-300 flex-1"
value={question}
onChange={(e) => setQuestion(e.target.value)}
required
/>
<input
id="answer"
type="text"
placeholder="Enter joke answer"
className="w-full p-2 border rounded focus:ring focus:ring-blue-300 flex-1 py-4"
value={answer}
onChange={(e) => setAnswer(e.target.value)}
required
/>
<button
type="submit"
disabled={isSubmitting}
className="bg-blue-500 hover:bg-blue-600 text-white font-medium rounded disabled:opacity-50 px-4"
>
{isSubmitting ? 'Adding...' : 'Add Joke'}
</button>
</div>
</form>
)
}
// src/components/JokeForm.tsx
import { useState } from 'react'
import { useRouter } from '@tanstack/react-router'
import { addJoke } from '../serverActions/jokesActions'
export function JokeForm() {
const router = useRouter()
const [question, setQuestion] = useState('')
const [answer, setAnswer] = useState('')
const [isSubmitting, setIsSubmitting] = useState(false)
const [error, setError] = useState<string | null>(null)
return (
<form onSubmit={handleSubmit} className="flex flex-row gap-2 mb-6">
{error && (
<div className="bg-red-100 text-red-700 p-2 rounded mb-4">{error}</div>
)}
<div className="flex flex-col sm:flex-row gap-4 mb-8">
<input
id="question"
type="text"
placeholder="Enter joke question"
className="w-full p-2 border rounded focus:ring focus:ring-blue-300 flex-1"
value={question}
onChange={(e) => setQuestion(e.target.value)}
required
/>
<input
id="answer"
type="text"
placeholder="Enter joke answer"
className="w-full p-2 border rounded focus:ring focus:ring-blue-300 flex-1 py-4"
value={answer}
onChange={(e) => setAnswer(e.target.value)}
required
/>
<button
type="submit"
disabled={isSubmitting}
className="bg-blue-500 hover:bg-blue-600 text-white font-medium rounded disabled:opacity-50 px-4"
>
{isSubmitting ? 'Adding...' : 'Add Joke'}
</button>
</div>
</form>
)
}
Verbinden wir nun das Formular mit unserer addJoke Serverfunktion in der Funktion handleSubmit. Das Aufrufen einer Serveraktion ist einfach! Es ist nur ein Funktionsaufruf.
//JokeForm.tsx
import { useState } from 'react'
import { addJoke } from '../serverActions/jokesActions'
import { useRouter } from '@tanstack/react-router'
export function JokeForm() {
const router = useRouter()
const [question, setQuestion] = useState('')
const [answer, setAnswer] = useState('')
const [isSubmitting, setIsSubmitting] = useState(false)
const [error, setError] = useState<string | null>(null)
const handleSubmit = async () => {
if (!question || !answer || isSubmitting) return
try {
setIsSubmitting(true)
await addJoke({
data: { question, answer },
})
// Clear form
setQuestion('')
setAnswer('')
// Refresh data
router.invalidate()
} catch (error) {
console.error('Failed to add joke:', error)
setError('Failed to add joke')
} finally {
setIsSubmitting(false)
}
}
return (
<form onSubmit={handleSubmit} className="flex flex-row gap-2 mb-6">
{error && (
<div className="bg-red-100 text-red-700 p-2 rounded mb-4">{error}</div>
)}
<input
type="text"
name="question"
placeholder="Question"
className="p-1 border rounded w-full"
required
onChange={(e) => setQuestion(e.target.value)}
value={question}
/>
<input
type="text"
name="answer"
placeholder="Answer"
className="p-1 border rounded w-full"
required
onChange={(e) => setAnswer(e.target.value)}
value={answer}
/>
<button
className="bg-blue-500 text-white p-1 rounded hover:bg-blue-600"
disabled={isSubmitting}
>
{isSubmitting ? 'Adding...' : 'Add Joke'}
</button>
</form>
)
}
//JokeForm.tsx
import { useState } from 'react'
import { addJoke } from '../serverActions/jokesActions'
import { useRouter } from '@tanstack/react-router'
export function JokeForm() {
const router = useRouter()
const [question, setQuestion] = useState('')
const [answer, setAnswer] = useState('')
const [isSubmitting, setIsSubmitting] = useState(false)
const [error, setError] = useState<string | null>(null)
const handleSubmit = async () => {
if (!question || !answer || isSubmitting) return
try {
setIsSubmitting(true)
await addJoke({
data: { question, answer },
})
// Clear form
setQuestion('')
setAnswer('')
// Refresh data
router.invalidate()
} catch (error) {
console.error('Failed to add joke:', error)
setError('Failed to add joke')
} finally {
setIsSubmitting(false)
}
}
return (
<form onSubmit={handleSubmit} className="flex flex-row gap-2 mb-6">
{error && (
<div className="bg-red-100 text-red-700 p-2 rounded mb-4">{error}</div>
)}
<input
type="text"
name="question"
placeholder="Question"
className="p-1 border rounded w-full"
required
onChange={(e) => setQuestion(e.target.value)}
value={question}
/>
<input
type="text"
name="answer"
placeholder="Answer"
className="p-1 border rounded w-full"
required
onChange={(e) => setAnswer(e.target.value)}
value={answer}
/>
<button
className="bg-blue-500 text-white p-1 rounded hover:bg-blue-600"
disabled={isSubmitting}
>
{isSubmitting ? 'Adding...' : 'Add Joke'}
</button>
</form>
)
}
Damit sollte unsere UI wie folgt aussehen: 
Lassen Sie uns aufschlüsseln, wie die verschiedenen Teile unserer Anwendung zusammenarbeiten
Serverfunktionen: Diese laufen auf dem Server und verwalten Datenoperationen
TanStack Router: Verwaltet Routing und Datenladung
React-Komponenten: Bauen die Benutzeroberfläche unserer Anwendung auf
Dateibasiertes Speichersystem: Speichert unsere Witze in einer JSON-Datei

Wenn ein Benutzer die Startseite besucht
Wenn ein Benutzer einen neuen Witz hinzufügt
Hier ist eine Demo der App in Aktion
Hier sind einige häufige Probleme, auf die Sie beim Erstellen Ihrer TanStack Start-Anwendung stoßen könnten, und wie Sie sie lösen können
Wenn Ihre Serverfunktionen nicht wie erwartet funktionieren
Wenn Routendaten nicht korrekt geladen werden
Wenn Formularübermittlungen nicht funktionieren
Beim Arbeiten mit dateibasiertem Speicher
Herzlichen Glückwunsch! Sie haben eine Full-Stack DevJokes-App mit TanStack Start erstellt. In diesem Tutorial haben Sie gelernt
Diese einfache Anwendung demonstriert die Leistungsfähigkeit von TanStack Start zum Erstellen von Full-Stack-Anwendungen mit minimalem Code. Sie können diese App erweitern, indem Sie Funktionen hinzufügen wie
Der vollständige Code für dieses Tutorial ist auf GitHub verfügbar.
Ihre wöchentliche Dosis JavaScript-Nachrichten. Jeden Montag kostenlos an über 100.000 Entwickler geliefert.