fe-api
3
总安装量
3
周安装量
#54898
全站排名
安装命令
npx skills add https://github.com/ingpdw/pdw-fe-dev-tool --skill fe-api
Agent 安装分布
cline
3
gemini-cli
3
github-copilot
3
codex
3
cursor
3
opencode
3
Skill 文档
FE API Integration
$ARGUMENTS를 ë¶ìíì¬ API íµí© ë ì´ì´ë¥¼ ì¤ê³íê±°ë ê°ì íë¤.
ë¶ì ì ì°¨
- ì구ì¬í íì : API ìëí¬ì¸í¸, ë°ì´í° 구조, ì¬ì© í¨í´ì íì¸íë¤
- 기존 ì½ë ë¶ì: íë¡ì í¸ì API ë ì´ì´ 구조를 Glob/Readë¡ íì íë¤
- í¨í´ ì ì: ìµì ì ë°ì´í° íì¹ ì ëµì ì ìíë¤
- 구í/ê°ì : ì¹ì¸ í ì½ë를 ìì±íê±°ë ê°ì íë¤
API í´ë¼ì´ì¸í¸ ì¤ê³
íì ìì í Fetch Wrapper
// src/lib/api.ts
import { z } from "zod";
class ApiError extends Error {
constructor(
public status: number,
public statusText: string,
public data?: unknown
) {
super(`API Error: ${status} ${statusText}`);
this.name = "ApiError";
}
}
async function fetchApi<T>(
url: string,
schema: z.ZodType<T>,
options?: RequestInit
): Promise<T> {
const response = await fetch(url, {
headers: { "Content-Type": "application/json", ...options?.headers },
...options,
});
if (!response.ok) {
throw new ApiError(response.status, response.statusText);
}
const data = await response.json();
return schema.parse(data);
}
export { fetchApi, ApiError };
API ìëí¬ì¸í¸ ì ì
// src/lib/api/users.ts
import { z } from "zod";
import { fetchApi } from "@/lib/api";
const userSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
role: z.enum(["admin", "user"]),
});
const usersResponseSchema = z.object({
data: z.array(userSchema),
total: z.number(),
});
type User = z.infer<typeof userSchema>;
async function getUsers(params?: { page?: number; limit?: number }) {
const searchParams = new URLSearchParams();
if (params?.page) searchParams.set("page", String(params.page));
if (params?.limit) searchParams.set("limit", String(params.limit));
return fetchApi(`/api/users?${searchParams}`, usersResponseSchema);
}
async function getUser(id: string) {
return fetchApi(`/api/users/${id}`, userSchema);
}
async function createUser(data: Omit<User, "id">) {
return fetchApi(`/api/users`, userSchema, {
method: "POST",
body: JSON.stringify(data),
});
}
export { getUsers, getUser, createUser };
export type { User };
TanStack Query í¨í´
Query Key ê´ë¦¬
// src/lib/queryKeys.ts
const queryKeys = {
users: {
all: ["users"] as const,
lists: () => [...queryKeys.users.all, "list"] as const,
list: (filters: Record<string, unknown>) =>
[...queryKeys.users.lists(), filters] as const,
details: () => [...queryKeys.users.all, "detail"] as const,
detail: (id: string) => [...queryKeys.users.details(), id] as const,
},
products: {
all: ["products"] as const,
lists: () => [...queryKeys.products.all, "list"] as const,
list: (filters: Record<string, unknown>) =>
[...queryKeys.products.lists(), filters] as const,
details: () => [...queryKeys.products.all, "detail"] as const,
detail: (id: string) => [...queryKeys.products.details(), id] as const,
},
} as const;
export { queryKeys };
Query Hook
// src/hooks/useUsers.ts
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { getUsers, getUser, createUser } from "@/lib/api/users";
import { queryKeys } from "@/lib/queryKeys";
function useUsers(filters?: { page?: number; limit?: number }) {
return useQuery({
queryKey: queryKeys.users.list(filters ?? {}),
queryFn: () => getUsers(filters),
});
}
function useUser(id: string) {
return useQuery({
queryKey: queryKeys.users.detail(id),
queryFn: () => getUser(id),
enabled: !!id,
});
}
function useCreateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: createUser,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.users.lists() });
},
});
}
export { useUsers, useUser, useCreateUser };
Optimistic Update
function useUpdateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: updateUser,
onMutate: async (newUser) => {
await queryClient.cancelQueries({
queryKey: queryKeys.users.detail(newUser.id),
});
const previousUser = queryClient.getQueryData(
queryKeys.users.detail(newUser.id)
);
queryClient.setQueryData(
queryKeys.users.detail(newUser.id),
newUser
);
return { previousUser };
},
onError: (_err, newUser, context) => {
queryClient.setQueryData(
queryKeys.users.detail(newUser.id),
context?.previousUser
);
},
onSettled: (_data, _err, newUser) => {
queryClient.invalidateQueries({
queryKey: queryKeys.users.detail(newUser.id),
});
},
});
}
Infinite Query (무í ì¤í¬ë¡¤)
function useInfiniteUsers() {
return useInfiniteQuery({
queryKey: queryKeys.users.lists(),
queryFn: ({ pageParam }) => getUsers({ page: pageParam, limit: 20 }),
initialPageParam: 1,
getNextPageParam: (lastPage, allPages) => {
const totalFetched = allPages.reduce((sum, p) => sum + p.data.length, 0);
return totalFetched < lastPage.total ? allPages.length + 1 : undefined;
},
});
}
Prefetching
// Server Componentìì prefetch
import { dehydrate, HydrationBoundary, QueryClient } from "@tanstack/react-query";
export default async function UsersPage() {
const queryClient = new QueryClient();
await queryClient.prefetchQuery({
queryKey: queryKeys.users.list({}),
queryFn: () => getUsers(),
});
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<UserList />
</HydrationBoundary>
);
}
Server Actions í¨í´
기본 Server Action
// src/app/actions/users.ts
"use server";
import { revalidatePath } from "next/cache";
import { z } from "zod";
const createUserSchema = z.object({
name: z.string().min(1, "ì´ë¦ì ì
ë ¥íì¸ì"),
email: z.string().email("ì í¨í ì´ë©ì¼ì ì
ë ¥íì¸ì"),
});
interface ActionState {
success: boolean;
message: string;
errors?: Record<string, string[]>;
}
async function createUserAction(
_prevState: ActionState,
formData: FormData
): Promise<ActionState> {
const raw = {
name: formData.get("name"),
email: formData.get("email"),
};
const result = createUserSchema.safeParse(raw);
if (!result.success) {
return {
success: false,
message: "ì í¨ì± ê²ì¬ ì¤í¨",
errors: result.error.flatten().fieldErrors,
};
}
try {
await db.user.create({ data: result.data });
revalidatePath("/users");
return { success: true, message: "ì¬ì©ìê° ìì±ëììµëë¤" };
} catch (error) {
return { success: false, message: "ìë² ì¤ë¥ê° ë°ìíìµëë¤" };
}
}
export { createUserAction };
export type { ActionState };
useActionStateë¡ í¼ ì°ë
"use client";
import { useActionState } from "react";
import { createUserAction } from "@/app/actions/users";
import type { ActionState } from "@/app/actions/users";
const initialState: ActionState = { success: false, message: "" };
function CreateUserForm() {
const [state, formAction, isPending] = useActionState(
createUserAction,
initialState
);
return (
<form action={formAction}>
<Input name="name" placeholder="ì´ë¦" />
{state.errors?.name && (
<p className="text-sm text-destructive">{state.errors.name[0]}</p>
)}
<Input name="email" placeholder="ì´ë©ì¼" type="email" />
{state.errors?.email && (
<p className="text-sm text-destructive">{state.errors.email[0]}</p>
)}
<Button type="submit" disabled={isPending}>
{isPending ? "ìì± ì¤..." : "ìì±"}
</Button>
{state.message && (
<p className={state.success ? "text-green-600" : "text-destructive"}>
{state.message}
</p>
)}
</form>
);
}
API ìë¬ í¸ë¤ë§
ì ì ìë¬ í¸ë¤ë§ (QueryClient)
// src/lib/queryClient.ts
import { QueryClient } from "@tanstack/react-query";
import { ApiError } from "@/lib/api";
function makeQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000,
retry: (failureCount, error) => {
if (error instanceof ApiError && error.status === 401) return false;
if (error instanceof ApiError && error.status === 404) return false;
return failureCount < 3;
},
},
mutations: {
onError: (error) => {
if (error instanceof ApiError && error.status === 401) {
window.location.href = "/login";
}
},
},
},
});
}
export { makeQueryClient };
ì»´í¬ëí¸ ë 벨 ìë¬ ì²ë¦¬
function UserProfile({ id }: { id: string }) {
const { data, error, isLoading } = useUser(id);
if (isLoading) return <Skeleton className="h-40 w-full" />;
if (error) {
if (error instanceof ApiError && error.status === 404) {
return <p>ì¬ì©ì를 ì°¾ì ì ììµëë¤.</p>;
}
return <p>ë°ì´í°ë¥¼ ë¶ë¬ì¤ë ì¤ ì¤ë¥ê° ë°ìíìµëë¤.</p>;
}
return <div>{data.name}</div>;
}
Route Handler (API Route)
CRUD Route Handler
// src/app/api/users/route.ts
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
const createUserSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
});
export async function GET(request: NextRequest) {
const { searchParams } = request.nextUrl;
const page = Number(searchParams.get("page") ?? "1");
const limit = Number(searchParams.get("limit") ?? "20");
const [users, total] = await Promise.all([
db.user.findMany({ skip: (page - 1) * limit, take: limit }),
db.user.count(),
]);
return NextResponse.json({ data: users, total });
}
export async function POST(request: NextRequest) {
const body = await request.json();
const result = createUserSchema.safeParse(body);
if (!result.success) {
return NextResponse.json(
{ error: "Validation failed", details: result.error.flatten() },
{ status: 400 }
);
}
const user = await db.user.create({ data: result.data });
return NextResponse.json(user, { status: 201 });
}
ëì Route Handler
// src/app/api/users/[id]/route.ts
import { NextRequest, NextResponse } from "next/server";
export async function GET(
_request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params;
const user = await db.user.findUnique({ where: { id } });
if (!user) {
return NextResponse.json({ error: "Not found" }, { status: 404 });
}
return NextResponse.json(user);
}
export async function PATCH(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params;
const body = await request.json();
const user = await db.user.update({ where: { id }, data: body });
return NextResponse.json(user);
}
export async function DELETE(
_request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params;
await db.user.delete({ where: { id } });
return new NextResponse(null, { status: 204 });
}
MSW ê°ë°ì© Mock ìë²
// src/mocks/handlers.ts
import { http, HttpResponse } from "msw";
const users = [
{ id: "1", name: "Alice", email: "alice@example.com", role: "admin" },
{ id: "2", name: "Bob", email: "bob@example.com", role: "user" },
];
export const handlers = [
http.get("/api/users", ({ request }) => {
const url = new URL(request.url);
const page = Number(url.searchParams.get("page") ?? "1");
const limit = Number(url.searchParams.get("limit") ?? "20");
const start = (page - 1) * limit;
return HttpResponse.json({
data: users.slice(start, start + limit),
total: users.length,
});
}),
http.post("/api/users", async ({ request }) => {
const body = await request.json();
const newUser = { id: String(users.length + 1), ...body };
users.push(newUser);
return HttpResponse.json(newUser, { status: 201 });
}),
];
ì¤í ê·ì¹
- ì¸ìê° ìì¼ë©´ ì¬ì©ììê² API íµí© ëìì ì§ë¬¸íë¤
- íë¡ì í¸ì 기존 API ë ì´ì´ë¥¼ 먼ì íì íë¤ (lib/api, hooks, actions ë±)
- TanStack Query ì¬ì© ì¬ë¶ë¥¼ íì¸íê³ , 미ì¤ì¹ ì ì¤ì¹ë¥¼ ìë´íë¤
- Zod ì¤í¤ë§ë¡ API ìëµ íì ì ê²ì¦íë í¨í´ì 기본ì¼ë¡ ì ì©íë¤
- Server Componentììì ë°ì´í° íì¹ê³¼ Client Componentììì TanStack Query를 구ë¶íë¤
- ìë¬ í¸ë¤ë§ì ë°ëì í¬í¨íë¤ (ë¤í¸ìí¬ ìë¬, ì í¨ì± ìë¬, ìë² ìë¬)