AgentSkillsCN

add-trpc-endpoint

为dealflow-network项目搭建新的tRPC API端点,配备适当的Zod验证、中间件、数据库函数和客户端钩子。当添加新API路由、创建CRUD操作或扩展现有路由器时,请使用此功能。

SKILL.md
--- frontmatter
name: add-trpc-endpoint
description: Scaffold new tRPC API endpoints for the dealflow-network project with proper Zod validation, middleware, database functions, and client hooks. Use when adding new API routes, creating CRUD operations, or extending existing routers.

Add tRPC Endpoint

Scaffold complete tRPC endpoints following project patterns.

Quick Start

When adding a new endpoint, I will:

  1. Add Zod input schema to server/routers.ts
  2. Create database function in server/db.ts (if needed)
  3. Add procedure with appropriate middleware
  4. Show client usage pattern

Procedure Types

Choose based on auth requirements:

typescript
// No authentication required
publicProcedure

// Requires logged-in user (ctx.user available)
protectedProcedure

// Requires admin role (ctx.user.role === 'admin')
adminProcedure

Template: Query Endpoint

typescript
// In server/routers.ts - add to appropriate router

const getItem = protectedProcedure
  .input(z.object({
    id: z.number(),
  }))
  .query(async ({ ctx, input }) => {
    const db = await getDb();
    if (!db) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });

    const [item] = await db
      .select()
      .from(items)
      .where(eq(items.id, input.id));

    if (!item) {
      throw new TRPCError({ code: "NOT_FOUND", message: "Item not found" });
    }

    return item;
  });

Template: Mutation Endpoint

typescript
const createItem = protectedProcedure
  .input(z.object({
    name: z.string().min(1, "Name is required"),
    description: z.string().optional(),
    categoryId: z.number().optional(),
  }))
  .mutation(async ({ ctx, input }) => {
    const db = await getDb();
    if (!db) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });

    const [result] = await db.insert(items).values({
      ...input,
      createdBy: ctx.user.id,
      createdAt: new Date(),
    });

    return { id: result.insertId, ...input };
  });

Template: List with Pagination

typescript
const listItems = protectedProcedure
  .input(z.object({
    page: z.number().default(1),
    limit: z.number().default(20),
    search: z.string().optional(),
  }))
  .query(async ({ ctx, input }) => {
    const db = await getDb();
    if (!db) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });

    const offset = (input.page - 1) * input.limit;

    let query = db.select().from(items);

    if (input.search) {
      query = query.where(like(items.name, `%${input.search}%`));
    }

    const results = await query.limit(input.limit).offset(offset);

    return results;
  });

Adding to Router

typescript
// In server/routers.ts
export const appRouter = router({
  // ... existing routers
  items: router({
    list: listItems,
    get: getItem,
    create: createItem,
    update: updateItem,
    delete: deleteItem,
  }),
});

Client Usage

typescript
// Query hook
const { data, isLoading, error } = trpc.items.list.useQuery({ page: 1 });

// Mutation hook
const createMutation = trpc.items.create.useMutation({
  onSuccess: () => {
    // Invalidate cache to refetch list
    utils.items.list.invalidate();
    toast.success("Item created");
  },
  onError: (error) => {
    toast.error(`Failed: ${error.message}`);
  },
});

// Call mutation
createMutation.mutate({ name: "New Item" });

Common Zod Patterns

typescript
// Required string with min length
name: z.string().min(1, "Required")

// Optional email
email: z.string().email().optional().or(z.literal(""))

// URL validation
linkedinUrl: z.string().url().optional().or(z.literal(""))

// Enum
status: z.enum(["pending", "active", "completed"])

// Array of IDs
tagIds: z.array(z.number())

// Nested object
metadata: z.object({
  source: z.string(),
  confidence: z.number(),
}).optional()

Error Handling

typescript
import { TRPCError } from "@trpc/server";

// Common error codes
throw new TRPCError({ code: "NOT_FOUND", message: "Item not found" });
throw new TRPCError({ code: "UNAUTHORIZED" });
throw new TRPCError({ code: "FORBIDDEN", message: "Admin only" });
throw new TRPCError({ code: "BAD_REQUEST", message: "Invalid input" });
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });

Checklist

  • Input validation with Zod schema
  • Appropriate procedure type (public/protected/admin)
  • Database null check
  • Error handling with TRPCError
  • Add to router export
  • Client cache invalidation strategy