Frontend Project Scaffolder
Generates a complete Next.js/React project structure with TypeScript,
Tailwind CSS, and best practice configurations.
python frontend_scaffolder.py my-app --template nextjs
python frontend_scaffolder.py dashboard --template react --features auth,api
python frontend_scaffolder.py landing --template nextjs --dry-run
from typing import Dict, List, Optional
"name": "Next.js 14+ App Router",
"description": "Modern Next.js with App Router, Server Components, and TypeScript",
"layout.tsx": "ROOT_LAYOUT",
"globals.css": "GLOBALS_CSS",
"login": {"page.tsx": "AUTH_PAGE"},
"register": {"page.tsx": "AUTH_PAGE"},
"health": {"route.ts": "HEALTH_ROUTE"},
"button.tsx": "UI_BUTTON",
"header.tsx": "LAYOUT_HEADER",
"footer.tsx": "LAYOUT_FOOTER",
"sidebar.tsx": "LAYOUT_SIDEBAR",
"constants.ts": "CONSTANTS",
"use-debounce.ts": "HOOK_DEBOUNCE",
"use-local-storage.ts": "HOOK_LOCAL_STORAGE",
"index.ts": "TYPES_INDEX",
"description": "Modern React with Vite, TypeScript, and Tailwind CSS",
"main.tsx": "REACT_MAIN",
"index.css": "GLOBALS_CSS",
"button.tsx": "UI_BUTTON",
"use-debounce.ts": "HOOK_DEBOUNCE",
"use-local-storage.ts": "HOOK_LOCAL_STORAGE",
"index.ts": "TYPES_INDEX",
# Feature modules that can be added
"description": "Authentication with session management",
"lib/auth.ts": "AUTH_LIB",
"middleware.ts": "AUTH_MIDDLEWARE",
"components/auth/login-form.tsx": "LOGIN_FORM",
"components/auth/register-form.tsx": "REGISTER_FORM",
"dependencies": ["next-auth", "@auth/core"],
"description": "API client with React Query",
"lib/api-client.ts": "API_CLIENT",
"lib/query-client.ts": "QUERY_CLIENT",
"providers/query-provider.tsx": "QUERY_PROVIDER",
"dependencies": ["@tanstack/react-query", "axios"],
"description": "Form handling with React Hook Form + Zod",
"lib/form-utils.ts": "FORM_UTILS",
"components/forms/form-field.tsx": "FORM_FIELD",
"dependencies": ["react-hook-form", "@hookform/resolvers", "zod"],
"description": "Testing setup with Vitest and Testing Library",
"vitest.config.ts": "VITEST_CONFIG",
"src/test/setup.ts": "TEST_SETUP",
"src/test/utils.tsx": "TEST_UTILS",
"dependencies": ["vitest", "@testing-library/react", "@testing-library/jest-dom"],
"description": "Component documentation with Storybook",
".storybook/main.ts": "STORYBOOK_MAIN",
".storybook/preview.ts": "STORYBOOK_PREVIEW",
"dependencies": ["@storybook/react-vite", "@storybook/addon-essentials"],
"ROOT_LAYOUT": '''import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
const inter = Inter({ subsets: ['latin'], variable: '--font-inter' });
export const metadata: Metadata = {
description: 'Built with Next.js',
export default function RootLayout({
children: React.ReactNode;
<body className={`${inter.variable} font-sans antialiased`}>
"HOME_PAGE": '''export default function Home() {
<main className="flex min-h-screen flex-col items-center justify-center p-24">
<h1 className="text-4xl font-bold">Welcome</h1>
<p className="mt-4 text-lg text-gray-600">
Get started by editing app/page.tsx
"GLOBALS_CSS": '''@tailwind base;
--foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
@apply bg-background text-foreground;
"UI_BUTTON": '''import { forwardRef } from 'react';
import { cn } from '@/lib/utils';
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'default' | 'destructive' | 'outline' | 'ghost';
size?: 'default' | 'sm' | 'lg';
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant = 'default', size = 'default', ...props }, ref) => {
'inline-flex items-center justify-center rounded-md font-medium transition-colors',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
'disabled:pointer-events-none disabled:opacity-50',
'bg-primary text-primary-foreground hover:bg-primary/90': variant === 'default',
'bg-destructive text-destructive-foreground hover:bg-destructive/90': variant === 'destructive',
'border border-input bg-background hover:bg-accent': variant === 'outline',
'hover:bg-accent hover:text-accent-foreground': variant === 'ghost',
'h-10 px-4 py-2': size === 'default',
'h-9 px-3': size === 'sm',
'h-11 px-8': size === 'lg',
Button.displayName = 'Button';
export { Button, type ButtonProps };
"UI_INPUT": '''import { forwardRef } from 'react';
import { cn } from '@/lib/utils';
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
const Input = forwardRef<HTMLInputElement, InputProps>(
({ className, error, ...props }, ref) => {
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2',
'text-sm ring-offset-background file:border-0 file:bg-transparent',
'file:text-sm file:font-medium placeholder:text-muted-foreground',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
'disabled:cursor-not-allowed disabled:opacity-50',
error && 'border-destructive focus-visible:ring-destructive',
{error && <p className="mt-1 text-sm text-destructive">{error}</p>}
Input.displayName = 'Input';
export { Input, type InputProps };
"UI_CARD": '''import { cn } from '@/lib/utils';
interface CardProps extends React.HTMLAttributes<HTMLDivElement> {}
function Card({ className, ...props }: CardProps) {
'rounded-lg border bg-card text-card-foreground shadow-sm',
function CardHeader({ className, ...props }: CardProps) {
return <div className={cn('flex flex-col space-y-1.5 p-6', className)} {...props} />;
function CardTitle({ className, ...props }: React.HTMLAttributes<HTMLHeadingElement>) {
return <h3 className={cn('text-2xl font-semibold leading-none', className)} {...props} />;
function CardContent({ className, ...props }: CardProps) {
return <div className={cn('p-6 pt-0', className)} {...props} />;
function CardFooter({ className, ...props }: CardProps) {
return <div className={cn('flex items-center p-6 pt-0', className)} {...props} />;
export { Card, CardHeader, CardTitle, CardContent, CardFooter };
"UI_INDEX": '''export { Button } from './button';
export { Input } from './input';
export { Card, CardHeader, CardTitle, CardContent, CardFooter } from './card';
"UTILS": '''import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
export function formatDate(date: Date | string): string {
return new Intl.DateTimeFormat('en-US', {
}).format(new Date(date));
export function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
"CONSTANTS": '''export const APP_NAME = 'My App';
export const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000/api';
export const QUERY_KEYS = {
"HOOK_DEBOUNCE": '''import { useState, useEffect } from 'react';
export function useDebounce<T>(value: T, delay: number = 500): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value);
const timer = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timer);
"HOOK_LOCAL_STORAGE": '''import { useState, useEffect } from 'react';
export function useLocalStorage<T>(
): [T, (value: T | ((prev: T) => T)) => void] {
const [storedValue, setStoredValue] = useState<T>(() => {
if (typeof window === 'undefined') return initialValue;
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
if (typeof window !== 'undefined') {
window.localStorage.setItem(key, JSON.stringify(storedValue));
return [storedValue, setStoredValue];
"TYPES_INDEX": '''export interface User {
export interface ApiResponse<T> {
export interface PaginatedResponse<T> {
"HEALTH_ROUTE": '''import { NextResponse } from 'next/server';
export async function GET() {
return NextResponse.json({
timestamp: new Date().toISOString(),
"AUTH_PAGE": ''''use client';
export default function AuthPage() {
<div className="flex min-h-screen items-center justify-center">
<div className="w-full max-w-md p-8">
<h1 className="text-2xl font-bold text-center">Authentication</h1>
"LAYOUT_HEADER": '''import Link from 'next/link';
export function Header() {
<header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur">
<div className="container flex h-14 items-center">
<Link href="/" className="font-bold">
<nav className="ml-auto flex gap-4">
<Link href="/about" className="text-sm text-muted-foreground hover:text-foreground">
"LAYOUT_FOOTER": '''export function Footer() {
<footer className="border-t py-6">
<div className="container text-center text-sm text-muted-foreground">
<p>© {new Date().getFullYear()} My App. All rights reserved.</p>
"LAYOUT_SIDEBAR": '''interface SidebarProps {
children?: React.ReactNode;
export function Sidebar({ children }: SidebarProps) {
<aside className="fixed left-0 top-14 z-30 h-[calc(100vh-3.5rem)] w-64 border-r bg-background">
<div className="p-4">{children}</div>
"REACT_APP": '''import { Button } from './components/ui';
<main className="flex min-h-screen flex-col items-center justify-center p-24">
<h1 className="text-4xl font-bold">Welcome</h1>
<p className="mt-4 text-lg text-gray-600">
Get started by editing src/App.tsx
<Button className="mt-6">Get Started</Button>
"REACT_MAIN": '''import React from 'react';
import ReactDOM from 'react-dom/client';
ReactDOM.createRoot(document.getElementById('root')!).render(
"""Generate directory structure recursively."""
for name, content in structure.items():
current_path = base_path / name
if isinstance(content, dict):
current_path.mkdir(parents=True, exist_ok=True)
created_files.extend(generate_structure(current_path, content, dry_run))
current_path.parent.mkdir(parents=True, exist_ok=True)
file_content = FILE_CONTENTS.get(content, "")
current_path.write_text(file_content)
created_files.append(str(current_path))
def generate_config_files(
"""Generate configuration files."""
config_templates = get_config_templates(project_name, template, features)
template_config = TEMPLATES[template]
for config_file in template_config["config_files"]:
file_path = project_path / config_file
if config_file in config_templates:
file_path.write_text(config_templates[config_file])
created_files.append(str(file_path))
def get_config_templates(name: str, template: str, features: List[str]) -> Dict[str, str]:
"""Get configuration file contents."""
"tailwind-merge": "^2.0.0",
"@types/node": "^20.0.0",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"autoprefixer": "^10.0.0",
"eslint-config-next": "^14.0.0",
"tailwind-merge": "^2.0.0",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@vitejs/plugin-react": "^4.0.0",
"autoprefixer": "^10.0.0",
# Add feature dependencies
for dep in FEATURES[feature].get("dependencies", []):
deps[template]["dependencies"][dep] = "latest"
"dev": "next dev" if template == "nextjs" else "vite",
"build": "next build" if template == "nextjs" else "vite build",
"start": "next start" if template == "nextjs" else "vite preview",
"lint": "eslint . --ext .ts,.tsx",
"format": "prettier --write .",
"dependencies": deps[template]["dependencies"],
"devDependencies": deps[template]["devDependencies"],
"package.json": json.dumps(package_json, indent=2),
"lib": ["dom", "dom.iterable", "esnext"],
"moduleResolution": "bundler",
"resolveJsonModule": true,
"plugins": [{ "name": "next" }],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
"tailwind.config.ts": '''import type { Config } from 'tailwindcss';
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
'./components/**/*.{js,ts,jsx,tsx,mdx}',
'./app/**/*.{js,ts,jsx,tsx,mdx}',
'./src/**/*.{js,ts,jsx,tsx,mdx}',
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))',
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))',
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))',
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))',
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))',
border: 'hsl(var(--border))',
ring: 'hsl(var(--ring))',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)',
"postcss.config.js": '''module.exports = {
"next.config.js": '''/** @type {import('next').NextConfig} */
formats: ['image/avif', 'image/webp'],
optimizePackageImports: ['lucide-react'],
module.exports = nextConfig;
"vite.config.ts": '''import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
'@': path.resolve(__dirname, './src'),
"extends": ["next/core-web-vitals", "prettier"],
"react/no-unescaped-entities": "off"
".gitignore": '''# Dependencies
"index.html": '''<!DOCTYPE html>
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>''' + name + '''</title>
<script type="module" src="/src/main.tsx"></script>
template: str = "nextjs",
features: Optional[List[str]] = None,
"""Scaffold a complete frontend project."""
features = features or []
project_path = output_dir / name
if project_path.exists() and not dry_run:
return {"error": f"Directory already exists: {project_path}"}
template_config = TEMPLATES.get(template)
return {"error": f"Unknown template: {template}"}
# Create project directory
project_path.mkdir(parents=True, exist_ok=True)
# Generate base structure
generate_structure(project_path, template_config["structure"], dry_run)
generate_config_files(project_path, template, name, features, dry_run)
for file_path, content_key in FEATURES[feature]["files"].items():
full_path = project_path / file_path
full_path.parent.mkdir(parents=True, exist_ok=True)
content = FILE_CONTENTS.get(content_key, f"// TODO: Implement {content_key}")
full_path.write_text(content)
created_files.append(str(full_path))
"template_name": template_config["name"],
"path": str(project_path),
"files_created": len(created_files),
def print_result(result: Dict) -> None:
"""Print scaffolding result."""
print(f"Error: {result['error']}", file=sys.stderr)
print(f"Project Scaffolded: {result['name']}")
print(f"Template: {result['template_name']}")
print(f"Location: {result['path']}")
print(f"Files Created: {result['files_created']}")
print(f"Features: {', '.join(result['features'])}")
for step in result["next_steps"]:
parser = argparse.ArgumentParser(
description="Scaffold a frontend project with best practices"
help="Project name (kebab-case recommended)"
help="Output directory (default: current directory)"
choices=list(TEMPLATES.keys()),
help="Project template (default: nextjs)"
help="Comma-separated features to add (auth,api,forms,testing,storybook)"
help="List available templates"
help="List available features"
help="Show what would be created without creating files"
help="Output in JSON format"
args = parser.parse_args()
print("\nAvailable Templates:")
for key, template in TEMPLATES.items():
print(f" {key}: {template['name']}")
print(f" {template['description']}")
print("\nAvailable Features:")
for key, feature in FEATURES.items():
print(f" {key}: {feature['description']}")
deps = ", ".join(feature.get("dependencies", []))
features = [f.strip() for f in args.features.split(",")]
invalid = [f for f in features if f not in FEATURES]
print(f"Unknown features: {', '.join(invalid)}", file=sys.stderr)
print(f"Valid features: {', '.join(FEATURES.keys())}")
result = scaffold_project(
output_dir=Path(args.dir),
print(json.dumps(result, indent=2))
if __name__ == "__main__":