← スキル一覧に戻る

cyberk-fsd-fe
by cyberk-dev
⭐ 1🍴 0📅 2026年1月18日
SKILL.md
name: cyberk-fsd-fe description: Build Next.js applications using Feature Sliced Design (FSD) methodology. Organizes code into layers (app, screens, widgets, features, entities, shared) with clear separation - entities for data fetching/search, features for mutations. Uses React Query for data fetching, Zustand for state management. Use when creating Next.js apps with FSD architecture, organizing React code by business domain, or implementing scalable frontend structure.
CyberK FSD Frontend
Next.js + Feature Sliced Design with React Query patterns.
When to Use This Skill
- Building Next.js applications with scalable architecture
- Organizing React code by business domain
- Implementing data fetching with React Query (queries + mutations)
- Setting up FSD layer structure (app, screens, widgets, features, entities, shared)
- Creating reusable entity components with query factories
- Implementing mutation features with proper cache invalidation
Core Principles
- Entities: GET/SEARCH operations + Query Factory (DTOs from swagger)
- Features: MUTATIONS only + Mutation Hooks
- Import Rule: Only import from layers below via their
index.tsexports - Types: DTOs/entities generated from swagger →
ApiTypesin@/shared/api - Note: We use
screens/instead ofpages/to avoid conflict with Next.jspages/directory
Project Structure
src/
├── app/ # Next.js routing shell
├── screens/ # FSD page composition (named screens to avoid Next.js conflict)
├── widgets/ # Large UI blocks
├── features/ # MUTATIONS (useMutation hooks)
│ └── create-post/
│ ├── api/
│ │ └── use-create-post.ts
│ ├── ui/
│ └── index.ts
├── entities/ # GET/SEARCH (Query Factory)
│ └── post/
│ ├── api/
│ │ ├── post-api.ts # All CRUD calls
│ │ └── post.queries.ts # Query factory
│ ├── model/
│ │ ├── types.ts # Custom domain models (if needed)
│ │ └── store.ts # Zustand store (optional)
│ ├── ui/
│ └── index.ts
└── shared/
├── api/
│ ├── client.ts # Axios instance
│ └── query-client.ts # React Query client
├── ui/
└── lib/
Layer Import Rules
app/ → screens, widgets, features, entities, shared (via index.ts)
screens/ → widgets, features, entities, shared (via index.ts)
widgets/ → features, entities, shared (via index.ts)
features/ → entities, shared (via index.ts)
entities/ → shared (via index.ts)
shared/ → (nothing)
Rules:
- Never import from same layer!
- Only import what's exported from
index.tsof lower layers
Types from Swagger
DTOs and entity types are auto-generated from backend swagger API:
// Import types from swagger
import { ApiTypes } from "@/shared/api";
// Use directly
type Post = ApiTypes.PostDto;
type CreatePostRequest = ApiTypes.CreatePostRequest;
Only create custom types in entities/{name}/model/ when you need:
- Transformed/enriched domain models
- Frontend-specific computed properties
- Different structure than backend DTO
Entity Pattern (Query Factory)
1. API Layer (post-api.ts)
// entities/post/api/post-api.ts
import { apiClient, ApiTypes } from "@/shared/api";
export const postApi = {
getAll: async (filter?: ApiTypes.PostsFilter): Promise<ApiTypes.PostDto[]> => {
const { data } = await apiClient.get<ApiTypes.PostDto[]>("/posts", { params: filter });
return data;
},
getById: async (id: string): Promise<ApiTypes.PostDto> => {
const { data } = await apiClient.get<ApiTypes.PostDto>(`/posts/${id}`);
return data;
},
create: (data: ApiTypes.CreatePostRequest) => apiClient.post<ApiTypes.PostDto>("/posts", data),
update: (id: string, data: ApiTypes.UpdatePostRequest) => apiClient.patch<ApiTypes.PostDto>(`/posts/${id}`, data),
delete: (id: string) => apiClient.delete(`/posts/${id}`),
};
2. Query Factory (post.queries.ts)
// entities/post/api/post.queries.ts
import { queryOptions, keepPreviousData } from "@tanstack/react-query";
import { postApi } from "./post-api";
export const postQueries = {
all: () => ["posts"],
lists: () => [...postQueries.all(), "list"],
list: (filter?: PostsFilter) =>
queryOptions({
queryKey: [...postQueries.lists(), filter],
queryFn: () => postApi.getAll(filter),
placeholderData: keepPreviousData,
}),
details: () => [...postQueries.all(), "detail"],
detail: (id: string) =>
queryOptions({
queryKey: [...postQueries.details(), id],
queryFn: () => postApi.getById(id),
staleTime: 5000,
}),
};
3. Model Layer (Optional - only if custom types needed)
// entities/post/model/types.ts (only when different from API)
import { ApiTypes } from "@/shared/api";
export interface PostWithMeta extends ApiTypes.PostDto {
isBookmarked: boolean; // Frontend-only property
formattedDate: string; // Computed property
}
4. Zustand Store (Optional)
// entities/post/model/store.ts
import { create } from "zustand";
interface PostStore {
selectedPostId: string | null;
setSelectedPost: (id: string | null) => void;
}
export const usePostStore = create<PostStore>((set) => ({
selectedPostId: null,
setSelectedPost: (id) => set({ selectedPostId: id }),
}));
5. Usage in Components
import { useQuery } from "@tanstack/react-query";
import { postQueries } from "@/entities/post";
const { data: posts } = useQuery(postQueries.list({ page: 1 }));
const { data: post } = useQuery(postQueries.detail(id));
5. Public API (index.ts)
// entities/post/index.ts
export { postApi } from "./api/post-api";
export { postQueries } from "./api/post.queries";
export { PostCard } from "./ui/post-card";
export { usePostStore } from "./model/store"; // if exists
export type { PostWithMeta } from "./model/types"; // only custom types
Feature Pattern (Mutations)
Mutation Hook
// features/create-post/api/use-create-post.ts
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { postApi, postQueries } from "@/entities/post";
export const useCreatePost = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: postApi.create,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: postQueries.lists() });
},
});
};
Feature UI
// features/create-post/ui/create-post-form.tsx
"use client";
import { useCreatePost } from "../api/use-create-post";
export function CreatePostForm({ onSuccess }: { onSuccess?: () => void }) {
const { mutateAsync, isPending } = useCreatePost();
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
await mutateAsync({ title: formData.get("title") as string });
onSuccess?.();
};
return (
<form onSubmit={handleSubmit}>
<input name="title" required />
<button disabled={isPending}>Create</button>
</form>
);
}
Shared Layer
API Client (Axios)
// shared/api/client.ts
import axios from "axios";
export const apiClient = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL,
headers: { "Content-Type": "application/json" },
});
Query Client
// shared/api/query-client.ts
import { QueryClient } from "@tanstack/react-query";
export const queryClient = new QueryClient({
defaultOptions: {
queries: { staleTime: 5 * 60 * 1000 },
},
});
Cross-Slice UI (Slots Pattern)
// entities/post/ui/post-card.tsx
interface PostCardProps {
post: Post;
authorSlot?: React.ReactNode;
actionsSlot?: React.ReactNode;
}
export function PostCard({ post, authorSlot, actionsSlot }: PostCardProps) {
return (
<Card>
{authorSlot}
<h3>{post.title}</h3>
{actionsSlot}
</Card>
);
}
// Usage in screens/
<PostCard
post={post}
authorSlot={<UserAvatar userId={post.authorId} />}
actionsSlot={<LikeButton postId={post.id} />}
/>;
Decision Guide
"Where should I put this code?"
| Scenario | Layer | Reason |
|---|---|---|
| API client, axios instance | shared/api/ | Infrastructure, no business logic |
| Reusable Button, Input | shared/ui/ | Generic UI kit |
| User, Post, Product data | entities/{name}/ | Business domain entity |
| GET/Search data + Query factory | entities/{name}/api/ | Read operations belong to entities |
| Create/Update/Delete action | features/{action}-{entity}/ | Mutations are features |
| Action used on ONE page only | screens/{page}/ | No need for feature if not reused |
| Large reused UI block | widgets/{name}/ | Reused across pages |
| UI block for ONE page only | screens/{page}/ui/ | Keep in page, not widget |
| Page composition | screens/{name}/ | Combine widgets, features, entities |
| Route definition | app/ | Next.js routing shell |
"Should I create a feature?"
- ✅ YES if: Reused on 2+ pages
- ❌ NO if: Only used on 1 page → keep in page slice
"Should I create a widget?"
- ✅ YES if: Reused on 2+ pages OR page has multiple independent large blocks
- ❌ NO if: Only used on 1 page → keep in page slice
"Entity A needs Entity B data?"
- Prefer: Pass data via props from higher layer
- If must: Use
@xnotation (entities/A/@x/B.ts)
Quick Checklist
New Entity:
- Create
api/{entity}-api.tswith CRUD (useApiTypesfrom swagger) - Create
api/{entity}.queries.tswith query factory - Create
model/types.tsONLY if custom domain models needed - Create
model/store.tsfor Zustand store (optional) - Export via
index.ts
New Feature:
- Create
api/use-{action}-{entity}.tsmutation hook - Import from entity's
index.tsonly (postApi, postQueries) - Invalidate queries on success
- Create UI components
Reference Navigation
FSD Architecture:
- FSD Layers - Layer hierarchy and patterns
- FSD Segments - Segment organization (api/, ui/, model/)
- FSD Import Rules - Dependency management
Next.js Integration:
- App Router Architecture - Routing, layouts, pages
- Server Components - RSC patterns, streaming
- Optimization - Images, fonts, bundles
UI Components:
- RemixIcon Integration - Icons usage
Resources
- Feature Sliced Design: https://feature-sliced.design
- Next.js App Router: https://nextjs.org/docs
- React Query: https://tanstack.com/query
- Zustand: https://zustand-demo.pmnd.rs
スコア
総合スコア
50/100
リポジトリの品質指標に基づく評価
✓SKILL.md
SKILL.mdファイルが含まれている
+20
○LICENSE
ライセンスが設定されている
0/10
○説明文
100文字以上の説明がある
0/10
○人気
GitHub Stars 100以上
0/15
○最近の活動
3ヶ月以内に更新がある
0/10
○フォーク
10回以上フォークされている
0/5
✓Issue管理
オープンIssueが50未満
+5
✓言語
プログラミング言語が設定されている
+5
○タグ
1つ以上のタグが設定されている
0/5
レビュー
💬
レビュー機能は近日公開予定です