AgentSkillsCN

openapi-to-components

将 Synnovator OpenAPI 规范转换为完全集成的 Next.js 前端组件。读取 .synnovator/openapi.yaml 和 frontend/components/pages/*.tsx 文件,随后生成 API 客户端(lib/api-client.ts)、TypeScript 类型定义(lib/types.ts)、服务器端数据获取函数(lib/api/*.ts),并更新现有页面组件,以 Next.js App Router 服务器组件为基础,将硬编码的模拟数据替换为真实的 API 调用。适用场景如下:(1) 需要将前端组件与后端 API 接口进行对接;(2) 希望用真实的 API 获取数据替换页面组件中的模拟数据或硬编码数据;(3) 新增一个需要集成 API 的页面组件;(4) 在 OpenAPI 规范发生变更后,重新生成 API 类型定义。

SKILL.md
--- frontmatter
name: openapi-to-components
description: >
  Convert Synnovator OpenAPI spec into fully integrated Next.js frontend components.
  Reads .synnovator/openapi.yaml and frontend/components/pages/*.tsx, then generates
  API client (lib/api-client.ts), TypeScript types (lib/types.ts), server-side data
  fetching functions (lib/api/*.ts), and updates existing page components to replace
  hardcoded mock data with real API calls using Next.js App Router Server Components.
  Use when:
  (1) Need to wire frontend components to backend API endpoints
  (2) Want to replace mock/hardcoded data in page components with real API fetching
  (3) Adding a new page component that needs API integration
  (4) Regenerating API types after OpenAPI spec changes

Auto OpenAPI to Components

Generate API client code and update Synnovator frontend components to fetch real data via Next.js Server Components.

Prerequisites

  • OpenAPI spec at .synnovator/openapi.yaml
  • Frontend components at frontend/components/pages/*.tsx
  • Mapping reference at docs/frontend-api-mapping.md or references/frontend-api-mapping.md

Workflow

Phase 1: Read Inputs

  1. Read .synnovator/openapi.yaml to get all endpoints, schemas, and enums
  2. Read the target component(s) in frontend/components/pages/ to identify mock data variables and hardcoded values
  3. Read references/frontend-api-mapping.md for the line-by-line mapping of mock data to API endpoints

Phase 2: Generate API Infrastructure

Generate these files in order:

2.1 TypeScript Types — frontend/lib/types.ts

Extract all components/schemas from the OpenAPI spec and convert to TypeScript interfaces:

typescript
// Example: Post schema -> TypeScript interface
export interface Post {
  id: string;
  title: string;
  type: PostType;
  tags: string[];
  status: PostStatus;
  like_count: number;
  comment_count: number;
  average_rating: number | null;
  content?: string;
  created_by?: string;
  created_at: string;
  updated_at: string;
}

export type PostType = "profile" | "team" | "category" | "for_category" | "certificate" | "general";
export type PostStatus = "draft" | "pending_review" | "published" | "rejected";

Conversion rules:

  • OpenAPI string -> string
  • OpenAPI integer -> number
  • OpenAPI number with format: float -> number
  • OpenAPI boolean -> boolean
  • OpenAPI string with format: date-time -> string (ISO 8601)
  • OpenAPI string with format: uri -> string
  • OpenAPI string with format: email -> string
  • OpenAPI array with items -> T[]
  • OpenAPI object with additionalProperties -> Record<string, T>
  • OpenAPI enum -> TypeScript union type
  • OpenAPI $ref -> reference to another interface
  • OpenAPI default: "null" -> | null union
  • Fields listed in required are non-optional; others use ?
  • Paginated list schemas -> PaginatedList<T> generic

Generate these paginated wrapper:

typescript
export interface PaginatedList<T> {
  items: T[];
  total: number;
  skip: number;
  limit: number;
}

2.2 API Client — frontend/lib/api-client.ts

Create a typed fetch wrapper for server-side use:

typescript
const API_BASE = process.env.API_BASE_URL || "http://localhost:8000";

export class ApiError extends Error {
  constructor(public status: number, public code: string, message: string) {
    super(message);
  }
}

export async function apiFetch<T>(
  path: string,
  options?: RequestInit & { params?: Record<string, string | number | boolean | undefined> }
): Promise<T> {
  const { params, ...fetchOptions } = options || {};

  const url = new URL(path, API_BASE);
  if (params) {
    Object.entries(params).forEach(([key, value]) => {
      if (value !== undefined) url.searchParams.set(key, String(value));
    });
  }

  const res = await fetch(url.toString(), {
    ...fetchOptions,
    headers: {
      "Content-Type": "application/json",
      ...fetchOptions?.headers,
    },
  });

  if (!res.ok) {
    const body = await res.json().catch(() => ({}));
    throw new ApiError(
      res.status,
      body?.error?.code || "UNKNOWN",
      body?.error?.message || res.statusText
    );
  }

  if (res.status === 204) return undefined as T;
  return res.json();
}

2.3 Resource-Specific API Functions — frontend/lib/api/*.ts

Create one file per OpenAPI tag. Each function maps to one operation in the spec.

File naming: frontend/lib/api/{tag}.ts (e.g., posts.ts, categories.ts, users.ts, groups.ts, resources.ts, rules.ts, interactions.ts, admin.ts)

Pattern for each function:

typescript
// frontend/lib/api/posts.ts
import { apiFetch } from "../api-client";
import type { Post, PostCreate, PostUpdate, PaginatedList } from "../types";

export async function listPosts(params?: {
  skip?: number;
  limit?: number;
  type?: string;
  status?: string;
  tags?: string[];
}) {
  return apiFetch<PaginatedList<Post>>("/posts", { params, next: { revalidate: 60 } });
}

export async function getPost(postId: string) {
  return apiFetch<Post>(`/posts/${postId}`, { next: { revalidate: 60 } });
}

export async function createPost(data: PostCreate) {
  return apiFetch<Post>("/posts", { method: "POST", body: JSON.stringify(data) });
}

Mapping rules from OpenAPI to function names:

  • operationId: list_posts -> listPosts
  • operationId: get_post -> getPost
  • operationId: create_post -> createPost
  • operationId: update_post -> updatePost
  • operationId: delete_post -> deletePost
  • Nested operations use compound names: list_post_comments -> listPostComments

For GET requests, add next: { revalidate: 60 } for ISR caching.

Phase 3: Update Page Components

For each page component, apply these transformations:

3.1 Convert to Async Server Components

Change the component from client-side to async server component:

typescript
// BEFORE (client component with mock data)
"use client"
const cards = [{ title: "...", author: "..." }]
export function Home() {
  return <div>...</div>
}

// AFTER (server component with real data)
import { listPosts } from "@/lib/api/posts";
import { listCategories } from "@/lib/api/categories";

export default async function Home() {
  const [postsData, categoriesData] = await Promise.all([
    listPosts({ status: "published", limit: 6 }),
    listCategories({ limit: 10 }),
  ]);
  return <div>...</div>
}

3.2 Replace Mock Data Variables

For each mock variable identified in the mapping doc:

  1. Delete the const declaration (e.g., const cards = [...])
  2. Add the corresponding API import and call at the top of the async function
  3. Update JSX to reference the API response fields

Field mapping from API response to component props:

  • Post.title -> card title text
  • Post.created_by -> author ID (requires separate getUser call or embed)
  • Post.tags -> Badge components
  • Post.like_count -> like counter display
  • Post.comment_count -> comment counter display
  • Post.content -> markdown content area
  • Category.name -> tab/filter label
  • Category.cover_image -> banner/card image src
  • User.display_name -> author name text
  • User.avatar_url -> Avatar image src
  • User.bio -> user description text
  • Resource.url -> image/file src
  • Group.name -> team name text
  • Group.description -> team description text

3.3 Extract Interactive Parts to Client Components

Server Components cannot use onClick, useState, useEffect etc. Extract interactive UI into separate client components:

code
frontend/components/
├── pages/          (server components - data fetching)
│   └── home.tsx
├── interactive/    (client components - user interactions)
│   ├── like-button.tsx
│   ├── comment-form.tsx
│   └── search-bar.tsx
└── ui/             (existing shadcn components)

Interactive elements to extract:

  • Like/unlike button -> components/interactive/like-button.tsx
  • Comment form -> components/interactive/comment-form.tsx
  • Search bar -> components/interactive/search-bar.tsx
  • Tab state management -> keep "use client" wrapper per tab section
  • Follow/unfollow button -> components/interactive/follow-button.tsx

Client components use Server Actions for mutations:

typescript
// frontend/app/actions/posts.ts
"use server"
import { likePost, unlikePost } from "@/lib/api/interactions";

export async function toggleLike(postId: string, isLiked: boolean) {
  if (isLiked) {
    await unlikePost(postId);
  } else {
    await likePost(postId);
  }
}

Phase 4: Validate

After generating all files:

  1. Check TypeScript compilation: npx tsc --noEmit
  2. Verify no broken imports
  3. Ensure all "use client" directives are only on interactive components
  4. Confirm mock data variables have been removed

Component-to-API Quick Reference

ComponentPrimary API calls
home.tsxlistPosts, listCategories, getUser
post-list.tsxlistPosts (type=team), listPosts (type=general)
post-detail.tsxgetPost, getUser, listPostComments, listPostResources, listPostRelated
proposal-list.tsxlistPosts (type=for_category), listCategories
proposal-detail.tsxgetPost, getUser, listPostComments, listPostRatings, listPostRelated, listPostResources
category-detail.tsxgetCategory, listCategoryRules, listCategoryPosts, listCategoryGroups
user-profile.tsxgetUser, listPosts (filtered by user), listResources
team.tsxgetGroup, listGroupMembers, getUser (per member)
assets.tsxlistResources
following-list.tsxlistUsers (API gap: no follow relationship endpoint)

API Gaps

These frontend features have no matching API endpoint. Skip integration for these areas and leave mock data with a // TODO: API gap comment:

  • Follow/unfollow relationships
  • Search
  • Notifications
  • Favorites/bookmarks
  • Post version history
  • Resource tags, availability, deadline fields
  • Group avatar
  • Posts filtered by group
  • User statistics aggregation
  • Category-to-category relationships