Files
2025-11-29 18:20:21 +08:00

22 KiB

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

# 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

// 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<void> {
        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<void> {
        await queryRunner.dropTable('{table_name}');
    }
}

Entity/Model Scaffold

// 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

// 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<void> {
        await this.repository.delete(id);
    }
}

Service Scaffold

// 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<void> {
        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

// 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

// 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

// 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

// 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<T> {
    success: boolean;
    data: T[];
    meta: {
        total: number;
        page: number;
        totalPages: number;
        limit: number;
    };
}

API Client Scaffold

// 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<PaginatedResponse<{FeatureName}>> {
        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<void> {
        await this.client.delete(`/{feature-name}/${id}`);
    }
}

export const {featureName}Api = new {FeatureName}Api();

Component Scaffolds

// 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 <LoadingSpinner />;
    }

    if (error) {
        return <ErrorMessage message={error.message} onRetry={refetch} />;
    }

    if (items.length === 0) {
        return <div>No items found.</div>;
    }

    return (
        <div className="{feature-name}-list">
            {items.map((item) => (
                <{FeatureName}Card key={item.id} item={item} />
            ))}
        </div>
    );
};
// 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 (
        <div className="{feature-name}-card">
            <h3>{item.name}</h3>
            <div className="{feature-name}-card__actions">
                {onEdit && (
                    <button onClick={() => onEdit(item.id)}>Edit</button>
                )}
                {onDelete && (
                    <button onClick={() => onDelete(item.id)}>Delete</button>
                )}
            </div>
        </div>
    );
};
// 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<typeof {featureName}Schema>;

interface {FeatureName}FormProps {
    initialData?: {FeatureName}FormData;
    onSubmit: (data: {FeatureName}FormData) => Promise<void>;
    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 (
        <form onSubmit={handleSubmit(onSubmit)} className="{feature-name}-form">
            <div className="form-group">
                <label htmlFor="name">Name</label>
                <input
                    id="name"
                    type="text"
                    {...register('name')}
                    disabled={isSubmitting}
                />
                {errors.name && (
                    <span className="error">{errors.name.message}</span>
                )}
            </div>

            <div className="form-actions">
                {onCancel && (
                    <button type="button" onClick={onCancel} disabled={isSubmitting}>
                        Cancel
                    </button>
                )}
                <button type="submit" disabled={isSubmitting}>
                    {isSubmitting ? 'Submitting...' : 'Submit'}
                </button>
            </div>
        </form>
    );
};

Custom Hook Scaffold

// 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<Error | null>(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

// 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
});
// 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

# 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