Skip to content

Repository files navigation

faststrapy

A CLI tool to scaffold production-ready FastAPI projects instantly — inspired by create-next-app, powered by Jinja2 templates and interactive prompts.

Answer a handful of prompts (or pass flags and skip them entirely) and get a working FastAPI project: settings management, an optional SQLAlchemy + Alembic database layer, structured logging, a health-check route, git init, dependency install, and code formatting — all done for you.

$ faststrapy create-app
----------------------------------------------------
Generating `my-service` at /home/anoop/my-service
  → project metadata
  → project structure
  → settings + .env
  → database layer
  → logger
  → health route
  → main.py entrypoint
  → alembic
  → docker config
Running post-generation steps...
  → git init
  → ensure uv installed
  → dependency install (uv sync)
  → freeze requirements.txt
  → format with black
----------------------------------------------------
Done. Next steps:
----------------------------------------------------
  cd /home/anoop/my-service

  # activate the virtual environment uv created
  # (optional — `uv run` below works without activating)
  # Windows (cmd):        .venv\Scripts\activate
  # Windows (PowerShell): .venv\Scripts\Activate.ps1
  # macOS / Linux:        source .venv/bin/activate

  # run the project (from the project root)
  uv run python -m app.main

  # once it's running:
  http://127.0.0.1:8000
  http://127.0.0.1:8000/docs   (interactive Swagger UI)
  http://127.0.0.1:8000/redoc  (ReDoc API reference)
----------------------------------------------------

Table of contents


Requirements

  • Python 3.10+ (the generated projects default to 3.11, configurable per-project down to 3.10 during the prompts)
  • uv — not strictly required to install faststrapy itself, but the post-generation step uses it to install the new project's dependencies. If it's missing, faststrapy installs it for you automatically (see Post-generation steps).
  • git — used for the automatic git init + first commit after scaffolding. Not required to run faststrapy itself, but that step is skipped with a warning if git isn't on PATH.
  • Docker — only needed if you plan to build/run a generated project's Dockerfile / docker-compose.yml. Not required by faststrapy itself; Docker config files are generated on disk either way.

Installation

# with uv (recommended)
uv tool install faststrapy
faststrapy create-app

# uv add faststrapy

# or run it once without installing, always pulling the latest version
uvx faststrapy create-app

# or with plain pip
pip install faststrapy

Once installed, the faststrapy command is on your PATH. There's no separate "run" step and no npm-style @latest suffix on individual commands — uv add / pip install pin the version at install time, and faststrapy create-app always runs whichever version you currently have installed. Upgrade with uv add faststrapy --upgrade or pip install -U faststrapy.

Quick start

Fully interactive — no flags, faststrapy asks you everything (project name, framework, Python version, and whether to use the recommended defaults or customize every option):

faststrapy create-app

Non-interactive — pass any flag and all prompts are skipped; anything you don't pass falls back to a recommended default:

faststrapy create-app --project-name my-service --python 3.12 --sync-type async

Scaffold into a specific directory:

faststrapy create-app --project-name my-service --path ./services/my-service

Skip git init / uv sync / black formatting (just generate the files):

faststrapy create-app --project-name my-service --skip-postgen

CLI reference

faststrapy create-app

Flag Type Description
--project-name str Name of the project. Also becomes the output folder name if --path isn't given, and the default .env variable prefix.
--template str Framework to scaffold. Currently only fastapi is implemented (see Roadmap).
--python float Target Python version for the generated project, e.g. 3.12.
--sync-type str sync or async — controls both the database session style and the Alembic migration engine mode.
--path str Output directory. Defaults to ./<project-name> in the current working directory.
--skip-postgen flag Skip git init, the uv/uv sync step, and Black formatting. Just write the files.

Passing any of --project-name, --template, --python, --sync-type, or --path skips the interactive prompts entirely for that run — faststrapy fills in the rest with the recommended defaults (see _default_prompt_config). Pass none of them and you get the full interactive flow, including the "customize every option" path.

faststrapy logs

faststrapy logs [--upgrade] [--downgrade]

Reports where a generated project's logs are configured to go (file vs. database), based on the use_logs / save_logs_db choices made at scaffold time.

What gets generated

A default run (recommended settings, database + Alembic + logging + Black all enabled) produces:

my-service/
├── app/                        # ← everything lives here, not the project root
│   ├── __init__.py
│   ├── main.py                 #    FastAPI() app instance + entrypoint
│   ├── core/
│   │   ├── config/
│   │   │   └── settings.py     #    pydantic-settings Settings, reads .env
│   │   ├── database/           #    only if a database was selected
│   │   │   ├── base.py
│   │   │   ├── connection.py
│   │   │   ├── async_session.py    #  (or sync_session.py)
│   │   │   ├── dependency.py
│   │   │   └── module_registry.py
│   │   └── log/                #    only if logging was enabled
│   │       └── logger.py
│   ├── middlewares/
│   ├── utils/
│   └── modules/
│       ├── routes/
│       │   └── health_route.py #    GET /health, always generated
│       └── models/             #    only if a database was selected
├── alembic/                    #    only if Alembic was enabled
│   ├── env.py
│   ├── script.py.mako
│   └── versions/
├── alembic.ini
├── .env                        #    real values, gitignored
├── .env.example                #    same keys, committed
├── .gitignore
├── .python-version
├── pyproject.toml              #    dependencies resolved from your choices
├── requirements.txt            #    frozen from the synced .venv after uv sync
├── Dockerfile                  #    only if Docker config was enabled
├── .dockerignore                #   only if Docker config was enabled
├── docker-compose.yml           #   only if Docker config was enabled;
│                                #   includes a postgres service + volume
│                                #   when the project uses local Postgres
└── README.md

Notes on a few of the choices baked into this layout:

  • main.py lives inside app/, not the project root. The app is meant to be run as a module — uv run python -m app.main — which keeps app.core...-style absolute imports working correctly. Running it as a bare script (python app/main.py) would break those imports, since Python only adds the script's own directory to sys.path, not the project root.
  • Both pyproject.toml and requirements.txt are generated. requirements.txt starts as a hand-built list matching pyproject.toml at generation time, then gets overwritten with the real, pinned output of uv pip freeze once dependencies are synced (see Post-generation steps) — so the project stays installable either the uv/PEP 621 way or the classic pip install -r requirements.txt way, with accurate pins either way.
  • SERVER_PATH in .env is set to app.main:app (or <your-folder-name>.main:app if you renamed the holder folder) — this is the import string uvicorn.run() uses internally, and it's kept in sync with wherever main.py actually is. The generated Dockerfile's CMD uses the same import string.
  • Docker config is opt-in but on by default. docker-compose.yml only adds a db service (Postgres 16, with a named volume) when your project is actually configured for local Postgres — SQLite-only projects get just the app service, no dead container definition.

How the prompts work

  1. Pre-config — project name, framework (fastapi; flask/django are recognized but not yet implemented — see Roadmap), Python version, and whether the app's code should live inside a subfolder (default: yes, app/).
  2. You're then asked: use the recommended defaults, or customize?
    • Recommended defaults: Pydantic ✓, SQLAlchemy ORM ✓, Postgres (both local + Neon-ready) ✓, async DB access, Alembic ✓ (async), logging ✓ (console only), Black ✓, Docker config ✓.
    • Customize: one prompt per option — database on/off, which database, sync vs async, ORM on/off, env var prefix, Alembic on/off, logging on/off (and whether to persist to a file), Black on/off, Docker config on/off.

All of this is captured in ProjectConfigSchema (PreConfig + DefaultConfig), which is what every generator function receives.

Post-generation steps

After the files are written, faststrapy runs (unless --skip-postgen):

  1. git init — initializes a repo and creates the first commit (chore: scaffold project with faststrapy). Skipped with a warning if git isn't installed, or if your global git identity (user.name/user.email) isn't configured yet — the commit will fail but nothing else is affected.

    If the parent folder already has its own .git (e.g. you scaffolded inside an existing monorepo or workspace), faststrapy won't nest a second repo silently — it asks first: "A git repo already exists in the parent folder. Initialize a new one inside this project too?" (default: no). Answer no and the new project is left for the parent repo to track; answer yes and it behaves exactly as if there were no parent repo at all.

  2. uv auto-install — if uv isn't already on your PATH, faststrapy runs the official installer for your OS (the curl | sh one-liner on macOS/Linux, the irm | iex one on Windows) so the next step can succeed. This is best-effort: if it fails (offline, restricted permissions, unsupported shell), you get a one-line notice and the run continues — nothing crashes.

  3. uv sync — installs the generated project's dependencies into a fresh .venv.

  4. Freeze requirements.txt — runs uv pip freeze against the just-synced .venv and overwrites requirements.txt with the real, pinned versions actually installed, replacing the hand-built loose list written at generation time. If uv isn't available or this step is skipped, the generation-time file is kept as a fallback.

  5. Black formatting — if you kept Black enabled, the generated code is formatted in place, pinned to the project's target Python version.

Every post-generation step is independently best-effort: if one fails, you get a ⚠ skipped (<reason>) line and the rest still run. Nothing about a failed post-gen step blocks the "Done" summary at the end.

Running the generated project

cd my-service

# optional — uv run works without activating a venv at all
source .venv/bin/activate        # macOS / Linux
.venv\Scripts\activate           # Windows (cmd)
.venv\Scripts\Activate.ps1       # Windows (PowerShell)

uv run python -m app.main

Then visit:

  • http://127.0.0.1:8000 — the app
  • http://127.0.0.1:8000/docs — Swagger UI
  • http://127.0.0.1:8000/redoc — ReDoc

Host, port, and reload behavior are all controlled by the generated .env (SERVER_HOST, SERVER_PORT, SERVER_RELOAD) via app/core/config/settings.py.


Project architecture (for contributors)

faststrapy has three moving pieces, each with its own registry pattern:

faststrapy/
├── main.py                # Typer app entrypoint — registers sub_command
├── prompts.py              # All interactive prompt logic (FaststrapyPrompts)
├── schemas/
│   └── project_config.py   # PreConfig, DefaultConfig, ProjectConfigSchema — the
│                            # single object passed to every generator/postgen step
├── configs/
│   └── commands.py         # `create-app` and `logs` Typer commands; wires
│                            # prompts → generators → postgen → summary output
├── generators/              # Each file writes one slice of the new project
│   ├── registry.py          #  @register_generator("name", order=N) decorator
│   ├── structure.py         #  order=10  — folder skeleton + __init__.py files
│   ├── project_meta.py      #  order=5   — pyproject.toml, requirements.txt, .gitignore, README
│   ├── core_config.py       #  order=20  — settings.py, .env, .env.example
│   ├── core_database.py     #  order=30  — database layer (conditional)
│   ├── logging_gen.py       #  order=40  — logger.py (conditional)
│   ├── routes.py             #  order=50  — health_route.py
│   ├── entrypoint.py        #  order=60  — main.py
│   ├── alembic_gen.py       #  order=70  — alembic/ (conditional)
│   ├── docker_gen.py        #  order=80  — Dockerfile, .dockerignore,
│   │                        #  docker-compose.yml (conditional)
│   └── fs_utils.py          #  write_file() / touch_init() helpers
├── postgen/                  # Runs after files exist on disk
│   ├── registry.py          #  @register_postgen("name", order=N), catches
│                            #  exceptions per-step (best-effort by design)
│   └── actions.py           #  git init (parent-.git aware) → ensure uv
│                            #  installed → uv sync → freeze requirements.txt
│                            #  → black
├── templates/                # .jinja templates rendered by the generators
│   └── *.py.jinja
└── utils/
    └── jinja_env.py          # Environment + render_template()

How the registries work: a generator (or postgen step) is just a function decorated with @register_generator("human readable name", order=N). run_generators() / run_postgen() sort by order and run them in that sequence, printing the name as they go. Lower order runs first. There's no other wiring needed — importing the module (already done for you in generators/__init__.py / postgen/__init__.py) registers it.

Adding a new generator

  1. Create faststrapy/generators/your_thing.py.
  2. Write a function def generate_your_thing(base_path: Path, config: ProjectConfigSchema) -> None:, decorated with @register_generator("your thing", order=N). Pick N based on where it needs to run relative to the existing steps (e.g. after structure at 10, before entrypoint at 60 if it needs to exist before main.py references it).
  3. If it's conditional (like the database layer), guard it: if not config.default_config.<your_flag>: return.
  4. If it writes a new field, add it to DefaultConfig in schemas/project_config.py first, and wire a prompt for it in prompts.py (both the customize path and, if it should have a sensible default, _default_prompt_config).
  5. Add the import to faststrapy/generators/__init__.py's import list so the decorator actually runs.
  6. If it renders a template, add the .jinja file under faststrapy/templates/ and call render_template("your_thing.py.jinja", **ctx).

Adding a new postgen step

Same pattern in faststrapy/postgen/actions.py, decorated with @register_postgen("name", order=N). Steps here are expected to shell out (git, uv, black, etc.) — raise a plain RuntimeError with a clear message on failure; the registry catches it and prints ⚠ skipped (<message>) without stopping the rest of the run. Don't add a try/except inside your own step unless you need to do something other than fail — that's already handled centrally.

Files you'll typically touch

Change you want to make File(s)
New scaffolding option / prompt schemas/project_config.py, prompts.py, the relevant generator
New file type in generated projects New file in generators/, new .jinja in templates/
Change what recommended defaults are prompts.py_default_prompt_config
Change CLI flags on create-app configs/commands.py
New post-generation shell step postgen/actions.py
Fix/change a generated file's content The relevant .jinja file in templates/not a .py file, these are text templates

Files to be careful changing (or avoid changing without discussion)

  • pyproject.toml [project.scripts] and [build-system] — this is what makes pip install faststrapy produce a working faststrapy command at all. Breaking the entry point (faststrapy.main:app) or the packages = ["faststrapy"] wheel config breaks every install.
  • The faststrapy/ package name itself — this must match the PyPI project name and the console-script entry point. Don't rename or nest it further without updating pyproject.toml to match.
  • generators/registry.py / postgen/registry.py — the ordering/execution engine both generator sets rely on. Changing its behavior (e.g. making generator failures best-effort like postgen's) is a real design decision, not a small tweak — raise it in an issue/PR description rather than changing it silently.
  • SERVER_PATH in templates/env.py.jinja and where entrypoint.py writes main.py — these two have to agree with each other (see What gets generated). If you move main.py's output location, update SERVER_PATH to match, and vice versa.
  • templates/script_mako.py.jinja — despite the .jinja extension (for consistency with the rest of the folder), this is copied verbatim, not rendered through Jinja — it uses Mako's ${...} syntax, consumed by Alembic itself at alembic revision time. Don't run it through render_template().

Contributing

  1. Fork the repo, then clone your fork.

  2. Set up a dev environment:

    uv sync
    uv pip install -e .

    This installs faststrapy in editable mode so faststrapy create-app (or uv run faststrapy create-app) reflects your local changes immediately.

  3. Branch naming — prefix by intent, short and hyphenated:

    • feature/<short-description> — new generator, new prompt, new flag
    • fix/<short-description> — bug fixes
    • docs/<short-description> — README/docs-only changes
    • chore/<short-description> — packaging, CI, dependency bumps, refactors with no behavior change

    e.g. feature/flask-template, fix/alembic-sync-mode, docs/contributing-section.

  4. Commit messagesConventional Commits style is preferred: feat: add flask template support, fix: correct SERVER_PATH for renamed app folder, docs: expand CLI reference. Not strictly enforced, but it's what the existing history follows and keeps changelogs generatable later.

  5. Test your change manually before opening a PR — there's no automated test suite yet (see Roadmap), so the working smoke test is:

    uv run faststrapy create-app --project-name smoke-test --path /tmp/smoke-test
    cd /tmp/smoke-test
    uv run python -m app.main   # confirm it boots and /docs loads

    Run this with both the recommended-defaults path and, if your change touches a conditional generator (database/logging/alembic), the customize path with that option toggled off, to make sure nothing assumes it's always on.

  6. Open the PR against main, describe what changed and why, and mention which generator/postgen order values (if any) you added or touched, since ordering bugs are the easiest thing to introduce silently.

Reporting issues

Open a GitHub issue with: the exact faststrapy create-app command you ran (flags included), your OS and Python version, and the full output. If it's about the generated project rather than the CLI itself, include the relevant generated file.

See CONTRIBUTORS.md for everyone who's helped so far, and CHANGELOG.md for the release history.

Roadmap / known limitations

  • --template flask and --template django are recognized by the prompts but explicitly rejected as "still in progress" — only fastapi is implemented today.
  • No automated test suite yet — contributions here are especially welcome (a good first one: a pytest suite that runs create_app programmatically against a temp dir for each generator combination and asserts the expected files exist).
  • faststrapy logs currently only reports the configured log destination; it doesn't yet manage log files/DB rows for an existing project.
  • The uv auto-install postgen step shells out to the official installer scripts (astral.sh/uv/install.sh / .ps1) at run time — if your network blocks that domain, the step fails gracefully but you'll need to install uv yourself.

License

MIT — see LICENSE.

About

A CLI tool to scaffold production-ready projects instantly — inspired by create-next-app, powered by Jinja2 templates and interactive prompts.

Topics

Resources

Stars

5 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages