Next.js 14 Server Actions 实战:告别 API Routes 的 5 个理由
Next.js 14 的 Server Actions 是我今年最喜欢的前端特性。
过去写一个表单提交要这样:定义 API Route → 写 fetch 调用 → 处理 loading 状态 → 处理 error → 更新 UI。一套下来至少 4 个文件。
现在用 Server Actions,一个文件搞定。
什么是 Server Actions
Server Actions 就是可以在客户端直接调用的服务端函数。用 'use server' 标记。
// app/actions.ts
'use server'
import { revalidatePath } from 'next/cache'
import { db } from '@/lib/db'
export async function createPost(formData: FormData) {
const title = formData.get('title') as string
const content = formData.get('content') as string
// 直接在服务端操作数据库
await db.post.create({
data: { title, content }
})
// 重新验证缓存
revalidatePath('/posts')
}// app/page.tsx
'use client'
import { createPost } from './actions'
export default function NewPostForm() {
return (
<form action={createPost}>
<input name="title" required />
<textarea name="content" required />
<button type="submit">发布</button>
</form>
)
}理由 1:代码量减少 60%
传统方式需要:
app/
api/
posts/
route.ts # API Route 定义
components/
NewPostForm.tsx # 表单组件 + fetch + 状态管理
types/
post.ts # 类型定义Server Actions 方式:
app/
actions.ts # 所有服务端操作
page.tsx # 表单组件对比一下代码量:
// 传统方式 - API Route
// app/api/posts/route.ts
export async function POST(request: Request) {
try {
const body = await request.json()
const post = await db.post.create({ data: body })
return Response.json(post, { status: 201 })
} catch (error) {
return Response.json({ error: '创建失败' }, { status: 500 })
}
}
// 传统方式 - 前端调用
// components/NewPostForm.tsx
'use client'
export function NewPostForm() {
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const handleSubmit = async (e: FormEvent) => {
e.preventDefault()
setLoading(true)
try {
const res = await fetch('/api/posts', {
method: 'POST',
body: JSON.stringify({ title, content })
})
if (!res.ok) throw new Error('失败')
router.refresh()
} catch (err) {
setError(err.message)
} finally {
setLoading(false)
}
}
// ... JSX
}
// Server Actions 方式 - 一个文件搞定
// app/actions.ts
'use server'
export async function createPost(formData: FormData) {
await db.post.create({
data: {
title: formData.get('title'),
content: formData.get('content')
}
})
revalidatePath('/posts')
}理由 2:渐进增强
Server Actions 天然支持无 JS 运行。表单的 action 属性在 JS 禁用时依然能工作:
// 这个表单在 JS 禁用时依然能提交
<form action={createPost}>
<input name="title" required />
<button type="submit">提交</button>
</form>配合 useFormStatus 还能拿到提交状态:
'use client'
import { useFormStatus } from 'react-dom'
function SubmitButton() {
const { pending } = useFormStatus()
return (
<button type="submit" disabled={pending}>
{pending ? '提交中...' : '提交'}
</button>
)
}理由 3:类型安全
传统 API Route 需要自己定义请求/响应的类型,前端和后端的类型很容易不同步。Server Actions 天然共享类型:
// actions.ts
'use server'
interface CreatePostInput {
title: string
content: string
tags: string[]
}
export async function createPost(input: CreatePostInput) {
// input 的类型在前后端完全一致
return await db.post.create({ data: input })
}
// page.tsx - 调用时自动有类型提示
import { createPost } from './actions'
// createPost({ title: '', ... }) // IDE 自动补全参数理由 4:乐观更新更简单
'use client'
import { useOptimistic } from 'react'
import { createPost } from './actions'
export function PostList({ initialPosts }: { initialPosts: Post[] }) {
const [posts, addOptimisticPost] = useOptimistic(
initialPosts,
(state, newPost: Post) => [newPost, ...state]
)
const handleCreate = async (formData: FormData) => {
const newPost = {
id: 'temp',
title: formData.get('title') as string,
content: formData.get('content') as string,
}
// 立即更新 UI
addOptimisticPost(newPost)
// 后台提交
await createPost(formData)
}
return (
<>
<form action={handleCreate}>...</form>
{posts.map(post => (
<PostCard key={post.id} post={post} />
))}
</>
)
}理由 5:安全
Server Actions 默认是 POST 请求,自动处理 CSRF 保护:
// Next.js 自动为 Server Actions 添加 CSRF token
// 你不需要手动处理
// 还可以用 server-only 包确保代码只在服务端运行
import 'server-only'
export async function deleteUser(userId: string) {
// 这个函数永远不会被打包到客户端代码中
await db.user.delete({ where: { id: userId } })
}注意事项
Server Actions 不是银弹:
1. 不适合 GET 请求 - 查询数据还是用 Server Components 或 API Routes
2. 不适合第三方调用 - 如果需要对外开放 API,还是用 API Routes
3. 序列化限制 - 参数和返回值必须可序列化(不能传函数、Class 实例等)
4. 调试困难 - 错误栈不直观,建议加详细日志
'use server'
export async function safeAction<T>(
fn: () => Promise<T>,
errorMessage: string
): Promise<{ data?: T; error?: string }> {
try {
const data = await fn()
return { data }
} catch (error) {
console.error(`[Server Action] ${errorMessage}:`, error)
return { error: errorMessage }
}
}
// 使用
const result = await safeAction(
() => db.post.create({ data: input }),
'创建文章失败'
)总结
Server Actions 让全栈开发回到了"简单"的轨道上。如果你在用 Next.js 14+,强烈建议试试。
读者评论 4