Agent skill
target-fe-architecture
Target frontend architecture for Lingx. Progressive FSD Hybrid combining Next.js App Router with Feature-Sliced Design layers. Use when implementing frontend features, reviewing code, or making architectural decisions.
Stars
163
Forks
31
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/data/target-fe-architecture
SKILL.md
Lingx Frontend Architecture
Progressive FSD Hybrid: Next.js App Router + FSD layers when complexity grows.
Why Progressive FSD?
| Challenge | Solution |
|---|---|
| Simple pages become complex | Migrate to widgets/features when needed |
| Shared components scattered | Organize by business domain (entities) |
| Feature coupling | Strict layer imports (shared → entities → features → widgets) |
| App Router integration | Keep pages thin, compose FSD components |
Directory Structure
apps/web/src/
├── app/ # Next.js App Router (thin pages)
│ ├── (auth)/ # Auth route group
│ ├── (dashboard)/ # Dashboard route group
│ ├── (project)/ # Project route group
│ └── workbench/ # Translation editor
├── widgets/ # Complex UI blocks (when needed)
│ └── translation-editor/ # Real-time collaborative editor
├── features/ # User actions (when needed)
│ └── ai-translate/ # AI translation feature
├── entities/ # Business entities (when needed)
│ └── project/ # Project card, list item
├── shared/ # Always use
│ ├── ui/ # shadcn/ui components
│ ├── api/ # API client, React Query hooks
│ ├── lib/ # Utilities (cn, formatters)
│ └── hooks/ # Shared hooks
└── components/ # Legacy (migrate over time)
Layer Hierarchy
┌─────────────────────────────────────────────────────────────┐
│ app/ │
│ Pages compose widgets and features, fetch data on server │
└─────────────────────┬───────────────────────────────────────┘
│ imports
┌─────────────────────▼───────────────────────────────────────┐
│ widgets/ │
│ Complex UI blocks with internal state and multiple features │
└─────────────────────┬───────────────────────────────────────┘
│ imports
┌─────────────────────▼───────────────────────────────────────┐
│ features/ │
│ User actions: forms, modals, buttons with side effects │
└─────────────────────┬───────────────────────────────────────┘
│ imports
┌─────────────────────▼───────────────────────────────────────┐
│ entities/ │
│ Business objects: cards, list items, viewers │
└─────────────────────┬───────────────────────────────────────┘
│ imports
┌─────────────────────▼───────────────────────────────────────┐
│ shared/ │
│ UI primitives, utilities, API client - no business logic │
└─────────────────────────────────────────────────────────────┘
Import Rule: Lower layers cannot import from higher layers.
When to Use Each Layer
| Complexity | Location | Example |
|---|---|---|
| Simple page-specific | app/[route]/_components/ |
Dashboard stats card |
| Shared UI primitive | shared/ui/ |
Button, Input, Card |
| Business entity display | entities/ |
ProjectCard, KeyRow |
| User action | features/ |
AITranslate, BulkEdit |
| Complex widget | widgets/ |
TranslationEditor |
Documentation
| Document | Purpose |
|---|---|
| fsd-overview.md | FSD layers explained |
| migration-rules.md | When to adopt FSD layers |
| widgets.md | Widget patterns |
| features.md | Feature patterns |
| entities.md | Entity patterns |
| server-components.md | RSC patterns |
| hooks.md | Custom hooks |
| data-fetching.md | Server vs client fetching |
Quick Reference
App Router Page (Thin)
tsx
// app/(project)/projects/[id]/page.tsx
import { getProject } from '@/shared/api/projects';
import { ProjectHeader } from '@/entities/project';
import { TranslationEditor } from '@/widgets/translation-editor';
export default async function ProjectPage({ params }: Props) {
const project = await getProject(params.id);
return (
<div className="space-y-6">
<ProjectHeader project={project} />
<TranslationEditor projectId={project.id} />
</div>
);
}
Widget (Complex UI Block)
tsx
// widgets/translation-editor/ui/translation-editor.tsx
'use client';
import { KeyList } from './key-list';
import { PresenceBar } from './presence-bar';
import { useRealtimeSync } from '../model/use-realtime-sync';
import { usePresence } from '../model/use-presence';
export function TranslationEditor({ projectId }: Props) {
const { keys, updateKey } = useRealtimeSync(projectId);
const { users, focusKey } = usePresence(projectId);
return (
<div className="island">
<PresenceBar users={users} />
<KeyList keys={keys} onUpdate={updateKey} onFocus={focusKey} />
</div>
);
}
Feature (User Action)
tsx
// features/ai-translate/ui/ai-translate-button.tsx
'use client';
import { Button } from '@/shared/ui/button';
import { useAITranslate } from '../model/use-ai-translate';
export function AITranslateButton({ keyId, targetLanguages }: Props) {
const { translate, isPending } = useAITranslate();
return (
<Button onClick={() => translate({ keyId, targetLanguages })} disabled={isPending}>
{isPending ? 'Translating...' : 'AI Translate'}
</Button>
);
}
Entity (Business Object)
tsx
// entities/project/ui/project-card.tsx
import Link from 'next/link';
import type { Project } from '@lingx/shared';
interface ProjectCardProps {
project: Project;
}
export function ProjectCard({ project }: ProjectCardProps) {
return (
<Link href={`/projects/${project.id}`} className="island p-4">
<h3 className="font-medium">{project.name}</h3>
<p className="text-muted-foreground text-sm">{project.slug}</p>
</Link>
);
}
Decision Tree
Should I use FSD layer?
Is it a simple page-specific component?
└─ YES → Keep in app/[route]/_components/
Is it used across 3+ pages?
└─ YES → Consider entities/ or features/
Does it have complex internal state?
└─ YES → Consider widgets/
Is it a reusable UI primitive?
└─ YES → Put in shared/ui/
Is it a user action with side effects?
└─ YES → Put in features/
Otherwise → Start in _components/, migrate later
Real-time Collaboration Widget
widgets/translation-editor/
├── index.ts # Public API
├── ui/
│ ├── translation-editor.tsx # Main widget
│ ├── presence-bar.tsx # Who's online
│ ├── key-list.tsx # Keys with translations
│ ├── key-row.tsx # Single key
│ └── conflict-dialog.tsx # Conflict resolution
├── model/
│ ├── use-realtime-sync.ts # WebSocket sync
│ ├── use-presence.ts # Presence state
│ ├── use-optimistic-update.ts
│ └── types.ts
└── lib/
└── conflict-resolver.ts # OT/CRDT logic
Anti-Patterns
❌ Feature importing widget
tsx
// BAD - features cannot import widgets
import { TranslationEditor } from '@/widgets/translation-editor';
export function AITranslateFeature() {
return <TranslationEditor />; // ❌
}
✅ Widget using feature
tsx
// GOOD - widgets compose features
import { AITranslateButton } from '@/features/ai-translate';
export function TranslationEditor() {
return (
<div>
<AITranslateButton keyId={keyId} /> {/* ✅ */}
</div>
);
}
❌ Entity with side effects
tsx
// BAD - entities shouldn't have mutations
export function ProjectCard({ project }) {
const { mutate } = useDeleteProject(); // ❌
return (
<Card>
<button onClick={() => mutate(project.id)}>Delete</button>
</Card>
);
}
✅ Feature handles action
tsx
// GOOD - features handle mutations
// features/delete-project/ui/delete-button.tsx
export function DeleteProjectButton({ projectId }) {
const { mutate, isPending } = useDeleteProject();
return <Button onClick={() => mutate(projectId)}>Delete</Button>;
}
// entities/project/ui/project-card.tsx
// Pass render prop or slot for actions
export function ProjectCard({ project, actions }) {
return (
<Card>
<h3>{project.name}</h3>
{actions}
</Card>
);
}
Progressive Adoption
- Start simple: New pages use
_components/ - Extract when reused: Move to entities when used 3+ places
- Extract on complexity: Move to features when adding mutations
- Promote to widget: When combining multiple features with state
- Keep shared lean: Only true primitives (Button, Input, Card)
Benefits
- Clear boundaries - Each layer has one responsibility
- Predictable imports - Always know where to look
- Isolated changes - Modify one layer without affecting others
- Team scalability - Different teams own different layers
- Progressive complexity - Start simple, scale when needed
Sources:
Didn't find tool you were looking for?