Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions internal/cli/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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,
}

Expand Down
2 changes: 1 addition & 1 deletion internal/cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
80 changes: 80 additions & 0 deletions internal/stacks/fastapi/fastapi.go
Original file line number Diff line number Diff line change
@@ -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, "python3 -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/python -m 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
}
2 changes: 1 addition & 1 deletion internal/stacks/templates/embed.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
27 changes: 27 additions & 0 deletions internal/stacks/templates/fastapi/main.py.tmpl
Original file line number Diff line number Diff line change
@@ -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)
11 changes: 11 additions & 0 deletions internal/stacks/templates/fastapi/pyproject.toml.tmpl
Original file line number Diff line number Diff line change
@@ -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"
4 changes: 4 additions & 0 deletions internal/stacks/templates/fastapi/requirements.txt.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
fastapi
uvicorn
python-dotenv
ruff
16 changes: 16 additions & 0 deletions internal/stacks/templates/mongodb/fastapi/db/client.py.tmpl
Original file line number Diff line number Diff line change
@@ -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()

10 changes: 10 additions & 0 deletions internal/stacks/templates/mongodb/fastapi/seed.tmpl
Original file line number Diff line number Diff line change
@@ -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")
Loading