Getting started
===============
This guide walks through setting up a new project with the **be** target
from scratch. By the end you will have a working FastAPI application
with generated routes, schemas, and (optionally) auth wired to
SQLAlchemy models you define yourself.
For the fastest path -- a fully scaffolded ``main.py`` /
``pyproject.toml`` / ``justfile`` / ``config/project.jsonnet``
skeleton -- skip ahead to :ref:`bootstrap-with-be-root`; the rest of
this page walks through the underlying pieces by hand.
For the React / TypeScript side, see :doc:`usage` -- the ``fe`` and
``fe_root`` targets are introduced there.
What be generates -- and doesn't
---------------------------------
The ``be`` target generates FastAPI code from a config file. Specifically,
it produces:
* **Routes** -- one FastAPI router per resource, with handlers for the
CRUD operations (and custom actions) you enable.
* **Pydantic schemas** -- request and response models for every route.
* **Serializers** -- model-to-schema helpers used by the generated
handlers.
* **An app router** -- aggregates all resource routers into one
``APIRouter`` your FastAPI app can mount.
* **A project router** *(multi-app projects only)* -- mounts every
app router under its configured prefix.
* **Scaffolding** -- database session factories (one per configured
database) and, when auth is enabled, a ``get_current_user``
dependency and optional login router.
be does **not** generate your SQLAlchemy models. You write those
yourself and point the config at them by dotted import path.
Prerequisites
-------------
* Python 3.14+
* `uv `_ (recommended) or pip
* A PostgreSQL database, if you want to run the generated code
Install
-------
.. code-block:: bash
pip install kiln-generator
# or with uv
uv add kiln-generator
# or, to make ``codegen`` available globally without adding a
# Python dep to your project:
uv tool install kiln-generator
Verify the CLI is available::
codegen --help
codegen targets list
``kiln-generator`` ships four targets (``be``, ``be_root``, ``fe``,
``fe_root``). When more than one is installed, every ``codegen``
command takes ``--target `` to pick.
.. _bootstrap-with-be-root:
Fast path: bootstrap with ``be_root``
-------------------------------------
The ``be_root`` target is a one-shot scaffolder that emits the
boilerplate you'd otherwise type by hand: ``main.py``,
``pyproject.toml``, ``justfile``, ``.gitignore``, and the starter
``config/project.jsonnet``. (``requires-python`` in
``pyproject.toml`` is the only Python-version pin -- no
``.python-version`` since uv reads it from there.) Write a small
``bootstrap.jsonnet`` describing the project's identity and which
optional bits to enable:
.. code-block:: jsonnet
{
name: "myapp",
module: "myapp",
description: "FastAPI backend bootstrapped by be_root.",
opentelemetry: false,
auth: true,
psycopg: true,
pgcraft: false,
pgqueuer: false,
editable: false,
rate_limit: false,
comms: false,
notification_preferences: false,
}
Setting ``rate_limit: true`` adds the ``kiln-generator[rate-limit]``
extra and stamps a ``rate_limit: rate_limit.slowapi('...')`` block
into ``config/project.jsonnet`` pointing at a placeholder bucket-model
dotted path you fill in once your model exists.
Setting ``comms: true`` stamps a ``comms: comms.platform({...})``
block and emits a starter ``comms.py`` skeleton with stub context
schemas, a stub :class:`~ingot.comms.Transport`, and a stub
:class:`~ingot.comms.PreferenceResolver`. ``comms`` requires
``pgqueuer: true`` -- the bootstrap rejects the combination
otherwise so the broken state is caught at config-load time. See
:doc:`comms` for the runtime surface.
Setting ``notification_preferences: true`` (requires ``comms: true``)
upgrades the comms scaffold: the stub
:class:`~ingot.comms.PreferenceResolver` is replaced by a real
``DbPreferenceResolver`` querying
``{module}.models.NotificationPreference``, and the per-app
``config/{module}.jsonnet`` gains a full-CRUD resource for
managing the preference rows. You still own the
``NotificationPreference`` SQLAlchemy class -- subclass
:class:`~ingot.comms.NotificationPreferenceMixin` on your
project's ``Base`` and migrate the table.
Then run::
codegen generate --target be_root --config bootstrap.jsonnet --out .
Every output is ``if_exists="skip"``, so re-running after editing the
bootstrap config is non-destructive. Pass ``--force`` (or
``--force-paths a,b,c``) when you want a re-run to clobber.
After the bootstrap, jump to :ref:`step-3-generate` -- the Step 1
(SQLAlchemy models) and Step 2 (config) sections below describe what
the bootstrap already gave you.
Project layout
--------------
By default be writes all generated code into ``_generated/``
(controlled by the ``package_prefix`` config field). A typical
single-app project looks like:
.. code-block:: text
myproject/
├── config/
│ └── project.jsonnet # be config
├── myapp/
│ └── models.py # your hand-written SQLAlchemy models
├── main.py # your FastAPI entry point
└── _generated/ # written by be (never edit)
├── auth/ # get_current_user dependency
├── db/ # async session factories
└── myapp/
├── routes/ # FastAPI routers
├── schemas/ # Pydantic request/response models
└── serializers/ # model-to-schema helpers
Everything under ``_generated/`` is overwritten on every
``codegen generate --target be`` run. Source-control the config file
and your models, not the generated output.
Step 1 -- Define your SQLAlchemy models
---------------------------------------
be generates routes and schemas *around* SQLAlchemy models you
define. A minimal ``myapp/models.py``:
.. code-block:: python
import uuid
from datetime import datetime
from sqlalchemy import DateTime, String, func
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import (
DeclarativeBase, Mapped, mapped_column,
)
class Base(DeclarativeBase):
pass
class Article(Base):
__tablename__ = "articles"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
primary_key=True,
default=uuid.uuid4,
)
title: Mapped[str] = mapped_column(String)
body: Mapped[str] = mapped_column(String)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
server_default=func.now(),
)
Step 2 -- Write a config
------------------------
Create ``config/project.jsonnet`` at your project root:
.. code-block:: jsonnet
{
version: "1",
module: "myapp",
package_prefix: "_generated",
databases: [
{ key: "primary", url_env: "DATABASE_URL", default: true },
],
resources: [{
model: "myapp.models.Article",
pk: "id",
pk_type: "uuid",
route_prefix: "/articles",
require_auth: false,
operations: [
"get", "list", "create", "update", "delete",
{
name: "create",
fields: [
{ name: "title", type: "str" },
{ name: "body", type: "str" },
],
},
],
}],
}
Key points:
* ``model`` is the dotted import path to your SQLAlchemy class. be
does not require a specific base class -- any SQLAlchemy
``DeclarativeBase`` subclass works.
* ``operations`` lists the operations to run for this resource. A
string is shorthand for the operation with default options. An
object with ``name`` carries per-operation options (here, specifying
exactly which fields the ``CreateRequest`` schema should expose).
* ``databases`` produces one async session factory per entry. Set
``default: true`` on exactly one; resources omitting ``db_key`` use
the default.
.. _step-3-generate:
Step 3 -- Generate
------------------
Run the CLI::
codegen generate --target be --config config/project.jsonnet
Output lands in ``_generated/``:
.. code-block:: text
_generated/
├── db/
│ ├── __init__.py
│ └── primary_session.py
└── myapp/
├── __init__.py
├── routes/
│ ├── __init__.py # app router
│ └── article.py
├── schemas/
│ └── article.py
└── serializers/
└── article.py
``--out`` overrides the output root; ``--clean`` runs ``codegen clean``
first to remove any stale files.
Step 4 -- Mount the router
--------------------------
Wire the generated router into your FastAPI app:
.. code-block:: python
from fastapi import FastAPI
from _generated.myapp.routes import router
app = FastAPI()
app.include_router(router, prefix="/v1")
Step 5 -- Environment and database
----------------------------------
The generated session factory reads the URL from an environment
variable (``url_env`` on the database config -- default
``DATABASE_URL``)::
export DATABASE_URL="postgresql+asyncpg://user:pw@localhost/mydb"
Create the database tables with whatever migration tool you use
(Alembic is a common choice). be does not manage schema migrations.
Step 6 -- Run the server
------------------------
::
uvicorn main:app --reload
Interactive API docs land at ``http://localhost:8000/docs``.
Adding authentication
---------------------
Add an ``auth`` block to the config to turn on JWT authentication:
.. code-block:: jsonnet
{
...,
auth: {
type: "jwt",
secret_env: "JWT_SECRET",
algorithm: "HS256",
verify_credentials_fn: "myapp.auth.verify_credentials",
},
resources: [{
model: "myapp.models.Article",
require_auth: true, // applies to all handlers on this resource
...
}],
}
You provide ``verify_credentials`` -- be generates the rest
(``auth/dependencies.py`` with ``get_current_user``, and
``auth/router.py`` with a ``/auth/token`` login endpoint).
Auth is implemented as a cross-cutting ``@operation`` with a
``when`` hook -- it runs whenever both ``config.auth`` is set and
``resource.require_auth`` is true. No extra wiring is required on
your end.
Multi-app projects
------------------
For projects that bundle multiple apps (a blog API and an inventory
API, say), wrap each app's config in an ``apps`` list:
.. code-block:: jsonnet
// project.jsonnet
{
version: "1",
package_prefix: "_generated",
auth: { type: "jwt", secret_env: "JWT_SECRET", ... },
databases: [{ key: "primary", default: true }],
apps: [
{ config: import "blog.jsonnet", prefix: "/blog" },
{ config: import "inventory.jsonnet", prefix: "/inventory" },
],
}
``codegen generate --target be --config project.jsonnet`` produces the
per-app code plus a top-level ``_generated/routes/__init__.py`` that
mounts each app at its prefix. Mount that in FastAPI:
.. code-block:: python
from _generated.routes import router
app = FastAPI()
app.include_router(router, prefix="/v1")
See the :doc:`playground` for a runnable multi-app example with auth,
multiple databases, and custom actions.
Where to next
-------------
* :doc:`usage` -- day-to-day usage patterns, the full be config shape,
and an introduction to the ``fe`` / ``fe_root`` frontend targets.
* :doc:`extending` -- add your own operations, renderers, or targets.
* :doc:`architecture` -- how the engine, scopes, operations, and
renderers fit together.
* :doc:`reference` -- the exhaustive config reference.