#!/usr/bin/env python3
"""
Fullstack Project Scaffolder
Generates project structure and boilerplate for various fullstack architectures.
Supports Next.js, FastAPI+React, MERN, Django+React, and more.
Usage:
python project_scaffolder.py nextjs my-app
python project_scaffolder.py fastapi-react my-api --with-docker
python project_scaffolder.py mern my-project --with-auth
python project_scaffolder.py --list-templates
"""
import argparse
import json
import os
import sys
from pathlib import Path
from typing import Dict, List, Optional
# Project templates with file structures
TEMPLATES = {
"nextjs": {
"name": "Next.js Full Stack",
"description": "Next.js 14+ with App Router, TypeScript, Tailwind CSS",
"structure": {
"src/app": ["layout.tsx", "page.tsx", "globals.css", "api/health/route.ts"],
"src/components/ui": ["Button.tsx", "Card.tsx", "Input.tsx"],
"src/components/layout": ["Header.tsx", "Footer.tsx"],
"src/lib": ["utils.ts", "db.ts"],
"src/types": ["index.ts"],
"public": [],
"": ["package.json", "tsconfig.json", "tailwind.config.ts", "next.config.js",
".env.example", ".gitignore", "README.md"]
}
},
"fastapi-react": {
"name": "FastAPI + React",
"description": "FastAPI backend with React frontend, PostgreSQL",
"structure": {
"backend/app": ["__init__.py", "main.py", "config.py", "database.py"],
"backend/app/api": ["__init__.py", "routes.py", "deps.py"],
"backend/app/models": ["__init__.py", "user.py"],
"backend/app/schemas": ["__init__.py", "user.py"],
"backend": ["requirements.txt", "alembic.ini", "Dockerfile"],
"frontend/src": ["App.tsx", "main.tsx", "index.css"],
"frontend/src/components": ["Layout.tsx"],
"frontend/src/hooks": ["useApi.ts"],
"frontend": ["package.json", "tsconfig.json", "vite.config.ts", "Dockerfile"],
"": ["docker-compose.yml", ".env.example", ".gitignore", "README.md"]
}
},
"mern": {
"name": "MERN Stack",
"description": "MongoDB, Express, React, Node.js with TypeScript",
"structure": {
"server/src": ["index.ts", "config.ts", "database.ts"],
"server/src/routes": ["index.ts", "users.ts"],
"server/src/models": ["User.ts"],
"server/src/middleware": ["auth.ts", "error.ts"],
"server": ["package.json", "tsconfig.json", "Dockerfile"],
"client/src": ["App.tsx", "main.tsx"],
"client/src/components": ["Layout.tsx"],
"client/src/services": ["api.ts"],
"client": ["package.json", "tsconfig.json", "vite.config.ts", "Dockerfile"],
"": ["docker-compose.yml", ".env.example", ".gitignore", "README.md"]
}
},
"django-react": {
"name": "Django + React",
"description": "Django REST Framework backend with React frontend",
"structure": {
"backend/config": ["__init__.py", "settings.py", "urls.py", "wsgi.py"],
"backend/apps/users": ["__init__.py", "models.py", "serializers.py", "views.py", "urls.py"],
"backend": ["manage.py", "requirements.txt", "Dockerfile"],
"frontend/src": ["App.tsx", "main.tsx"],
"frontend/src/components": ["Layout.tsx"],
"frontend": ["package.json", "tsconfig.json", "vite.config.ts", "Dockerfile"],
"": ["docker-compose.yml", ".env.example", ".gitignore", "README.md"]
}
}
}
def get_file_content(template: str, filepath: str, project_name: str) -> str:
"""Generate file content based on template and file type."""
filename = Path(filepath).name
contents = {
# Next.js files
"layout.tsx": f'''import type {{ Metadata }} from "next";
import "./globals.css";
export const metadata: Metadata = {{
title: "{project_name}",
description: "Generated by project scaffolder",
}};
export default function RootLayout({{
children,
}}: {{
children: React.ReactNode;
}}) {{
return (
{{children}}
);
}}
''',
"page.tsx": f'''export default function Home() {{
return (
{project_name}
Welcome to your new project.
);
}}
''',
"globals.css": '''@tailwind base;
@tailwind components;
@tailwind utilities;
''',
"route.ts": '''import { NextResponse } from "next/server";
export async function GET() {
return NextResponse.json({
status: "healthy",
timestamp: new Date().toISOString(),
});
}
''',
"Button.tsx": '''import { ButtonHTMLAttributes, forwardRef } from "react";
interface ButtonProps extends ButtonHTMLAttributes {
variant?: "primary" | "secondary" | "outline";
size?: "sm" | "md" | "lg";
}
export const Button = forwardRef(
({ className = "", variant = "primary", size = "md", ...props }, ref) => {
const base = "font-medium rounded-lg transition-colors";
const variants = {
primary: "bg-blue-600 text-white hover:bg-blue-700",
secondary: "bg-gray-200 text-gray-900 hover:bg-gray-300",
outline: "border border-gray-300 hover:bg-gray-50",
};
const sizes = { sm: "px-3 py-1.5 text-sm", md: "px-4 py-2", lg: "px-6 py-3 text-lg" };
return ;
}
);
Button.displayName = "Button";
''',
"Card.tsx": '''interface CardProps extends React.HTMLAttributes {}
export function Card({ className = "", children, ...props }: CardProps) {
return (
{children}
);
}
''',
"Input.tsx": '''import { InputHTMLAttributes, forwardRef } from "react";
interface InputProps extends InputHTMLAttributes {
label?: string;
error?: string;
}
export const Input = forwardRef(
({ label, error, className = "", ...props }, ref) => (
{label &&
}
{error &&
{error}
}
)
);
Input.displayName = "Input";
''',
"Header.tsx": f'''import Link from "next/link";
export function Header() {{
return (
);
}}
''',
"Footer.tsx": '''export function Footer() {
return (
);
}
''',
"utils.ts": '''export function cn(...classes: (string | undefined | false)[]): string {
return classes.filter(Boolean).join(" ");
}
export function formatDate(date: Date): string {
return new Intl.DateTimeFormat("en-US", {
year: "numeric", month: "long", day: "numeric",
}).format(date);
}
export function sleep(ms: number): Promise {
return new Promise((resolve) => setTimeout(resolve, ms));
}
''',
"db.ts": '''// Database connection (Prisma example)
// import { PrismaClient } from "@prisma/client";
// export const db = new PrismaClient();
export const db = {
// Placeholder - configure your database
};
''',
"index.ts": '''export interface User {
id: string;
email: string;
name: string | null;
createdAt: Date;
}
export interface ApiResponse {
data: T;
error: string | null;
success: boolean;
}
''',
# FastAPI files
"main.py": f'''from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.api.routes import router
from app.config import settings
app = FastAPI(title="{project_name}", openapi_url="/api/openapi.json")
app.add_middleware(
CORSMiddleware,
allow_origins=settings.ALLOWED_ORIGINS,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(router, prefix="/api")
@app.get("/health")
async def health_check():
return {{"status": "healthy"}}
''',
"config.py": '''from pydantic_settings import BaseSettings
from typing import List
class Settings(BaseSettings):
DATABASE_URL: str = "postgresql://user:pass@localhost:5432/db"
ALLOWED_ORIGINS: List[str] = ["http://localhost:3000", "http://localhost:5173"]
SECRET_KEY: str = "change-me-in-production"
class Config:
env_file = ".env"
settings = Settings()
''',
"database.py": '''from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from app.config import settings
engine = create_engine(settings.DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
''',
"routes.py": '''from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from app.database import get_db
router = APIRouter()
@router.get("/users")
async def get_users(db: Session = Depends(get_db)):
return {"users": []}
@router.post("/users")
async def create_user(db: Session = Depends(get_db)):
return {"message": "User created"}
''',
"deps.py": '''from typing import Generator
from fastapi import Depends
from sqlalchemy.orm import Session
from app.database import get_db
def get_current_user():
# Implement authentication
pass
''',
"user.py": '''from sqlalchemy import Column, Integer, String, DateTime
from sqlalchemy.sql import func
from app.database import Base
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
email = Column(String, unique=True, nullable=False)
name = Column(String)
created_at = Column(DateTime(timezone=True), server_default=func.now())
''',
"requirements.txt": '''fastapi>=0.104.0
uvicorn[standard]>=0.24.0
sqlalchemy>=2.0.0
pydantic>=2.0.0
pydantic-settings>=2.0.0
alembic>=1.12.0
psycopg2-binary>=2.9.0
python-jose[cryptography]>=3.3.0
passlib[bcrypt]>=1.7.0
''',
"alembic.ini": '''[alembic]
script_location = alembic
sqlalchemy.url = driver://user:pass@localhost/dbname
''',
# Express files
"index.ts": '''import express from "express";
import cors from "cors";
import helmet from "helmet";
import routes from "./routes";
const app = express();
const PORT = process.env.PORT || 8000;
app.use(helmet());
app.use(cors());
app.use(express.json());
app.use("/api", routes);
app.get("/health", (_, res) => res.json({ status: "healthy" }));
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
''',
"config.ts": '''export const config = {
PORT: parseInt(process.env.PORT || "8000"),
MONGODB_URI: process.env.MONGODB_URI || "mongodb://localhost:27017/app",
JWT_SECRET: process.env.JWT_SECRET || "change-me",
};
''',
"database.ts": '''import mongoose from "mongoose";
import { config } from "./config";
export async function connectDatabase(): Promise {
await mongoose.connect(config.MONGODB_URI);
console.log("Connected to MongoDB");
}
''',
"users.ts": '''import { Router } from "express";
const router = Router();
router.get("/", async (req, res) => {
res.json({ users: [] });
});
router.post("/", async (req, res) => {
res.status(201).json({ message: "User created" });
});
export default router;
''',
"User.ts": '''import mongoose, { Schema, Document } from "mongoose";
export interface IUser extends Document {
email: string;
name?: string;
createdAt: Date;
}
const userSchema = new Schema({
email: { type: String, required: true, unique: true },
name: String,
}, { timestamps: true });
export const User = mongoose.model("User", userSchema);
''',
"auth.ts": '''import { Request, Response, NextFunction } from "express";
export function authMiddleware(req: Request, res: Response, next: NextFunction) {
const token = req.headers.authorization?.replace("Bearer ", "");
if (!token) return res.status(401).json({ error: "Authentication required" });
// Verify token
next();
}
''',
"error.ts": '''import { Request, Response, NextFunction } from "express";
export function errorHandler(err: Error, req: Request, res: Response, next: NextFunction) {
console.error(err.stack);
res.status(500).json({ error: "Internal server error" });
}
''',
# React/Vite files
"App.tsx": f'''function App() {{
return (
{project_name}
Welcome to your new project.
);
}}
export default App;
''',
"main.tsx": '''import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import "./index.css";
ReactDOM.createRoot(document.getElementById("root")!).render(
);
''',
"index.css": '''@tailwind base;
@tailwind components;
@tailwind utilities;
''',
"Layout.tsx": f'''import {{ ReactNode }} from "react";
export function Layout({{ children }}: {{ children: ReactNode }}) {{
return (
);
}}
''',
"useApi.ts": '''import { useState, useCallback } from "react";
export function useApi(baseUrl = "/api") {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const request = useCallback(async (endpoint: string, options?: RequestInit) => {
setLoading(true);
setError(null);
try {
const res = await fetch(`${baseUrl}${endpoint}`, {
headers: { "Content-Type": "application/json" },
...options,
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const json = await res.json();
setData(json);
return json;
} catch (e) {
setError(e instanceof Error ? e.message : "Unknown error");
throw e;
} finally {
setLoading(false);
}
}, [baseUrl]);
return { data, loading, error, request };
}
''',
"api.ts": '''const API_BASE = import.meta.env.VITE_API_URL || "/api";
async function request(endpoint: string, options: RequestInit = {}): Promise {
const res = await fetch(`${API_BASE}${endpoint}`, {
headers: { "Content-Type": "application/json" },
...options,
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
export const api = {
get: (endpoint: string) => request(endpoint),
post: (endpoint: string, body: unknown) => request(endpoint, { method: "POST", body: JSON.stringify(body) }),
put: (endpoint: string, body: unknown) => request(endpoint, { method: "PUT", body: JSON.stringify(body) }),
delete: (endpoint: string) => request(endpoint, { method: "DELETE" }),
};
''',
"vite.config.ts": '''import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
server: {
port: 5173,
proxy: { "/api": { target: "http://localhost:8000", changeOrigin: true } },
},
});
''',
# Django files
"settings.py": f'''from pathlib import Path
import os
BASE_DIR = Path(__file__).resolve().parent.parent
SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY", "change-me")
DEBUG = os.environ.get("DEBUG", "True") == "True"
ALLOWED_HOSTS = ["localhost", "127.0.0.1"]
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"rest_framework",
"corsheaders",
"apps.users",
]
MIDDLEWARE = [
"corsheaders.middleware.CorsMiddleware",
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
]
ROOT_URLCONF = "config.urls"
WSGI_APPLICATION = "config.wsgi.application"
DATABASES = {{
"default": {{
"ENGINE": "django.db.backends.postgresql",
"NAME": os.environ.get("DB_NAME", "app"),
"USER": os.environ.get("DB_USER", "user"),
"PASSWORD": os.environ.get("DB_PASSWORD", "password"),
"HOST": os.environ.get("DB_HOST", "localhost"),
"PORT": "5432",
}}
}}
CORS_ALLOWED_ORIGINS = ["http://localhost:5173"]
''',
"urls.py": '''from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path("admin/", admin.site.urls),
path("api/", include("apps.users.urls")),
]
''',
"wsgi.py": '''import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
application = get_wsgi_application()
''',
"models.py": '''from django.contrib.auth.models import AbstractUser
from django.db import models
class User(AbstractUser):
email = models.EmailField(unique=True)
USERNAME_FIELD = "email"
REQUIRED_FIELDS = ["username"]
''',
"serializers.py": '''from rest_framework import serializers
from .models import User
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ["id", "email", "username", "date_joined"]
''',
"views.py": '''from rest_framework import viewsets
from .models import User
from .serializers import UserSerializer
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
''',
"manage.py": '''#!/usr/bin/env python
import os
import sys
def main():
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
from django.core.management import execute_from_command_line
execute_from_command_line(sys.argv)
if __name__ == "__main__":
main()
''',
# Config files
"package.json": f'''{{"name": "{project_name}", "version": "0.1.0", "private": true,
"scripts": {{"dev": "next dev", "build": "next build", "start": "next start", "lint": "next lint"}},
"dependencies": {{"next": "^14.0.0", "react": "^18.2.0", "react-dom": "^18.2.0"}},
"devDependencies": {{"@types/node": "^20.0.0", "@types/react": "^18.2.0", "typescript": "^5.0.0", "tailwindcss": "^3.3.0", "autoprefixer": "^10.4.0", "postcss": "^8.4.0"}}
}}''',
"tsconfig.json": '''{"compilerOptions": {"target": "ES2020", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "strict": true, "noEmit": true, "esModuleInterop": true, "module": "esnext", "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve", "incremental": true, "baseUrl": ".", "paths": {"@/*": ["./src/*"]}}, "include": ["**/*.ts", "**/*.tsx"], "exclude": ["node_modules"]}''',
"tailwind.config.ts": '''import type { Config } from "tailwindcss";
const config: Config = { content: ["./src/**/*.{js,ts,jsx,tsx,mdx}"], theme: { extend: {} }, plugins: [] };
export default config;
''',
"next.config.js": '''/** @type {import('next').NextConfig} */
module.exports = { reactStrictMode: true };
''',
".env.example": '''DATABASE_URL="postgresql://user:password@localhost:5432/dbname"
SECRET_KEY="your-secret-here"
''',
".gitignore": '''node_modules/
.next/
dist/
build/
.env
.env.local
__pycache__/
*.pyc
.venv/
.DS_Store
''',
"docker-compose.yml": f'''version: "3.8"
services:
db:
image: postgres:15-alpine
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: password
POSTGRES_DB: {project_name}
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
''',
"Dockerfile": '''FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["npm", "run", "dev"]
''',
"README.md": f'''# {project_name}
Generated by Fullstack Project Scaffolder.
## Getting Started
```bash
npm install
npm run dev
```
Open http://localhost:3000.
''',
"__init__.py": "",
}
# Handle special cases
if "routes" in filepath and filename == "index.ts":
return '''import { Router } from "express";
import usersRouter from "./users";
const router = Router();
router.use("/users", usersRouter);
export default router;
'''
if "schemas" in filepath and filename == "user.py":
return '''from pydantic import BaseModel, EmailStr
from datetime import datetime
from typing import Optional
class UserBase(BaseModel):
email: EmailStr
name: Optional[str] = None
class UserCreate(UserBase):
password: str
class UserResponse(UserBase):
id: int
created_at: datetime
class Config:
from_attributes = True
'''
if "users" in filepath and filename == "urls.py":
return '''from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import UserViewSet
router = DefaultRouter()
router.register("users", UserViewSet)
urlpatterns = [path("", include(router.urls))]
'''
return contents.get(filename, f"// {filename}\n")
def create_project(template_name: str, project_name: str, output_dir: Path) -> Dict:
"""Create project from template."""
if template_name not in TEMPLATES:
return {
"success": False,
"error": f"Unknown template: {template_name}",
"available": list(TEMPLATES.keys())
}
template = TEMPLATES[template_name]
project_dir = output_dir / project_name
if project_dir.exists():
return {"success": False, "error": f"Directory exists: {project_dir}"}
created_files = []
created_dirs = []
for dir_path, files in template["structure"].items():
if dir_path:
full_dir = project_dir / dir_path
else:
full_dir = project_dir
full_dir.mkdir(parents=True, exist_ok=True)
created_dirs.append(str(full_dir))
for filename in files:
filepath = full_dir / filename
filepath.parent.mkdir(parents=True, exist_ok=True)
content = get_file_content(template_name, str(dir_path / filename), project_name)
filepath.write_text(content)
created_files.append(str(filepath))
return {
"success": True,
"project_name": project_name,
"template": template_name,
"description": template["description"],
"location": str(project_dir),
"files_created": len(created_files),
"directories_created": len(created_dirs),
"next_steps": get_next_steps(template_name, project_name)
}
def get_next_steps(template: str, name: str) -> List[str]:
"""Get setup instructions for template."""
steps = {
"nextjs": [f"cd {name}", "npm install", "cp .env.example .env.local", "npm run dev"],
"fastapi-react": [
f"cd {name}",
"docker-compose up -d db",
"cd backend && pip install -r requirements.txt && uvicorn app.main:app --reload",
"cd frontend && npm install && npm run dev"
],
"mern": [
f"cd {name}",
"docker-compose up -d mongo",
"cd server && npm install && npm run dev",
"cd client && npm install && npm run dev"
],
"django-react": [
f"cd {name}",
"docker-compose up -d db",
"cd backend && pip install -r requirements.txt && python manage.py migrate && python manage.py runserver",
"cd frontend && npm install && npm run dev"
]
}
return steps.get(template, [f"cd {name}"])
def list_templates() -> Dict:
"""List available templates."""
return {
"templates": [
{"name": k, "display_name": v["name"], "description": v["description"]}
for k, v in TEMPLATES.items()
]
}
def print_result(result: Dict, as_json: bool = False) -> None:
"""Print result."""
if as_json:
print(json.dumps(result, indent=2))
return
if "templates" in result:
print("\nAvailable Templates:")
print("=" * 50)
for t in result["templates"]:
print(f"\n {t['name']}")
print(f" {t['description']}")
return
if not result.get("success"):
print(f"Error: {result.get('error')}")
return
print("\n" + "=" * 50)
print("Project Created Successfully")
print("=" * 50)
print(f"Project: {result['project_name']}")
print(f"Template: {result['template']}")
print(f"Location: {result['location']}")
print(f"Files: {result['files_created']}")
print("\nNext Steps:")
for i, step in enumerate(result["next_steps"], 1):
print(f" {i}. {step}")
print()
def main():
parser = argparse.ArgumentParser(
description="Generate fullstack project scaffolding",
epilog="Examples:\n %(prog)s nextjs my-app\n %(prog)s fastapi-react my-api\n %(prog)s --list-templates",
formatter_class=argparse.RawDescriptionHelpFormatter
)
parser.add_argument("template", nargs="?", help="Project template")
parser.add_argument("project_name", nargs="?", help="Project name")
parser.add_argument("--output", "-o", default=".", help="Output directory")
parser.add_argument("--list-templates", "-l", action="store_true", help="List templates")
parser.add_argument("--json", action="store_true", help="JSON output")
args = parser.parse_args()
if args.list_templates:
print_result(list_templates(), args.json)
return
if not args.template or not args.project_name:
parser.print_help()
sys.exit(1)
result = create_project(args.template, args.project_name, Path(args.output).resolve())
print_result(result, args.json)
if not result.get("success"):
sys.exit(1)
if __name__ == "__main__":
main()