mcpField
F
fullstack-workflow

fullstack-workflow

github.com/elie222开发12.8k41 次安装今天更新
代码仓库

关于 fullstack-workflow

用于指导端到端功能开发的技能,涵盖从 GET 路由到 UI 表单的实现流程。面向使用 Next.js、SWR、Server Action、React Hook Form 和 Zod 的开发者。

主要功能

  1. 01使用认证中间件构建 GET 路由
  2. 02使用 Server Action 执行数据变更
  3. 03通过 SWR 获取并缓存数据
  4. 04使用 React Hook Form 和 Zod 验证表单
  5. 05类型化响应与加载状态处理

适用场景

  1. 01开发同时读取和更新数据的功能
  2. 02添加需要认证的 API 路由
  3. 03创建经过验证的数据变更表单

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/elie222/inbox-zero --skill fullstack-workflow

安装后在 Claude Code 中输入/fullstack-workflow确认已加载。

详细介绍

全栈工作流

从 API 到 UI 构建功能的完整指南,涵盖 GET API 路由、数据获取、表单处理和服务器操作。

概述

构建新功能时,请遵循以下模式:

  1. GET API 路由 - 用于获取数据
  2. 服务器操作 - 用于修改数据(创建/更新/删除)
  3. 数据获取 - 在客户端使用 SWR
  4. 表单处理 - 使用 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):

客户端上下文使用场景
actionClientUserctx.userId仅需要经过身份验证的用户
actionClientctx.emailAccountId、ctx.userId需要用户和电子邮件账户(大多数修改操作)
adminActionClientctx.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 回调的 useAction Hook
  • 使用 @/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