A guide to organizing frontend code using Feature-Sliced Design (FSD) for better modularity.

Install

mkdir -p .claude/skills/webdev && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/19068" && unzip -o skill.zip -d .claude/skills/webdev && rm skill.zip

Installs to .claude/skills/webdev

Activation

This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.

Guidelines for TypeScript, React, and Next.js development.
58 chars · catalog descriptionno explicit “when” trigger
Advanced

Key capabilities

  • Organize frontend code using Feature-Sliced Design (FSD) layers
  • Enforce import restrictions between FSD layers
  • Partition code into slices by business domain within layers
  • Organize code within slices using segments for technical purpose
  • Export functionality through public API `index.ts` barrel files
  • Use ES module syntax for TypeScript imports and exports

How it works

This skill provides guidelines for organizing frontend code using Feature-Sliced Design, which structures code into layers, slices, and segments based on business domain and technical purpose. It enforces strict import rules between these architectural components.

Inputs & outputs

You give it
Frontend code for TypeScript, React, and Next.js
You get back
Organized codebase following FSD principles with enforced architectural boundaries

When to use webdev

  • Structuring new frontend modules
  • Refactoring code to FSD standards
  • Enforcing architecture boundaries

About this skill

Web Development Skill

Guidelines for TypeScript, React, and Next.js development.

Code Organization: Feature-Sliced Design

All frontend code must follow Feature-Sliced Design (FSD) — an architectural methodology that organizes code by business domain rather than technical concerns.

Layers

FSD defines standardized layers, ordered top to bottom. Modules may only import from layers strictly below them. No lateral imports between modules on the same layer.

LayerPurposeExamples
appRuntime essentials: routing, entry points, global styles, providersapp/layout.tsx, app/providers.tsx
pagesFull pages or major routed viewspages/dashboard, pages/calculator-editor
widgetsLarge self-contained UI blocks composed of features and entitieswidgets/calculator-preview, widgets/submission-list
featuresReusable product feature implementations that deliver business valuefeatures/create-calculator, features/apply-theme
entitiesBusiness domain objects — data models, their UI representations, and API callsentities/calculator, entities/submission, entities/user
sharedReusable, project-agnostic code: UI kit, utilities, API client, configshared/ui, shared/lib, shared/api

Slices

Within each layer (except app and shared), code is partitioned into slices by business domain. Each slice is a folder named after its domain concept.

Critical rule: Slices on the same layer cannot import from each other. This enforces low coupling.

features/
├── create-calculator/    # One slice
├── apply-theme/          # Another slice — cannot import from create-calculator
└── configure-formula/    # Another slice — cannot import from siblings

Segments

Each slice contains segments that organize code by technical purpose:

SegmentContents
uiReact components, styles
modelTypeScript types/interfaces, stores, business logic
apiBackend interaction, data fetching, request/response types
libInternal utilities specific to this slice
configConstants, feature flags, configuration

Public API

Every slice must export through an index.ts barrel file at its root. External consumers import only from this public API, never from internal segment files.

// features/create-calculator/index.ts  — public API
export { CreateCalculatorForm } from './ui/CreateCalculatorForm';
export { useCreateCalculator } from './model/useCreateCalculator';
export type { CreateCalculatorParams } from './model/types';

// ✅ Good — import from the slice's public API
import { CreateCalculatorForm } from '@/features/create-calculator';

// ❌ Bad — reaching into internal segments
import { CreateCalculatorForm } from '@/features/create-calculator/ui/CreateCalculatorForm';

Example Project Structure

src/
├── app/                          # App layer: Next.js routing, providers, global styles
│   ├── layout.tsx
│   ├── providers.tsx
│   ├── globals.css
│   ├── dashboard/
│   │   └── page.tsx
│   └── editor/[id]/
│       └── page.tsx
│
├── pages/                        # Pages layer: page-level compositions
│   ├── dashboard/
│   │   ├── ui/
│   │   ├── model/
│   │   └── index.ts
│   └── calculator-editor/
│       ├── ui/
│       ├── model/
│       └── index.ts
│
├── widgets/                      # Widgets layer: self-contained UI blocks
│   ├── calculator-preview/
│   │   ├── ui/
│   │   ├── model/
│   │   └── index.ts
│   └── submission-table/
│       ├── ui/
│       ├── model/
│       └── index.ts
│
├── features/                     # Features layer: user-facing capabilities
│   ├── create-calculator/
│   │   ├── ui/
│   │   ├── model/
│   │   ├── api/
│   │   └── index.ts
│   ├── configure-formula/
│   │   ├── ui/
│   │   ├── model/
│   │   └── index.ts
│   └── apply-theme/
│       ├── ui/
│       ├── model/
│       └── index.ts
│
├── entities/                     # Entities layer: business domain objects
│   ├── calculator/
│   │   ├── ui/
│   │   ├── model/
│   │   ├── api/
│   │   └── index.ts
│   ├── submission/
│   │   ├── ui/
│   │   ├── model/
│   │   ├── api/
│   │   └── index.ts
│   └── user/
│       ├── model/
│       ├── api/
│       └── index.ts
│
└── shared/                       # Shared layer: project-agnostic reusable code
    ├── ui/                       # UI kit (buttons, inputs, modals)
    ├── api/                      # API client, interceptors
    ├── lib/                      # Utilities (formatting, validation)
    └── config/                   # Constants, env config

Import Rules Summary

app       → pages, widgets, features, entities, shared
pages     → widgets, features, entities, shared
widgets   → features, entities, shared
features  → entities, shared
entities  → shared
shared    → (external packages only)

Violations of these import rules indicate an architectural problem. Fix the dependency direction rather than working around it.

TypeScript

ES Modules

Always use ES module syntax (import/export) instead of CommonJS (require/module.exports).

Bad - CommonJS:

const nextJest = require('next/jest');
module.exports = config;

Good - ES Modules:

import nextJest from 'next/jest.js';
export default config;

Important Notes:

  • When importing from npm packages in ESM mode, you may need to include the .js extension (e.g., 'next/jest.js')
  • Use import type for TypeScript types to enable type-only imports
  • Use named exports for utilities, default exports for components/configs

Never Use any Types

The any type defeats the purpose of TypeScript and should be avoided. Use proper type assertions or type guards instead.

Bad:

const result = convertFormat(input, options as any, output as any);

Good:

import type { ParseOptions, FormatOptions } from "./types";

const inputOptions: ParseOptions = { format: format as DateTimeFormat, customFormat };
const outputOptions: FormatOptions = { format: format as DateTimeFormat };
const result = convertFormat(input, inputOptions, outputOptions);

When you need type assertions:

  • Use specific type assertions: value as string, value as DateTimeFormat
  • Import and use the actual types from their definitions
  • Document why the assertion is safe with a comment if non-obvious

Alternatives to any:

  • unknown - for truly unknown types (forces type checking before use)
  • Union types - string | number | boolean
  • Generic types - <T> with constraints
  • Type guards - if (typeof x === 'string')

Component Props Typing

Use interfaces or types for component props:

interface MessageProps {
  /** The message content to display */
  content: string;
  /** Whether the message is from the current user */
  isOwn: boolean;
  /** Timestamp when the message was sent */
  timestamp: Date;
  /** Optional callback when message is clicked */
  onClick?: (messageId: string) => void;
}

function Message({ content, isOwn, timestamp, onClick }: MessageProps) {
  return (
    <div onClick={() => onClick?.(content)}>
      <p>{content}</p>
      <time>{timestamp.toISOString()}</time>
    </div>
  );
}

Hook Typing Patterns

useState - Let TypeScript infer when possible:

// Inferred as boolean
const [isOpen, setIsOpen] = useState(false);

// Union types need explicit typing
type Status = "idle" | "loading" | "success" | "error";
const [status, setStatus] = useState<Status>("idle");

// Discriminated unions for complex state
type RequestState =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: Message[] }
  | { status: 'error'; error: Error };
const [state, setState] = useState<RequestState>({ status: 'idle' });

useReducer - Type actions as discriminated unions:

interface State {
  messages: Message[];
  status: Status;
}

type Action =
  | { type: "messages_loaded"; messages: Message[] }
  | { type: "message_added"; message: Message }
  | { type: "message_deleted"; messageId: string }
  | { type: "status_changed"; status: Status };

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case "messages_loaded":
      return { ...state, messages: action.messages, status: "success" };
    case "message_added":
      return { ...state, messages: [...state.messages, action.message] };
    case "message_deleted":
      return {
        ...state,
        messages: state.messages.filter(m => m.id !== action.messageId)
      };
    case "status_changed":
      return { ...state, status: action.status };
  }
}

useContext - Handle nullable contexts:

type User = { id: string; name: string; role: string };
const UserContext = createContext<User | null>(null);

function useUser() {
  const user = useContext(UserContext);
  if (!user) {
    throw new Error("useUser must be used within UserProvider");
  }
  return user;
}

Event handlers:

function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
  setValue(event.currentTarget.value);
}

function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
  event.preventDefault();
  // ...
}

function handleClick(event: React.MouseEvent<HTMLButtonElement>) {
  // ...
}

Common React types:

interface ComponentProps {
  children: React.ReactNode;        // Any valid React child
  style?: React.CSSProperties;      // Inline styles
  className?: string;
  onClick?: React.MouseEventHandler<HTMLButtonElement>;
}

React State Management

State Structure Principles

1. Group related state

// ❌ Bad - separate state that updates together
const [x, setX] = useState(0);
const [y, setY] = useState(0);

// ✅ Good - group 

---

*Content truncated.*

When not to use it

  • When organizing code by technical concerns instead of business domain
  • When importing laterally between modules on the same FSD layer
  • When using CommonJS syntax for TypeScript

Limitations

  • Modules may only import from layers strictly below them
  • Slices on the same layer cannot import from each other
  • External consumers must import only from a slice's public API, never from internal segment files

How it compares

This workflow uses Feature-Sliced Design to organize code by business domain with strict import rules, unlike a generic approach that might organize code by technical type or allow unrestricted imports.

Compared to similar skills

webdev side by side with the closest alternatives in the catalog.

SkillInstallsUpdatedSafetyDifficulty
webdev (this skill)05moNo flagsAdvanced
nextjs-best-practices316moNo flagsIntermediate
project-overview151moNo flagsBeginner
nx-generate16moReviewBeginner

Try saying

Example prompts that trigger this skill in your AI assistant.

You might also like

nextjs-best-practices

davila7

Next.js App Router principles. Server Components, data fetching, routing patterns.

3164

project-overview

lobehub

Complete project architecture and structure guide. Use when exploring the codebase, understanding project organization, finding files, or needing comprehensive architectural context. Triggers on architecture questions, directory navigation, or project overview needs.

1548

nx-generate

nrwl

Generate code using nx generators. USE WHEN scaffolding code or transforming existing code - for example creating libraries or applications, or anything else that is boilerplate code or automates repetitive tasks. ALWAYS use this first when generating code with Nx instead of calling MCP tools or running nx generate immediately.

111

javascript-typescript-typescript-scaffold

sickn33

You are a TypeScript project architecture expert specializing in scaffolding production-ready Node.js and frontend applications. Generate complete project structures with modern tooling (pnpm, Vite, N

31

server-components

davepoon

This skill should be used when the user asks about "Server Components", "Client Components", "'use client' directive", "when to use server vs client", "RSC patterns", "component composition", "data fetching in components", or needs guidance on React Server Components architecture in Next.js.

13

supabase-architecture-variants

jeremylongshore

Execute choose and implement Supabase validated architecture blueprints for different scales. Use when designing new Supabase integrations, choosing between monolith/service/microservice architectures, or planning migration paths for Supabase applications. Trigger with phrases like "supabase architecture", "supabase blueprint", "how to structure supabase", "supabase project layout", "supabase microservice".

02

Search skills

Search the agent skills registry