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 Fast path: bootstrap with be_root; the rest of
this page walks through the underlying pieces by hand.
For the React / TypeScript side, see 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
APIRouteryour 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_userdependency 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¶
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 <name> to pick.
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:
{
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 Transport, and a stub
PreferenceResolver. comms requires
pgqueuer: true – the bootstrap rejects the combination
otherwise so the broken state is caught at config-load time. See
Communication platform for the runtime surface.
Setting notification_preferences: true (requires comms: true)
upgrades the comms scaffold: the stub
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
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 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:
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:
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:
{
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:
modelis the dotted import path to your SQLAlchemy class. be does not require a specific base class – any SQLAlchemyDeclarativeBasesubclass works.operationslists the operations to run for this resource. A string is shorthand for the operation with default options. An object withnamecarries per-operation options (here, specifying exactly which fields theCreateRequestschema should expose).databasesproduces one async session factory per entry. Setdefault: trueon exactly one; resources omittingdb_keyuse the default.
Step 3 – Generate¶
Run the CLI:
codegen generate --target be --config config/project.jsonnet
Output lands in _generated/:
_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:
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:
{
...,
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:
// 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:
from _generated.routes import router
app = FastAPI()
app.include_router(router, prefix="/v1")
See the Playground for a runnable multi-app example with auth, multiple databases, and custom actions.
Where to next¶
Usage – day-to-day usage patterns, the full be config shape, and an introduction to the
fe/fe_rootfrontend targets.Extending kiln – add your own operations, renderers, or targets.
Architecture – how the engine, scopes, operations, and renderers fit together.
Reference – the exhaustive config reference.