File: reading-writing-file.md | Updated: 11/15/2025
Search...
+ K
Auto
Docs Examples GitHub Contributors
Docs Examples GitHub Contributors
Docs Examples GitHub Contributors
Docs Examples Github Contributors
Docs Examples Github Contributors
Docs Examples Github Contributors
Docs Examples Github Contributors
Docs Examples Github Contributors
Maintainers Partners Support Learn StatsBETA Discord Merch Blog GitHub Ethos Brand Guide
Documentation
Framework
React
Version
Latest
Search...
+ K
Menu
Getting Started
Guides
Examples
Tutorials
Framework
React
Version
Latest
Menu
Getting Started
Guides
Examples
Tutorials
On this page
Copy Markdown
This tutorial will guide you through building a complete full-stack application using TanStack Start. You'll create a DevJokesapp where users can view and add developer-themed jokes, demonstrating key concepts of TanStack Start including server functions, file-based data storage, and React components.
Here's a demo of the app in action:
The complete code for this tutorial is available on GitHub .
What You'll Learn
-----------------
Setting up a TanStack Start Project
-----------------------------------
First, let's create a new TanStack Start project:
bash
pnpx create-start-app devjokes
cd devjokes
pnpx create-start-app devjokes
cd devjokes
When this script runs, it will ask you a few setup questions. You can either pick choices that work for you or just press enter to accept the defaults.
Optionally, you can pass in a --add-on flag to get options such as Shadcn, Clerk, Convex, TanStack Query, etc.
Once setup is complete, install dependencies and start the development server:
bash
pnpm i
pnpm dev
pnpm i
pnpm dev
For this project, we'll need a few additional packages:
bash
# 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
Understanding the project structure
-----------------------------------
At this point, the project structure should look like this -
/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
├── vite.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
├── vite.config.ts # TanStack Start configuration
├── package.json # Project dependencies
└── tsconfig.json # TypeScript configuration
This structure might seem overwhelming at first, but here are the key files you need to focus on:
Once your project is set up, you can access your app at localhost:3000. You should see the default TanStack Start welcome page.
At this point, your app will look like this -

Step 1: Reading Data From a File
--------------------------------
Let's start by creating a file-based storage system for our jokes.
### Step 1.1: Create a JSON File with Jokes
Let's set up a list of jokes that we can use to render on the page. Create a data directory in your project root and a jokes.json file within it:
bash
mkdir -p src/data
touch src/data/jokes.json
mkdir -p src/data
touch src/data/jokes.json
Now, let's add some sample jokes to this file:
json
[\
{\
"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;"\
}\
]
### Step 1.2: Create Types for Our Data
Let's create a file to define our data types. Create a new file at src/types/index.ts:
typescript
// 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[]
### Step 1.3: Create Server Functions to Read the File
Let's create a new file src/serverActions/jokesActions.ts to create a server function to perform a read-write operation. We will be creating a server function using createServerFn .
tsx
// 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 this code, we are using createServerFn to create a server function that reads the jokes from the JSON file. The handler function is where we are using the fs module to read the file.
### Step 1.4: Consume Server Function on the Client Side
Now to consume this server function, we can simply call it in our code using TanStack Router which already comes with TanStack Start!
Now let's create a new component JokesList to render the jokes on the page with a little Tailwind styling sprinkle.
tsx
// 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>
)
}
Now let's call our server function inside App.jsx using TanStack Router which already comes with TanStack Start!
jsx
// 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>
)
}
When the page loads, jokes will have data from the jokes.json file already!
With a little Tailwind styling, the app should look like this:

Step 2: Writing Data to a File
------------------------------
So far, we have been able to read from the file successfully! We can use the same approach to write to the jokes.json file using createServerFunction.
### Step 2.1: Create Server Function to Write to the File
It's time to modify the jokes.json file so that we can add new jokes to it. Let's create another server function but this time with a POST method to write to the same file.
tsx
// 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' })
.inputValidator((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' })
.inputValidator((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 this code:
### Step 2.2: Adding a Form to Add Jokes to our JSON File
Now, let's modify our home page to display jokes and provide a form to add new ones. Let's create a new component called JokeForm.jsx and add the following form to it:
tsx
// 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>
)
}
### Step 2.3: Wire Up the Form to the Server Function
Now, let's wire the form up to our addJoke server function in the handleSubmit function. Calling a server action is simple! It's just a function call.
tsx
//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>
)
}
With this, our UI should look like this: 
Understanding How It All Works Together
---------------------------------------
Let's break down how the different parts of our application work together:
Server Functions: These run on the server and handle data operations
TanStack Router: Handles routing and data loading
React Components: Build the UI of our application
File-Based Storage: Stores our jokes in a JSON file
How Data Flows Through the Application
--------------------------------------
### Data Flow
mermaid
jokes.jsonServerRoute Loader (loader)Browser (HomePage + Form)jokes.jsonServerRoute Loader (loader)Browser (HomePage + Form)UserVisit /1loader() calls getJokes()2getJokes()3Read jokes.json4jokes data5jokes[]6useLoaderData() → jokes[]7Fill form and submit8handleSubmit → addJoke(newJoke)9Read jokes.json10Write updated jokes.json11addJoke() resolved12router.invalidate() (re-run loader)13getJokes()14Read jokes.json15updated jokes[]16updated jokes[]17useLoaderData() → updated jokes[]18User
jokes.jsonServerRoute Loader (loader)Browser (HomePage + Form)jokes.jsonServerRoute Loader (loader)Browser (HomePage + Form)UserVisit /1loader() calls getJokes()2getJokes()3Read jokes.json4jokes data5jokes[]6useLoaderData() → jokes[]7Fill form and submit8handleSubmit → addJoke(newJoke)9Read jokes.json10Write updated jokes.json11addJoke() resolved12router.invalidate() (re-run loader)13getJokes()14Read jokes.json15updated jokes[]16updated jokes[]17useLoaderData() → updated jokes[]18User
When a user visits the home page:
When a user adds a new joke:
Here's a demo of the app in action:
Common Issues and Debugging
---------------------------
Here are some common issues you might encounter when building your TanStack Start application and how to resolve them:
### Server Functions Not Working
If your server functions aren't working as expected:
If route data isn't loading properly:
If form submissions aren't working:
### File Reading/Writing Issues
When working with file-based storage:
Congratulations! You've built a full-stack DevJokes app using TanStack Start. In this tutorial, you've learned:
This simple application demonstrates the power of TanStack Start for building full-stack applications with a minimal amount of code. You can extend this app by adding features like:
The complete code for this tutorial is available on GitHub .
Fetching Data from External API
