# Scaffold Feature Operation Generate boilerplate code structure for a new feature across database, backend, and frontend layers. ## Parameters **Received**: `$ARGUMENTS` (after removing 'scaffold' operation name) Expected format: `name:"feature-name" [layers:"database,backend,frontend"] [pattern:"crud|workflow|custom"]` ## Workflow ### 1. Understand Scaffolding Requirements Clarify: - What is the feature name? - Which layers need scaffolding? - What pattern does it follow (CRUD, workflow, custom)? - What entity/resource is being managed? ### 2. Analyze Project Structure ```bash # Detect project structure ls -la src/ # Detect ORM cat package.json | grep -E "(prisma|typeorm|sequelize|mongoose)" # Detect frontend framework cat package.json | grep -E "(react|vue|angular|svelte)" # Detect backend framework cat package.json | grep -E "(express|fastify|nest|koa)" ``` ### 3. Generate Database Layer #### Migration Scaffold ```typescript // migrations/TIMESTAMP_add_{feature_name}.ts import { MigrationInterface, QueryRunner, Table, TableIndex, TableForeignKey } from 'typeorm'; export class Add{FeatureName}{Timestamp} implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { await queryRunner.createTable( new Table({ name: '{table_name}', columns: [ { name: 'id', type: 'uuid', isPrimary: true, default: 'gen_random_uuid()', }, { name: 'name', type: 'varchar', length: '255', isNullable: false, }, { name: 'created_at', type: 'timestamp', default: 'now()', }, { name: 'updated_at', type: 'timestamp', default: 'now()', }, ], }), true ); // Add indexes await queryRunner.createIndex( '{table_name}', new TableIndex({ name: 'idx_{table_name}_created_at', columnNames: ['created_at'], }) ); } public async down(queryRunner: QueryRunner): Promise { await queryRunner.dropTable('{table_name}'); } } ``` #### Entity/Model Scaffold ```typescript // entities/{FeatureName}.entity.ts import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, Index, } from 'typeorm'; @Entity('{table_name}') export class {FeatureName} { @PrimaryGeneratedColumn('uuid') id: string; @Column({ type: 'varchar', length: 255 }) name: string; @CreateDateColumn({ name: 'created_at' }) @Index() createdAt: Date; @UpdateDateColumn({ name: 'updated_at' }) updatedAt: Date; } ``` ### 4. Generate Backend Layer #### Repository Scaffold ```typescript // repositories/{FeatureName}Repository.ts import { Repository } from 'typeorm'; import { {FeatureName} } from '../entities/{FeatureName}.entity'; import { AppDataSource } from '../config/database'; export class {FeatureName}Repository { private repository: Repository<{FeatureName}>; constructor() { this.repository = AppDataSource.getRepository({FeatureName}); } async findById(id: string): Promise<{FeatureName} | null> { return this.repository.findOne({ where: { id } }); } async findAll(page: number = 1, limit: number = 20): Promise<[{FeatureName}[], number]> { const skip = (page - 1) * limit; return this.repository.findAndCount({ skip, take: limit, order: { createdAt: 'DESC' }, }); } async create(data: Partial<{FeatureName}>): Promise<{FeatureName}> { const entity = this.repository.create(data); return this.repository.save(entity); } async update(id: string, data: Partial<{FeatureName}>): Promise<{FeatureName}> { await this.repository.update(id, data); const updated = await this.findById(id); if (!updated) { throw new Error('{FeatureName} not found after update'); } return updated; } async delete(id: string): Promise { await this.repository.delete(id); } } ``` #### Service Scaffold ```typescript // services/{FeatureName}Service.ts import { {FeatureName}Repository } from '../repositories/{FeatureName}Repository'; import { {FeatureName} } from '../entities/{FeatureName}.entity'; import { NotFoundError, ValidationError } from '../errors'; export interface Create{FeatureName}Input { name: string; } export interface Update{FeatureName}Input { name?: string; } export class {FeatureName}Service { constructor(private repository: {FeatureName}Repository) {} async get{FeatureName}(id: string): Promise<{FeatureName}> { const entity = await this.repository.findById(id); if (!entity) { throw new NotFoundError(`{FeatureName} with ID ${id} not found`); } return entity; } async list{FeatureName}s( page: number = 1, limit: number = 20 ): Promise<{ data: {FeatureName}[]; total: number; page: number; totalPages: number }> { const [data, total] = await this.repository.findAll(page, limit); return { data, total, page, totalPages: Math.ceil(total / limit), }; } async create{FeatureName}(input: Create{FeatureName}Input): Promise<{FeatureName}> { this.validateInput(input); return this.repository.create(input); } async update{FeatureName}(id: string, input: Update{FeatureName}Input): Promise<{FeatureName}> { await this.get{FeatureName}(id); // Verify exists return this.repository.update(id, input); } async delete{FeatureName}(id: string): Promise { await this.get{FeatureName}(id); // Verify exists await this.repository.delete(id); } private validateInput(input: Create{FeatureName}Input): void { if (!input.name || input.name.trim().length === 0) { throw new ValidationError('Name is required'); } if (input.name.length > 255) { throw new ValidationError('Name must not exceed 255 characters'); } } } ``` #### Controller Scaffold ```typescript // controllers/{FeatureName}Controller.ts import { Request, Response, NextFunction } from 'express'; import { {FeatureName}Service } from '../services/{FeatureName}Service'; export class {FeatureName}Controller { constructor(private service: {FeatureName}Service) {} get{FeatureName} = async (req: Request, res: Response, next: NextFunction) => { try { const { id } = req.params; const entity = await this.service.get{FeatureName}(id); res.json({ success: true, data: entity, }); } catch (error) { next(error); } }; list{FeatureName}s = async (req: Request, res: Response, next: NextFunction) => { try { const page = parseInt(req.query.page as string) || 1; const limit = parseInt(req.query.limit as string) || 20; const result = await this.service.list{FeatureName}s(page, limit); res.json({ success: true, data: result.data, meta: { total: result.total, page: result.page, totalPages: result.totalPages, limit, }, }); } catch (error) { next(error); } }; create{FeatureName} = async (req: Request, res: Response, next: NextFunction) => { try { const entity = await this.service.create{FeatureName}(req.body); res.status(201).json({ success: true, data: entity, }); } catch (error) { next(error); } }; update{FeatureName} = async (req: Request, res: Response, next: NextFunction) => { try { const { id } = req.params; const entity = await this.service.update{FeatureName}(id, req.body); res.json({ success: true, data: entity, }); } catch (error) { next(error); } }; delete{FeatureName} = async (req: Request, res: Response, next: NextFunction) => { try { const { id } = req.params; await this.service.delete{FeatureName}(id); res.status(204).send(); } catch (error) { next(error); } }; } ``` #### Routes Scaffold ```typescript // routes/{feature-name}.routes.ts import { Router } from 'express'; import { {FeatureName}Controller } from '../controllers/{FeatureName}Controller'; import { {FeatureName}Service } from '../services/{FeatureName}Service'; import { {FeatureName}Repository } from '../repositories/{FeatureName}Repository'; import { authenticate } from '../middlewares/auth.middleware'; import { validate } from '../middlewares/validation.middleware'; import { create{FeatureName}Schema, update{FeatureName}Schema } from '../schemas/{feature-name}.schemas'; const router = Router(); // Initialize dependencies const repository = new {FeatureName}Repository(); const service = new {FeatureName}Service(repository); const controller = new {FeatureName}Controller(service); // Public routes router.get('/', controller.list{FeatureName}s); router.get('/:id', controller.get{FeatureName}); // Protected routes router.post( '/', authenticate, validate(create{FeatureName}Schema), controller.create{FeatureName} ); router.put( '/:id', authenticate, validate(update{FeatureName}Schema), controller.update{FeatureName} ); router.delete('/:id', authenticate, controller.delete{FeatureName}); export default router; ``` #### Validation Schema Scaffold ```typescript // schemas/{feature-name}.schemas.ts import { z } from 'zod'; export const create{FeatureName}Schema = z.object({ body: z.object({ name: z.string().min(1).max(255), // Add more fields as needed }), }); export const update{FeatureName}Schema = z.object({ body: z.object({ name: z.string().min(1).max(255).optional(), // Add more fields as needed }), params: z.object({ id: z.string().uuid(), }), }); ``` ### 5. Generate Frontend Layer #### Types Scaffold ```typescript // features/{feature-name}/types/index.ts export interface {FeatureName} { id: string; name: string; createdAt: string; updatedAt: string; } export interface Create{FeatureName}Input { name: string; } export interface Update{FeatureName}Input { name?: string; } export interface {FeatureName}Filters { page?: number; limit?: number; } export interface PaginatedResponse { success: boolean; data: T[]; meta: { total: number; page: number; totalPages: number; limit: number; }; } ``` #### API Client Scaffold ```typescript // features/{feature-name}/api/{feature-name}Api.ts import axios, { AxiosInstance } from 'axios'; import { {FeatureName}, Create{FeatureName}Input, Update{FeatureName}Input, {FeatureName}Filters, PaginatedResponse } from '../types'; class {FeatureName}Api { private client: AxiosInstance; constructor() { this.client = axios.create({ baseURL: import.meta.env.VITE_API_URL || '/api', timeout: 10000, }); this.client.interceptors.request.use((config) => { const token = localStorage.getItem('accessToken'); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }); } async list(filters: {FeatureName}Filters = {}): Promise> { const response = await this.client.get('/{feature-name}', { params: filters }); return response.data; } async getById(id: string): Promise<{FeatureName}> { const response = await this.client.get(`/{feature-name}/${id}`); return response.data.data; } async create(data: Create{FeatureName}Input): Promise<{FeatureName}> { const response = await this.client.post('/{feature-name}', data); return response.data.data; } async update(id: string, data: Update{FeatureName}Input): Promise<{FeatureName}> { const response = await this.client.put(`/{feature-name}/${id}`, data); return response.data.data; } async delete(id: string): Promise { await this.client.delete(`/{feature-name}/${id}`); } } export const {featureName}Api = new {FeatureName}Api(); ``` #### Component Scaffolds ```typescript // features/{feature-name}/components/{FeatureName}List.tsx import React from 'react'; import { use{FeatureName}s } from '../hooks/use{FeatureName}s'; import { {FeatureName}Card } from './{FeatureName}Card'; import { LoadingSpinner } from '@/components/LoadingSpinner'; import { ErrorMessage } from '@/components/ErrorMessage'; export const {FeatureName}List: React.FC = () => { const { items, isLoading, error, refetch } = use{FeatureName}s(); if (isLoading) { return ; } if (error) { return ; } if (items.length === 0) { return
No items found.
; } return (
{items.map((item) => ( <{FeatureName}Card key={item.id} item={item} /> ))}
); }; ``` ```typescript // features/{feature-name}/components/{FeatureName}Card.tsx import React from 'react'; import { {FeatureName} } from '../types'; interface {FeatureName}CardProps { item: {FeatureName}; onEdit?: (id: string) => void; onDelete?: (id: string) => void; } export const {FeatureName}Card: React.FC<{FeatureName}CardProps> = ({ item, onEdit, onDelete, }) => { return (

{item.name}

{onEdit && ( )} {onDelete && ( )}
); }; ``` ```typescript // features/{feature-name}/components/{FeatureName}Form.tsx import React from 'react'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; const {featureName}Schema = z.object({ name: z.string().min(1, 'Name is required').max(255), }); type {FeatureName}FormData = z.infer; interface {FeatureName}FormProps { initialData?: {FeatureName}FormData; onSubmit: (data: {FeatureName}FormData) => Promise; onCancel?: () => void; } export const {FeatureName}Form: React.FC<{FeatureName}FormProps> = ({ initialData, onSubmit, onCancel, }) => { const { register, handleSubmit, formState: { errors, isSubmitting }, } = useForm<{FeatureName}FormData>({ resolver: zodResolver({featureName}Schema), defaultValues: initialData, }); return (
{errors.name && ( {errors.name.message} )}
{onCancel && ( )}
); }; ``` #### Custom Hook Scaffold ```typescript // features/{feature-name}/hooks/use{FeatureName}s.ts import { useState, useEffect, useCallback } from 'react'; import { {featureName}Api } from '../api/{feature-name}Api'; import { {FeatureName} } from '../types'; export const use{FeatureName}s = () => { const [items, setItems] = useState<{FeatureName}[]>([]); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const fetch{FeatureName}s = useCallback(async () => { setIsLoading(true); setError(null); try { const response = await {featureName}Api.list(); setItems(response.data); } catch (err: any) { setError(err); } finally { setIsLoading(false); } }, []); useEffect(() => { fetch{FeatureName}s(); }, [fetch{FeatureName}s]); const create = useCallback(async (data: any) => { const newItem = await {featureName}Api.create(data); setItems((prev) => [...prev, newItem]); }, []); const update = useCallback(async (id: string, data: any) => { const updated = await {featureName}Api.update(id, data); setItems((prev) => prev.map((item) => (item.id === id ? updated : item))); }, []); const remove = useCallback(async (id: string) => { await {featureName}Api.delete(id); setItems((prev) => prev.filter((item) => item.id !== id)); }, []); return { items, isLoading, error, refetch: fetch{FeatureName}s, create, update, remove, }; }; ``` ### 6. Generate Test Scaffolds ```typescript // Backend test scaffold // repositories/__tests__/{FeatureName}Repository.test.ts import { {FeatureName}Repository } from '../{FeatureName}Repository'; import { createTestDataSource } from '../../test/utils'; describe('{FeatureName}Repository', () => { let repository: {FeatureName}Repository; beforeAll(async () => { await createTestDataSource(); repository = new {FeatureName}Repository(); }); it('should create {feature-name}', async () => { const entity = await repository.create({ name: 'Test' }); expect(entity.id).toBeDefined(); expect(entity.name).toBe('Test'); }); it('should find {feature-name} by id', async () => { const created = await repository.create({ name: 'Test' }); const found = await repository.findById(created.id); expect(found?.name).toBe('Test'); }); // Add more tests }); ``` ```typescript // Frontend test scaffold // features/{feature-name}/components/__tests__/{FeatureName}List.test.tsx import { render, screen } from '@testing-library/react'; import { {FeatureName}List } from '../{FeatureName}List'; import { use{FeatureName}s } from '../../hooks/use{FeatureName}s'; jest.mock('../../hooks/use{FeatureName}s'); describe('{FeatureName}List', () => { it('should render list of items', () => { (use{FeatureName}s as jest.Mock).mockReturnValue({ items: [{ id: '1', name: 'Test Item' }], isLoading: false, error: null, }); render(<{FeatureName}List />); expect(screen.getByText('Test Item')).toBeInTheDocument(); }); it('should show loading state', () => { (use{FeatureName}s as jest.Mock).mockReturnValue({ items: [], isLoading: true, error: null, }); render(<{FeatureName}List />); expect(screen.getByTestId('loading-spinner')).toBeInTheDocument(); }); // Add more tests }); ``` ## Output Format ```markdown # Scaffolded Feature: {Feature Name} ## Generated Files ### Database Layer - migrations/TIMESTAMP_add_{feature_name}.ts - entities/{FeatureName}.entity.ts ### Backend Layer - repositories/{FeatureName}Repository.ts - services/{FeatureName}Service.ts - controllers/{FeatureName}Controller.ts - routes/{feature-name}.routes.ts - schemas/{feature-name}.schemas.ts ### Frontend Layer - features/{feature-name}/types/index.ts - features/{feature-name}/api/{feature-name}Api.ts - features/{feature-name}/components/{FeatureName}List.tsx - features/{feature-name}/components/{FeatureName}Card.tsx - features/{feature-name}/components/{FeatureName}Form.tsx - features/{feature-name}/hooks/use{FeatureName}s.ts ### Test Files - repositories/__tests__/{FeatureName}Repository.test.ts - services/__tests__/{FeatureName}Service.test.ts - components/__tests__/{FeatureName}List.test.tsx ## Next Steps 1. Run database migration 2. Register routes in main app 3. Implement custom business logic 4. Add additional validations 5. Customize UI components 6. Write comprehensive tests 7. Add documentation ## Customization Points - Add custom fields to entity - Implement complex queries in repository - Add business logic to service - Customize UI components - Add additional API endpoints ``` ## Error Handling - If project structure unclear: Ask for clarification or detect automatically - If naming conflicts: Suggest alternative names - Generate placeholders for unknown patterns