关于 fullstack-workflow
用于指导端到端功能开发的技能,涵盖从 GET 路由到 UI 表单的实现流程。面向使用 Next.js、SWR、Server Action、React Hook Form 和 Zod 的开发者。
主要功能
- 01使用认证中间件构建 GET 路由
- 02使用 Server Action 执行数据变更
- 03通过 SWR 获取并缓存数据
- 04使用 React Hook Form 和 Zod 验证表单
- 05类型化响应与加载状态处理
适用场景
- 01开发同时读取和更新数据的功能
- 02添加需要认证的 API 路由
- 03创建经过验证的数据变更表单
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ elie222/ inbox-zero --skill fullstack-workflow安装后在 Claude Code 中输入/fullstack-workflow确认已加载。
详细介绍
全栈工作流
从 API 到 UI 构建功能的完整指南,涵盖 GET API 路由、数据获取、表单处理和服务器操作。
概述
构建新功能时,请遵循以下模式:
- GET API 路由 - 用于获取数据
- 服务器操作 - 用于修改数据(创建/更新/删除)
- 数据获取 - 在客户端使用 SWR
- 表单处理 - 使用 React Hook Form 和 Zod 验证
1. GET API 路由
用于获取数据。始终使用 withAuth 或 withEmailAccount 封装:
// apps/web/app/api/user/example/route.ts
import { NextResponse } from "next/server";
import prisma from "@/utils/prisma";
import { withEmailAccount } from "@/utils/middleware";
// Auto-generate response type for client use
export type GetExampleResponse = Awaited<ReturnType<typeof getData>>;
export const GET = withEmailAccount(async (request) => {
const { emailAccountId } = request.auth;
const result = await getData({ emailAccountId });
return NextResponse.json(result);
});
// We make this its own function so we can infer the return type for a type-safe response on the client
async function getData({ emailAccountId }: { emailAccountId: string }) {
const items = await prisma.example.findMany({
where: { emailAccountId },
});
return { items };
}
2. 服务器操作
用于修改数据。使用 next-safe-action 并进行适当的验证。
操作客户端(定义于 apps/web/utils/actions/safe-action.ts):
| 客户端 | 上下文 | 使用场景 |
|---|---|---|
actionClientUser | ctx.userId | 仅需要经过身份验证的用户 |
actionClient | ctx.emailAccountId、ctx.userId | 需要用户和电子邮件账户(大多数修改操作) |
adminActionClient | ctx.logger | 仅限管理员的操作(ctx 中没有 userId) |
始终使用 .metadata({ name: "actionName" }) 进行 Sentry 插桩。对于预期的错误,请使用 SafeError。
验证架构(apps/web/utils/actions/example.validation.ts):
import { z } from "zod";
export const createExampleBody = z.object({
name: z.string().min(1, "Name is required"),
email: z.string().email("Invalid email"),
description: z.string().optional(),
});
export type CreateExampleBody = z.infer<typeof createExampleBody>;
export const updateExampleBody = z.object({
id: z.string(),
name: z.string().optional(),
email: z.string().email().optional(),
description: z.string().optional(),
});
export type UpdateExampleBody = z.infer<typeof updateExampleBody>;
服务器操作(apps/web/utils/actions/example.ts):
"use server";
import { actionClient } from "@/utils/actions/safe-action";
import { createExampleBody, updateExampleBody } from "@/utils/actions/example.validation";
import prisma from "@/utils/prisma";
export const createExampleAction = actionClient
.metadata({ name: "createExample" })
.inputSchema(createExampleBody)
.action(async ({
ctx: { emailAccountId },
parsedInput: { name, email, description }
}) => {
const example = await prisma.example.create({
data: {
name,
email,
description,
emailAccountId,
},
});
return example;
});
export const updateExampleAction = actionClient
.metadata({ name: "updateExample" })
.inputSchema(updateExampleBody)
.action(async ({
ctx: { emailAccountId },
parsedInput: { id, name, email, description }
}) => {
const example = await prisma.example.update({
where: { id, emailAccountId },
data: { name, email, description },
});
return example;
});
3. 数据获取
使用 SWR 在客户端获取数据:
import useSWR from "swr";
import { GetExampleResponse } from "@/app/api/user/example/route";
export function useExamples() {
return useSWR<GetExampleResponse>("/api/user/example");
}
4. 表单处理
使用 React Hook Form,并搭配 next-safe-action/hooks 中的 useAction:
import { useCallback } from "react";
import { useForm, type SubmitHandler } from "react-hook-form";
import { useAction } from "next-safe-action/hooks";
import { zodResolver } from "@hookform/resolvers/zod";
import { Input } from "@/components/Input";
import { Button } from "@/components/ui/button";
import { toastSuccess, toastError } from "@/components/Toast";
import { getActionErrorMessage } from "@/utils/error";
import { createExampleAction } from "@/utils/actions/example";
import { createExampleBody, type CreateExampleBody } from "@/utils/actions/example.validation";
export function ExampleForm({ onSuccess }: { onSuccess?: () => void }) {
const {
register,
handleSubmit,
formState: { errors },
reset,
} = useForm<CreateExampleBody>({
resolver: zodResolver(createExampleBody),
});
const { execute, isExecuting } = useAction(createExampleAction, {
onSuccess: () => {
toastSuccess({ description: "Example created!" });
reset();
onSuccess?.();
},
onError: (error) => {
toastError({
description: getActionErrorMessage(error.error),
});
},
});
return (
<form className="space-y-4" onSubmit={handleSubmit(execute)}>
<Input
type="text"
name="name"
label="Name"
registerProps={register("name")}
error={errors.name}
/>
<Input
type="email"
name="email"
label="Email"
registerProps={register("email")}
error={errors.email}
/>
<Input
type="text"
name="description"
label="Description"
registerProps={register("description")}
error={errors.description}
/>
<Button type="submit" loading={isExecuting}>
Create Example
</Button>
</form>
);
}
5. 完整的数据获取组件
'use client';
import { useExamples } from "@/hooks/useExamples";
import { Button } from "@/components/ui/button";
import { LoadingContent } from "@/components/LoadingContent";
export function Examples() {
const { data, isLoading, error } = useExamples();
return (
<LoadingContent loading={isLoading} error={error}>
<div className="grid gap-4">
{data?.examples.map((example) => (
<div key={example.id} className="border p-4 rounded">
<h3 className="font-semibold">{example.name}</h3>
<p className="text-gray-600">{example.email}</p>
{example.description && (
<p className="text-sm text-gray-500">{example.description}</p>
)}
</div>
))}
</div>
</LoadingContent>
);
}
关键准则
身份验证与授权
- 用户级操作使用
withAuth - 电子邮件账户级操作使用
withEmailAccount - 服务器操作会自动获取正确的上下文
数据修改
- 所有数据修改操作(创建/更新/删除)都使用服务器操作
- 不要使用 POST API 路由修改数据,应改用服务器操作
错误处理
- 使用带有
onSuccess和onError回调的useActionHook - 使用
@/utils/error中的getActionErrorMessage(error.error)提取适合用户阅读的错误消息 - 对于“前缀 + 错误”模式:
getActionErrorMessage(error.error, { prefix: "Failed to save" }) next-safe-action提供集中式错误处理,并会扁平化验证错误- 使用中间件时,GET 路由无需使用 try/catch
类型安全
- 从 GET 路由导出响应类型
- 在客户端和服务端使用 Zod 架构进行验证
- 利用 TypeScript 类型推断,改善开发体验
加载和错误状态
- 使用
LoadingContent组件,以统一的方式处理加载和错误状态 - 向
LoadingContent传入loading、error和子元素属性 - 以标准化的方式显示加载指示器和错误消息
性能
- 使用 SWR 高效获取和缓存数据
- 成功修改数据后调用
mutate()刷新数据
文件组织
apps/web/
├── app/api/user/example/route.ts # GET API route
├── utils/actions/example.validation.ts # Zod schemas
├── utils/actions/example.ts # Server actions
├── hooks/useExamples.ts # SWR hook
└── components/ExampleForm.tsx # Form component
相关规则
评论
支持 Markdown