agentsclimarketplace

Feature architecture

Skill LaoitdevOpen/laoitdev-skills/skills/frontend/feature-architecture

Feature folder structure and architecture for React + TypeScript admin apps. Use this when creating a new feature, adding a major section, or when asked how to organize a feature's files. Covers the canonical folder layout (components, hooks, services, types, schemas, constants, utils), what goes where, and index export conventions.From its SKILL.md

Install
npx -y skills add LaoitdevOpen/laoitdev-skills --skill feature-architecture

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

7.7 KB, ~1.8k tokens by cl100k_base, as published. Nobody here has run it

Feature Architecture

Core Invariants (always enforced — never violate)

  • Route files NEVER go inside features/ — they live exclusively in src/routes/. A feature folder contains zero route files.
  • Feature folders contain only: components/, hooks/, services/, schemas/, types/, constants/, utils/, index.ts.
  • Nothing in features/ may import from src/routes/.

Project Top-Level Structure

src/
├── assets/          # Static assets, locale JSON files
├── core/            # App-level infrastructure (axios, queryClient, router, i18n, contexts)
├── features/        # Business logic, UI components, hooks, services — one folder per domain
├── routes/          # ALL route files — file-based routing only, never inside features/
└── shared/          # Cross-feature shared components, hooks, types, utils

Canonical Feature Folder Layout

features/
└── your-feature/
    ├── components/
    │   ├── YourFeatureList.tsx        # Data table component
    │   ├── YourFeatureDetail.tsx      # Detail view
    │   ├── YourFeatureForm.tsx        # Create/Update form
    │   ├── YourFeatureFilters.tsx     # Filter accordion
    │   ├── YourFeatureFormSections/   # Form sub-sections (optional)
    │   │   ├── BasicInfoSection.tsx
    │   │   └── index.ts
    │   └── index.ts                   # Re-exports all public components
    ├── constants/
    │   └── your-feature.constants.ts  # Endpoints, query keys, option lists
    ├── hooks/
    │   ├── useYourFeatures.ts         # List query
    │   ├── useYourFeature.ts          # Single item query
    │   ├── useCreateYourFeature.ts    # Create mutation
    │   ├── useUpdateYourFeature.ts    # Update mutation
    │   ├── useDeleteYourFeature.ts    # Delete mutation
    │   └── index.ts                   # Re-exports all hooks
    ├── schemas/
    │   └── your-feature.schema.ts     # Zod schemas (search + form) — imported by route files
    ├── services/
    │   ├── your-feature.service.ts    # Axios service object
    │   └── index.ts
    ├── types/
    │   └── index.ts                   # All TypeScript types/interfaces
    ├── utils/                         # Pure utility functions (optional)
    │   └── index.ts
    └── index.ts                       # Public API of the feature

What Goes Where

types/index.ts

All TypeScript types for the feature — API response shapes, filter params, form data types.

export interface YourFeature {
  id: number
  name: string | null
  // ...
}

export interface YourFeaturesResponse {
  items: YourFeature[]
  total: number
}

export interface YourFeatureFilterParams {
  page?: number
  page_size?: number
  search?: string
  // ...
}

constants/your-feature.constants.ts

Three things: API endpoint strings, TanStack Query key factory, and option arrays.

export const YOUR_FEATURE_ENDPOINTS = {
  BASE: '/v1/admin/your-features',
  DETAIL: (id: number) => `/v1/admin/your-features/${id}`,
  UPDATE: (id: number) => `/v1/admin/your-features/${id}`,
} as const

export const YOUR_FEATURE_QUERY_KEYS = {
  all: ['your-features'] as const,
  lists: () => [...YOUR_FEATURE_QUERY_KEYS.all, 'list'] as const,
  list: (params: YourFeatureFilterParams) => [...YOUR_FEATURE_QUERY_KEYS.lists(), params] as const,
  details: () => [...YOUR_FEATURE_QUERY_KEYS.all, 'detail'] as const,
  detail: (id: number) => [...YOUR_FEATURE_QUERY_KEYS.details(), id] as const,
}

export const STATUS_OPTIONS = [
  { value: 'active', label: 'yourFeature.statusOptions.active' },
  { value: 'inactive', label: 'yourFeature.statusOptions.inactive' },
] as const

services/your-feature.service.ts

Object with async methods, each taking an optional signal?: AbortSignal for cancellation.

import axiosInstance from '@/core/api/axios.config'

export const yourFeatureService = {
  async getAll(params?: YourFeatureFilterParams, signal?: AbortSignal) {
    const { data } = await axiosInstance.get<YourFeaturesResponse>(
      YOUR_FEATURE_ENDPOINTS.BASE, { params, signal }
    )
    return data
  },
  async getById(id: number, signal?: AbortSignal) {
    const { data } = await axiosInstance.get(YOUR_FEATURE_ENDPOINTS.DETAIL(id), { signal })
    return data
  },
  async update(id: number, payload: YourFeatureUpdateRequest, signal?: AbortSignal) {
    const { data } = await axiosInstance.put(YOUR_FEATURE_ENDPOINTS.UPDATE(id), payload, { signal })
    return data
  },
}

hooks/ — one file per operation

Thin wrappers around TanStack Query. See tanstack-query skill for full patterns.

schemas/your-feature.schema.ts

Zod schemas for both search params and form validation. Search schemas are imported by route files. Form schemas use factory functions that accept t: TFunction. See tanstack-form and tanstack-router skills.

import { fallback } from '@tanstack/zod-adapter'

// Search schema — imported by route file for validateSearch
export const yourFeatureSearchSchema = z.object({
  page: fallback(z.number(), 1).default(1),
  page_size: fallback(z.number(), 20).default(20),
  search: z.string().optional(),
})
export type YourFeatureSearch = z.infer<typeof yourFeatureSearchSchema>

// Form schema — factory because it needs translated error messages
export const getYourFeatureSchema = (t: TFunction) => z.object({
  name: z.string().min(1, t('validation.required')),
})
export type YourFeatureFormData = z.infer<ReturnType<typeof getYourFeatureSchema>>

components/index.ts

Named re-exports only — keeps route file imports clean.

export { YourFeatureList } from './YourFeatureList'
export { YourFeatureDetail } from './YourFeatureDetail'
export { YourFeatureFilters } from './YourFeatureFilters'

index.ts (feature root)

Barrel export of the feature's public API. Route files import from here or from specific sub-paths.

export * from './components'
export * from './hooks'
export * from './types'

Route Files — Where They Live

Route files are not part of the feature folder. They live in src/routes/ following the URL structure:

src/routes/
└── _layout/
    └── your-section/
        └── your-feature/
            ├── index.tsx          # list page
            ├── create.tsx         # create page
            └── $featureId/
                ├── index.tsx      # detail page
                └── edit.tsx       # edit page

Route files are thin — they only wire URL params/search to feature components. See tanstack-router skill for the full route file pattern.

Form Sections Pattern

For complex forms, split fields into section components under YourFeatureFormSections/. Each section receives form as a typed prop:

interface BasicInfoSectionProps {
  form: YourFeatureFormApi
}

export function BasicInfoSection({ form }: BasicInfoSectionProps) {
  return (
    <FormSectionCard title="Basic Info">
      <form.AppField name="name">
        {(field) => <field.TextField label={t('feature.name')} />}
      </form.AppField>
    </FormSectionCard>
  )
}

The YourFeatureFormApi type is defined in the schema file:

export interface YourFeatureFormApi extends AppFormApi<YourFeatureFormData> {}

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Keep looking

Skills are one crate of 325,949. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.