Project Structure
Understanding the structure of projects created by the TriStack CLI
Overview
TriStack scaffolds backend projects in Python, Go, and Rust. Each language follows its own native conventions, so a generated project looks like a real developer wrote it:
- Python —
pyproject.toml, sources undersrc/,migrations/ - Go —
go.mod, a root-levelmain.go,internal/packages - Rust —
Cargo.toml, sources undersrc/
The exact tree depends on your selections: base files are always present, then framework, ORM, migrations, database, and add-on files are layered on top. This page mirrors what the CLI actually writes.
How the tree is built
Files come from these layers, in order:
- Base — always included (
README.md,tristack.jsonc,.env.example,.gitignore, plus the language manifest:pyproject.toml,go.mod, orCargo.toml) - Framework — the HTTP entrypoint per framework
- ORM — the data layer
- Migrations — migration files/tooling
- Frontend — the server-rendered UI layer (per selected frontend)
- Database — database choice drives configuration and env values
- Addons — one set of files per selected addon
tristack.jsonc records the stack and its reproducible command. See tristack.jsonc.
Python
Root layout (default stack)
The default stack (fastapi + sqlmodel + alembic + sqlite + uv + docker, ruff, pytest) looks like this:
Notes:
- The import package is a fixed
src/directory — the project name never appears inside the tree. The distribution/project name inpyproject.tomlis still your snake_case project name (my_tristack_appformy-tristack-app). - Docker/CI files exist only when the matching addon is selected;
tests/only with thepytestaddon.
Framework variations
All Python frameworks share the base + import package, then differ in the entrypoint:
- FastAPI —
main.pywith aFastAPIapp (uv run uvicorn src.main:app --reload) - Litestar —
main.pywith aLitestarapp (uv run uvicorn src.main:app --reload) - Flask —
main.pywith aFlaskapp (uv run flask --app src.main run --debug) - Django — no
main.py; insteadmanage.pyplus aconfigpackage (settings,asgi.py,wsgi.py,urls.py) and per-domain apps underapps/(core,users)
Django brings its own ORM and migrations, so it skips the import-package layout entirely — the project name is only used for the root folder, and code lives in config/ and apps/.
ORM variations
- SQLModel — adds
db.py(engine/session) andmodels.py(models) tosrc/ - SQLAlchemy / Tortoise — adds the data-layer files to
src/ - None — no data-layer files
Migrations
With --migrations alembic, the CLI writes a migrations/ directory with alembic.ini, env.py, script.py.mako, and a versions/ folder for your migration files. Django uses its own bundled migrations.
Frontend
The frontend layer adds server-rendered pages served by the same backend (no separate UI server). With --frontend htmx, all Python frameworks except Django use the import-package layout:
web.py— the page routes (/and a fragment route like/web/items) returning Jinja2 templatestemplates/—base.html(layout with the HTMX script),home.html, and the fragment (e.g.items.html)static/css/style.css— the shared stylesheetpyproject.tomlinstallsjinja2(orlitestar[jinja]for Litestar)
Django instead gets a web app plus root templates:
config/urls.py includes apps.web.urls, apps.web is registered in INSTALLED_APPS, and STATICFILES_DIRS picks up the project static/ folder. Django ships its own template engine and static handling, so no extra dependencies are added.
Addons
| Addon | Adds |
|---|---|
docker | Dockerfile, docker-compose.yml, .dockerignore |
ruff | ruff.toml |
mypy | mypy.ini |
pytest | tests/test_health.py |
github-actions | .github/workflows/ci.yml |
Go
Every Go project is a single module with its entrypoint at cmd/api/main.go and a layered package tree under internal/ (config, handler, service, repository, model, db). Framework and data imports are pinned in go.mod.
Root layout (default stack)
The default stack (gin + gorm + goose + sqlite + go + docker, air, golangci-lint) looks like this:
Notes:
- The module path is the slugged project name (
my_apiformy-api) unless your project name is already a module path. - The server reads
PORT(default8000) andAPP_NAMEfrom the environment viainternal/config. - The entrypoint wires the layers:
config.Load()→db.Connect()→ repository → service → handler, then registersGET /health,GET /items, andPOST /items. ormmust not benonefor thehandler/service/repository/modellayers to be generated.
Framework variations
All frameworks share the entrypoint at cmd/api/main.go and the same internal/handler interface (ListItems, CreateItem); only the router wiring differs:
- Gin —
gin.Default()withGET /health - Fiber —
fiber.New()withGET /health - Echo —
echo.New()withGET /health - Chi —
chi.NewRouter()withGET /health - Go stdlib —
http.NewServeMux()with method-patternGET /health
ORM variations
- GORM —
internal/model/item.go(struct),internal/db/db.go(connection +AutoMigrate(&model.Item{})),internal/repository/item.go(gorm.DB) - sqlx —
internal/model/item.go(db:tags),internal/db/db.go(connection),internal/repository/item.go(sqlx.DB, placeholders viaRebind) - sqlc —
internal/model/item.go(plain JSON struct),internal/db/db.go(connection),internal/repository/item.go(wraps generatedinternal/db/sqlcqueries), plus rootschema/+queries/+sqlc.yaml; runsqlc generatevia thegenerateMakefile target before building - None — no persistence layers (
db,repository,model,service,handlerare skipped; only/healthis exposed)
The driver is chosen from the database value (sqlite, postgres, or mysql) and pinned in go.mod.
Migrations
- goose — a
migrations/folder of.sqlfiles (0001_create_items.sql) - golang-migrate — a
db/migrations/folder with paired.up.sql/.down.sqlfiles
Frontend
With --frontend htmx, Go adds a web layer under internal/web that registers page routes alongside the API. The handlers — GET / and a fragment like GET /web/items — render html/template, so no extra dependency is added:
web.go— a packageinternal/webexposing aNewconstructor that returns a handler wiring the page routes (/and a fragment like/web/items) to their templatestemplates/—base.html(layout with the HTMX script),index.html, and the fragment (e.g.items.html)static/css/style.css— the shared stylesheet, mounted at/static/cmd/api/main.go— mounts the web handler under the framework's router alongside the API routes
Addons
| Addon | Adds |
|---|---|
docker | Dockerfile, docker-compose.yml, .dockerignore |
air | air.toml (live reload) |
golangci-lint | .golangci.yml |
github-actions | .github/workflows/ci.yml |
Rust
Every Rust project is a single cargo package with its sources under src/. The Cargo build target is the default binary target, so main.rs lives at src/main.rs.
Root layout (default stack)
The default stack (axum + seaorm + cargo + docker, cargo-watch, clippy) looks like this:
Notes:
- The package name is the slugged project name (
my_apiformy-api). - The web framework entrypoints read
PORT(default8000) andAPP_NAMEfrom the environment (main.rs) and exposeGET /health. - Dependencies (
serde,tokio, the framework, and the ORM) are pinned inCargo.toml.
Framework variations
Every framework writes its entrypoint to src/main.rs. Each web framework exposes GET /health returning {"status":"ok"}:
- Axum —
axum::Routerwith a#[tokio::main]async runtime - Actix Web — the
actix-webHttpServer/App - Rocket —
rocketroutes with the#[launch](or#[rocket::main]) runtime - Warp — a
warpfilter pipeline ontokio - Salvo — a
salvoRouterontokio - Loco — a minimal, Rails-like
loco-rsentrypoint scaffold; generate the full app (controllers, initializers) withcargo loco new
ORM variations
The data layer always lands at src/db.rs and is wired to main.rs via mod db;:
- SeaORM — an async connection helper (
DatabaseConnectionforpostgres/mysql, the SQLite connection forsqlite) - Diesel — a synchronous connection helper (
PgConnection/MysqlConnection/SqliteConnection) - sqlx — an async pool helper (
PgPool/MySqlPool/SqlitePool) - None — no
src/db.rsand nomod db;
The database driver is chosen from the database value and enabled via Cargo features in Cargo.toml.
Migrations
Rust has no standard migration tool in the option set yet, so --migrations is none. Run migrations with your ORM's own tooling (e.g. diesel migration run, sqlx migrate run).
Frontend
With --frontend htmx, Rust adds Askama templates under src/templates/. The page templates are ORM-agnostic — the IndexTemplate / NowTemplate render data computed in main.rs, so the htmx frontend works with any ORM (or none):
main.rs— registersGET /(renderingIndexTemplate) andGET /web/now(rendering theNowTemplatefragment with the current Unix time)src/templates/—index.html(layout with the HTMX script and inline CSS) andnow.html(the fragment)Cargo.toml— addsaskama = "0.14"
Loco doesn't support htmx yet — the CLI rejects --framework loco --frontend htmx.
Addons
| Addon | Adds |
|---|---|
docker | Dockerfile, docker-compose.yml, .dockerignore |
cargo-watch | auto-reload dev server (run with cargo watch -x run) |
clippy | strict clippy lints in CI (cargo clippy -- -D warnings) |
github-actions | .github/workflows/ci.yml |
Config file
tristack.jsonc at the project root captures the resolved stack and a reproducibleCommand that reproduces it, regardless of language. See tristack.jsonc.
Frontend roadmap
The frontend dimension is language-aware: the CLI only offers options that exist for the selected language. This table tracks the full roadmap as frontends land.
| Language | Frontend | Status |
|---|---|---|
| Python | htmx | Available |
| Python | reflex / nicegui | Planned |
| Go | htmx | Available |
| Go | templ | Planned |
| Rust | htmx | Available |
| Rust | leptos | Planned |
