From b3eb8f4a426819f2d674383b3c6d37e193ac5f20 Mon Sep 17 00:00:00 2001 From: Adrian Vo Date: Thu, 12 Feb 2026 11:38:12 -0800 Subject: [PATCH 1/2] feat: setup mongodb compatibility, implement fastapi template, implement fastapi stack --- internal/cli/registry.go | 2 + internal/cli/root.go | 2 +- internal/stacks/fastapi/fastapi.go | 80 +++++++++++++++++++ .../stacks/templates/fastapi/main.py.tmpl | 27 +++++++ .../templates/fastapi/pyproject.toml.tmpl | 11 +++ .../templates/fastapi/requirements.txt.tmpl | 4 + .../mongodb/fastapi/db/client.py.tmpl | 16 ++++ .../templates/mongodb/fastapi/seed.tmpl | 10 +++ 8 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 internal/stacks/fastapi/fastapi.go create mode 100644 internal/stacks/templates/fastapi/main.py.tmpl create mode 100644 internal/stacks/templates/fastapi/pyproject.toml.tmpl create mode 100644 internal/stacks/templates/fastapi/requirements.txt.tmpl create mode 100644 internal/stacks/templates/mongodb/fastapi/db/client.py.tmpl create mode 100644 internal/stacks/templates/mongodb/fastapi/seed.tmpl diff --git a/internal/cli/registry.go b/internal/cli/registry.go index 300ca9d..cbbc2ce 100644 --- a/internal/cli/registry.go +++ b/internal/cli/registry.go @@ -5,6 +5,7 @@ import ( "github.com/b-jonathan/taco/internal/stacks" "github.com/b-jonathan/taco/internal/stacks/express" + "github.com/b-jonathan/taco/internal/stacks/fastapi" "github.com/b-jonathan/taco/internal/stacks/firebase" "github.com/b-jonathan/taco/internal/stacks/mongodb" "github.com/b-jonathan/taco/internal/stacks/nextjs" @@ -17,6 +18,7 @@ var Registry = map[string]Stack{ "nextjs": nextjs.New(), "mongodb": mongodb.New(), "firebase": firebase.New(), // TODO: implement Firebase stack + "fastapi": fastapi.New(), "none": nil, } diff --git a/internal/cli/root.go b/internal/cli/root.go index c94c0fe..3639c76 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -174,7 +174,7 @@ func initCmd() *cobra.Command { return err } - stack["backend"], _ = prompt.CreateSurveySelect("Choose a Backend Stack:\n", []string{"Express", "None"}, prompt.AskOpts{}) + stack["backend"], _ = prompt.CreateSurveySelect("Choose a Backend Stack:\n", []string{"Express", "FastAPI", "None"}, prompt.AskOpts{}) stack["backend"] = strings.ToLower(stack["backend"]) backend, err := GetFactory(stack["backend"]) if err != nil { diff --git a/internal/stacks/fastapi/fastapi.go b/internal/stacks/fastapi/fastapi.go new file mode 100644 index 0000000..5abc1dd --- /dev/null +++ b/internal/stacks/fastapi/fastapi.go @@ -0,0 +1,80 @@ +package fastapi + +import ( + "context" + "fmt" + "os" + "path/filepath" + + "github.com/b-jonathan/taco/internal/execx" + "github.com/b-jonathan/taco/internal/fsutil" + + "github.com/b-jonathan/taco/internal/stacks" +) + +type Stack = stacks.Stack +type Options = stacks.Options + +type fastapi struct{} + +func New() Stack { return &fastapi{} } + +func (fastapi) Type() string { return "backend" } +func (fastapi) Name() string { return "fastapi" } + +func (fastapi) Init(ctx context.Context, opts *Options) error { + backendDir := filepath.Join(opts.ProjectRoot, "backend") + + if err := os.MkdirAll(backendDir, 0o755); err != nil { + return fmt.Errorf("mkdir: %w", err) + } + + // initialize python environment + if err := execx.RunCmd(ctx, backendDir, "python -m venv venv"); err != nil { + return fmt.Errorf("create venv: %w", err) + } + + return nil +} + +func (fastapi) Generate(ctx context.Context, opts *Options) error { + templateDir := "fastapi" + outputDir := filepath.Join(opts.ProjectRoot, "backend") + + // generate files from template + if err := fsutil.GenerateFromTemplateDir(templateDir, outputDir); err != nil { + return err + } + + return nil +} + +func (fastapi) Post(ctx context.Context, opts *Options) error { + // install dependencies + backendDir := filepath.Join(opts.ProjectRoot, "backend") + if err := execx.RunCmd(ctx, backendDir, "venv/bin/pip install -r requirements.txt"); err != nil { + return fmt.Errorf("install dependencies: %w", err) + } + + gitignorePath := filepath.Join(opts.ProjectRoot, ".gitignore") + if err := fsutil.EnsureFile(gitignorePath); err != nil { + return fmt.Errorf("ensure gitignore file %w", err) + } + + _ = fsutil.AppendUniqueLines(gitignorePath, + []string{"backend/__pycache__/", "backend/venv/", "backend/.env*"}) + + path := filepath.Join(opts.ProjectRoot, "backend", ".env") + dir := filepath.Dir(path) + if err := os.MkdirAll(dir, 0o755); err != nil { + return fmt.Errorf("mkdir %s: %w", dir, err) + } + + content := `PORT=4000 +FRONTEND_ORIGIN=http://localhost:3000` + if err := os.WriteFile(path, []byte(content), 0o644); err != nil { + return fmt.Errorf("write %s: %w", path, err) + } + + return nil +} diff --git a/internal/stacks/templates/fastapi/main.py.tmpl b/internal/stacks/templates/fastapi/main.py.tmpl new file mode 100644 index 0000000..30341d4 --- /dev/null +++ b/internal/stacks/templates/fastapi/main.py.tmpl @@ -0,0 +1,27 @@ +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from dotenv import load_dotenv +import os + +load_dotenv() + +app = FastAPI() + +FRONTEND_ORIGIN = os.getenv("FRONTEND_ORIGIN", "http://localhost:3000") + +app.add_middleware( + CORSMiddleware, + allow_origins=[FRONTEND_ORIGIN], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +@app.get("/") +async def read_root(): + return {"message": "Hello, FastAPI!"} + +if __name__ == "__main__": + import uvicorn + port = int(os.getenv("PORT", "4000")) + uvicorn.run("main:app", host="0.0.0.0", port=port, reload=True) diff --git a/internal/stacks/templates/fastapi/pyproject.toml.tmpl b/internal/stacks/templates/fastapi/pyproject.toml.tmpl new file mode 100644 index 0000000..ec50667 --- /dev/null +++ b/internal/stacks/templates/fastapi/pyproject.toml.tmpl @@ -0,0 +1,11 @@ +[tool.ruff] +line-length = 100 +target-version = "py311" + +[tool.ruff.lint] +select = ["E", "F", "I", "B", "UP"] +ignore = ["E501"] + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" diff --git a/internal/stacks/templates/fastapi/requirements.txt.tmpl b/internal/stacks/templates/fastapi/requirements.txt.tmpl new file mode 100644 index 0000000..54aa173 --- /dev/null +++ b/internal/stacks/templates/fastapi/requirements.txt.tmpl @@ -0,0 +1,4 @@ +fastapi +uvicorn +python-dotenv +ruff diff --git a/internal/stacks/templates/mongodb/fastapi/db/client.py.tmpl b/internal/stacks/templates/mongodb/fastapi/db/client.py.tmpl new file mode 100644 index 0000000..217f8e9 --- /dev/null +++ b/internal/stacks/templates/mongodb/fastapi/db/client.py.tmpl @@ -0,0 +1,16 @@ +from pymongo import MongoClient +from dotenv import load_dotenv +import os + +load_dotenv() + +URI = os.getenv("MONGODB_URI") + +if not URI: + raise RuntimeError("❌ MONGODB_URI is not set in environment variables") + +client = MongoClient(URI) + +def get_db(): + return client.get_database() + diff --git a/internal/stacks/templates/mongodb/fastapi/seed.tmpl b/internal/stacks/templates/mongodb/fastapi/seed.tmpl new file mode 100644 index 0000000..7a5e5e2 --- /dev/null +++ b/internal/stacks/templates/mongodb/fastapi/seed.tmpl @@ -0,0 +1,10 @@ +@app.get("/seed") +def seed(): + try: + db = get_db() + docs = list(db["seed_test"].find({})) + for d in docs: + d["_id"] = str(d["_id"]) + return docs + except Exception: + raise HTTPException(status_code=500, detail="Database error") From 12b7ef618ae48a3a2b14c01bb76c659a576213f4 Mon Sep 17 00:00:00 2001 From: AddRain1 <77909535+AddRain1@users.noreply.github.com> Date: Thu, 19 Feb 2026 22:39:18 -0800 Subject: [PATCH 2/2] fix: add fastapi to embed, update python version --- internal/stacks/fastapi/fastapi.go | 4 ++-- internal/stacks/templates/embed.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/stacks/fastapi/fastapi.go b/internal/stacks/fastapi/fastapi.go index 5abc1dd..4edcf96 100644 --- a/internal/stacks/fastapi/fastapi.go +++ b/internal/stacks/fastapi/fastapi.go @@ -30,7 +30,7 @@ func (fastapi) Init(ctx context.Context, opts *Options) error { } // initialize python environment - if err := execx.RunCmd(ctx, backendDir, "python -m venv venv"); err != nil { + if err := execx.RunCmd(ctx, backendDir, "python3 -m venv venv"); err != nil { return fmt.Errorf("create venv: %w", err) } @@ -52,7 +52,7 @@ func (fastapi) Generate(ctx context.Context, opts *Options) error { func (fastapi) Post(ctx context.Context, opts *Options) error { // install dependencies backendDir := filepath.Join(opts.ProjectRoot, "backend") - if err := execx.RunCmd(ctx, backendDir, "venv/bin/pip install -r requirements.txt"); err != nil { + if err := execx.RunCmd(ctx, backendDir, "venv/bin/python -m pip install -r requirements.txt"); err != nil { return fmt.Errorf("install dependencies: %w", err) } diff --git a/internal/stacks/templates/embed.go b/internal/stacks/templates/embed.go index 861d8e5..2150385 100644 --- a/internal/stacks/templates/embed.go +++ b/internal/stacks/templates/embed.go @@ -2,5 +2,5 @@ package templates import "embed" -//go:embed express/* firebase/* mongodb/* nextjs/* +//go:embed express/* firebase/* mongodb/* nextjs/* fastapi/* var FS embed.FS