用 TanStack Start 调用外部 API
本指南演示如何用路由加载器把外部 API 调用集成到你的 TanStack Start 应用中。我们将用 TMDB API 获取热门电影,并理解在 TanStack Start 应用中如何获取数据。
本教程的完整代码见 GitHub。
你将学到
- 在 TanStack Start 中搭建外部 API 集成
- 实现用于服务端数据获取的路由加载器
- 用获取到的数据构建响应式 UI 组件
- 处理加载状态和错误管理
前置条件
- React 和 TypeScript 的基础知识
- 你的机器上安装了 Node.js(v18+)和
pnpm - 一个 TMDB API 密钥(在 themoviedb.org 免费获取)
最好先了解
搭建 TanStack Start 项目
首先,创建一个新的 TanStack Start 项目:
pnpx create-start-app movie-discovery
cd movie-discovery脚本运行时,会问你几个设置问题。你可以选择适合你的选项,也可以直接按回车接受默认值。
可选地,你可以传入 --add-on 标志,获得 Shadcn、Clerk、Convex、TanStack Query 等选项。
设置完成后,安装依赖并启动开发服务器:
pnpm i
pnpm dev理解项目结构
此时,项目结构应该看起来像这样:
/movie-discovery
├── src/
│ ├── routes/
│ │ ├── __root.tsx # Root layout
│ │ ├── index.tsx # Home page
│ │ └── fetch-movies.tsx # Movie fetching route
│ ├── types/
│ │ └── movie.ts # Movie type definitions
│ ├── router.tsx # Router configuration
│ ├── routeTree.gen.ts # Generated route tree
│ └── styles.css # Global styles
├── public/ # Static assets
├── vite.config.ts or rsbuild.config.ts # TanStack Start configuration
├── package.json # Project dependencies
└── tsconfig.json # TypeScript configuration
项目设置好后,你可以在 localhost:3000 访问你的应用。你应该会看到默认的 TanStack Start 欢迎页。
第 1 步:创建一个带 TMDB_AUTH_TOKEN 的 .env 文件
要从 TMDB API 获取电影,你需要一个认证令牌。你可以在 themoviedb.org 免费获取。
首先,为我们的 API 密钥设置环境变量。在项目根目录创建一个 .env 文件:
touch .env
把你的 TMDB API 令牌添加到这个文件:
TMDB_AUTH_TOKEN=your_bearer_token_here重要:确保把 .env 添加到你的 .gitignore 文件中,以保证你的 API 密钥安全。
第 2 步:定义数据类型
让我们为电影数据创建 TypeScript 接口。在 src/types/movie.ts 新建一个文件:
// src/types/movie.ts
export interface Movie {
id: number
title: string
overview: string
poster_path: string | null
backdrop_path: string | null
release_date: string
vote_average: number
popularity: number
}
export interface TMDBResponse {
page: number
results: Movie[]
total_pages: number
total_results: number
}第 3 步:创建带 API 获取函数的路由
要调用 TMDB API,我们将创建一个在服务器上获取数据的服务器函数。这种方式让我们的 API 凭据保持安全,永远不会暴露给客户端。
让我们创建从 TMDB API 获取数据的路由。在 src/routes/fetch-movies.tsx 新建一个文件:
// src/routes/fetch-movies.tsx
import { createFileRoute } from '@tanstack/react-router'
import type { Movie, TMDBResponse } from '../types/movie'
import { createServerFn } from '@tanstack/react-start'
const API_URL =
'https://api.themoviedb.org/3/discover/movie?include_adult=false&include_video=false&language=en-US&page=1&sort_by=popularity.desc'
const fetchPopularMovies = createServerFn().handler(
async (): Promise<TMDBResponse> => {
const response = await fetch(API_URL, {
headers: {
accept: 'application/json',
Authorization: `Bearer ${process.env.TMDB_AUTH_TOKEN}`,
},
})
if (!response.ok) {
throw new Error(`Failed to fetch movies: ${response.statusText}`)
}
return response.json()
},
)
export const Route = createFileRoute('/fetch-movies')({
component: MoviesPage,
loader: async (): Promise<{ movies: Movie[]; error: string | null }> => {
try {
const moviesData = await fetchPopularMovies()
return { movies: moviesData.results, error: null }
} catch (error) {
console.error('Error fetching movies:', error)
return { movies: [], error: 'Failed to load movies' }
}
},
})这里发生了什么:
createServerFn()创建了一个仅在服务器上运行的函数,确保我们的TMDB_AUTH_TOKEN环境变量永远不会暴露给客户端。服务器函数向 TMDB API 发起带认证的请求,并返回解析后的 JSON 响应。- 当用户访问 /fetch-movies 时,路由加载器在服务器上运行,在页面渲染之前调用我们的服务器函数
- 错误处理确保组件始终收到有效的数据结构——要么是电影列表,要么是一个空数组加错误信息
- 这种模式开箱即用地提供了服务端渲染、自动类型安全和安全地处理 API 凭据的能力。
译者注:为什么用服务器函数而不是直接在 loader 里 fetch?
关键原因是保护 API 凭据:TMDB_AUTH_TOKEN 是仅服务端的环境变量。如果直接在路由的 loader 里 fetch(加载器是同构的,会同时打进客户端和服务端包),令牌可能被编译进客户端打包产物。把请求包进 createServerFn(),客户端拿到的只是一个 RPC 桩,真实的网络请求只发生在服务器上。这就是「服务器函数是 API 凭据的安全边界」的典型例子。
第 4 步:构建电影组件
现在让我们创建展示电影数据的组件。把这些组件添加到同一个 fetch-movies.tsx 文件中:
// MovieCard component
const MovieCard = ({ movie }: { movie: Movie }) => {
return (
<div
className="bg-white/10 border border-white/20 rounded-lg overflow-hidden backdrop-blur-sm shadow-md hover:shadow-xl transition-all duration-300 hover:scale-105"
aria-label={`Movie: ${movie.title}`}
role="group"
>
{movie.poster_path && (
<img
src={`https://image.tmdb.org/t/p/w500${movie.poster_path}`}
alt={movie.title}
className="w-full h-64 object-cover"
/>
)}
<div className="p-4">
<MovieDetails movie={movie} />
</div>
</div>
)
}
// MovieDetails component
const MovieDetails = ({ movie }: { movie: Movie }) => {
return (
<>
<h3 className="text-lg font-semibold mb-2 line-clamp-2">{movie.title}</h3>
<p className="text-sm text-gray-300 mb-3 line-clamp-3 h-10">
{movie.overview}
</p>
<div className="flex justify-between items-center text-xs text-gray-400">
<span>{movie.release_date}</span>
<span className="flex items-center">
⭐️ {movie.vote_average.toFixed(1)}
</span>
</div>
</>
)
}第 5 步:创建 MoviesPage 组件
最后,让我们创建消费加载器数据的主组件:
// MoviesPage component
const MoviesPage = () => {
const { movies, error } = Route.useLoaderData()
return (
<div
className="flex items-center justify-center min-h-screen p-4 text-white"
style={{
backgroundColor: '#000',
backgroundImage:
'radial-gradient(ellipse 60% 60% at 0% 100%, #444 0%, #222 60%, #000 100%)',
}}
role="main"
aria-label="Popular Movies Section"
>
<div className="w-full max-w-6xl p-8 rounded-xl backdrop-blur-md bg-black/50 shadow-xl border-8 border-black/10">
<h1 className="text-3xl mb-6 font-bold text-center">Popular Movies</h1>
{error && (
<div
className="text-red-400 text-center mb-4 p-4 bg-red-900/20 rounded-lg"
role="alert"
>
{error}
</div>
)}
{movies.length > 0 ? (
<div
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6"
aria-label="Movie List"
>
{movies.slice(0, 12).map((movie) => (
<MovieCard key={movie.id} movie={movie} />
))}
</div>
) : (
!error && (
<div className="text-center text-gray-400" role="status">
Loading movies...
</div>
)
)}
</div>
</div>
)
}理解各部分如何协同工作
sequenceDiagram
autonumber
actor U as User
participant R as Router (TanStack Start)
participant L as Route Loader (/fetch-movies)
participant A as External API (TMDB)
participant V as MoviesPage (UI)
U->>R: Navigate to /fetch-movies
R->>L: Invoke loader (server-side)
L->>A: GET /movie/popular\nAuthorization: Bearer <TOKEN>
A-->>L: JSON TMDBResponse
alt response.ok
L-->>R: { movies, error: null }
R->>V: Render SSR with movies
V-->>U: HTML with movie grid
else non-ok / error
L-->>R: { movies: [], error: "Failed to load movies" }
R->>V: Render SSR with error alert
V-->>U: HTML with error state
end
note over L,V: Loader validates response.ok,\nreturns data or error for initial render让我们拆解应用的不同部分是如何协同工作的:
- 路由加载器:当用户访问
/fetch-movies时,加载器函数在服务器上运行 - API 调用:加载器调用
fetchPopularMovies(),它向 TMDB 发起 HTTP 请求 - 服务端渲染:数据在服务器上获取,减轻客户端的负载
- 组件渲染:
MoviesPage组件通过Route.useLoaderData()接收数据 - 渲染 UI:用获取到的数据渲染电影卡片
第 6 步:测试你的应用
现在你可以访问 http://localhost:3000/fetch-movies 来测试你的应用。如果一切设置正确,你应该会看到一个热门电影网格,带有海报、标题和评分。你的应用应该看起来像这样:

总结
你已经成功构建了一个用 TanStack Start 集成外部 API 的电影发现应用。本教程演示了如何用路由加载器做服务端数据获取,以及如何用外部数据构建 UI 组件。
虽然在 TanStack Start 中于构建时获取数据非常适合博客文章或产品页面这类静态内容,但它并不适合交互式应用。如果你需要实时更新、缓存或无限滚动等功能,你应该在客户端使用 TanStack Query。TanStack Query 让处理动态数据变得简单,内置缓存、后台更新和流畅的用户交互。用 TanStack Start 处理静态内容、用 TanStack Query 处理交互特性,你既能获得快速的页面加载,又能拥有用户期望的所有现代功能。