Project Templates¶
craft-easy-template is a cookiecutter repository that scaffolds ready-to-run Craft Easy services. It ships two templates — one for a Python API service and one for a React/Expo frontend app — each with its own prompts, generated layout, and post-generation hooks.
Use the templates when you want a new project that already has application wiring, infrastructure, and CI assembled. If you prefer to build from scratch, see Quick Start instead.
Installing cookiecutter¶
Run a template straight from GitHub, or point at a local clone:
# From GitHub
cookiecutter gh:easy-software-system/craft-easy-template --directory=api
cookiecutter gh:easy-software-system/craft-easy-template --directory=frontend
# From a local clone
cookiecutter /path/to/craft-easy-template --directory=api
cookiecutter /path/to/craft-easy-template --directory=frontend
The API template¶
Scaffolds a complete Python service: FastAPI app factory, Craft Easy settings, a sample resource, Docker Compose and Dockerfile, Terraform infrastructure for your chosen cloud, and a working test suite with GitHub Actions CI.
Prompts¶
All values have sensible defaults — press enter to accept them.
| Prompt | Default | Description |
|---|---|---|
project_name |
My API |
Human-readable name, used in README.md, APP_TITLE, and the settings class name. |
project_slug |
my-api |
Directory name and Docker image name. Must match ^[a-z][a-z0-9-]*$. |
package_name |
my_api |
Python package name used in imports. Must match ^[a-z][a-z0-9_]*$. |
description |
A REST API built on Craft Easy |
Written to pyproject.toml and README.md. |
author_name |
Your Name |
Written to pyproject.toml. |
author_email |
you@example.com |
Written to pyproject.toml. |
python_version |
3.12 |
Minimum Python version pinned in pyproject.toml and the Dockerfile. |
craft_easy_version |
>=0.1 |
Version constraint for the craft-easy-api dependency. |
database_uri |
mongodb://localhost:27017 |
MongoDB URI written to .env.example. |
database_name |
{package_name}_dev |
Database name written to .env.example and docker-compose.yml. |
auth_method |
jwt | oauth2 | otp |
Pre-configures the auth setting in .env.example; the first option is the default. |
cloud_provider |
azure | gcp | none |
Selects which Terraform configuration and deployment workflow to generate. |
cloud_region_azure |
swedencentral |
Azure region used as the default in variables.tf. |
cloud_region_gcp |
europe-north1 |
GCP region used as the default in variables.tf. |
enable_jobs_deployment |
no | yes |
If yes, retains infrastructure/jobs.tf; otherwise that file is removed by the post-generation hook. Requires enable_jobs=yes. |
Feature flags¶
Six yes/no prompts toggle optional subsystems. Each flag bakes a True/False constant into the generated settings.py. Flags you say no to leave no trace in the generated project.
| Flag | Default | Effect |
|---|---|---|
enable_jobs |
no |
Sets JOBS_ENABLED in settings.py and .env.example. |
enable_file_import |
no |
Sets FILE_IMPORT_ENABLED. |
enable_payments |
no |
Sets PAYMENTS_ENABLED. |
enable_bookkeeping |
no |
Sets BOOKKEEPING_ENABLED. |
enable_gdpr |
no |
Sets GDPR_ENABLED. |
enable_bi_export |
no |
Sets BI_EXPORT_ENABLED. |
Input validation¶
A pre-generation hook runs before any files are written. It rejects invalid values and exits with a clear error message:
project_slugmust match^[a-z][a-z0-9-]*$package_namemust match^[a-z][a-z0-9_]*$cloud_providermust be one ofazure,gcp, ornone
What you get¶
The annotated file tree below shows the result for a project named My API with azure as the cloud provider and all feature flags left at their defaults (no).
my-api/
├── .env.example # Environment variable reference — copy to .env
├── .gitignore
├── .github/
│ └── workflows/
│ ├── ci.yml # Lint, format-check, and pytest on push/PR
│ └── deploy-azure.yml # Container image build + Terraform apply (Azure)
│ # deploy-gcp.yml is generated instead for GCP;
│ # both are removed when cloud_provider=none
├── Dockerfile # Multi-stage production image, runs as non-root
├── docker-compose.yml # MongoDB + API with hot-reload for local development
├── pyproject.toml # Build system, dependencies, ruff, pytest config
├── README.md
├── infrastructure/ # Omitted when cloud_provider=none
│ ├── backend.tf
│ ├── main.tf # Terraform root: provider, modules, resources
│ ├── variables.tf # env, image_tag, region, and provider-specific vars
│ ├── terraform.tfvars.example
│ ├── README.md
│ ├── environments/
│ │ ├── dev.tfvars
│ │ └── prod.tfvars
│ └── modules/ # Copied from shared/infrastructure-modules/ by hook
│ ├── azure/ # Container Registry, Container Apps, Cosmos DB, …
│ └── cloudflare/ # Cloudflare modules (bundled with azure and gcp)
├── src/
│ └── my_api/
│ ├── __init__.py
│ ├── app.py # create_my_api_app() factory
│ ├── settings.py # MyAPISettings(BaseSettings)
│ ├── models/
│ │ ├── __init__.py
│ │ └── example.py # Sample TenantScopedDocument — remove or replace
│ └── routes/
│ └── __init__.py # Router stub — add your route modules here
└── tests/
├── __init__.py
└── test_health.py # Async ASGI health-check test
The generated pyproject.toml¶
[project]
name = "my-api"
version = "0.1.0"
description = "A REST API built on Craft Easy"
readme = "README.md"
license = "MIT"
requires-python = ">=3.12"
authors = [
{name = "Your Name", email = "you@example.com"},
]
dependencies = [
"craft-easy-api>=0.1",
]
[project.optional-dependencies]
dev = [
"pytest>=8.0",
"pytest-asyncio>=0.24",
"httpx>=0.28",
"ruff>=0.8",
"mongomock-motor>=0.0.34",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src/my_api"]
[tool.ruff]
target-version = "py312"
line-length = 100
[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
The dev extra installs everything needed for testing (real async HTTP client, in-memory MongoDB via mongomock-motor) and linting. CI runs pip install -e ".[dev]" so the local editable install and the pipeline share the exact same environment.
The generated settings.py¶
from craft_easy.settings import Settings as BaseSettings
class MyAPISettings(BaseSettings):
APP_TITLE: str = "My API"
APP_VERSION: str = "0.1.0"
DATABASE_NAME: str = "my_api_dev"
# Baked-in at generation time — override via environment variables
JOBS_ENABLED: bool = False
FILE_IMPORT_ENABLED: bool = False
PAYMENTS_ENABLED: bool = False
BOOKKEEPING_ENABLED: bool = False
GDPR_ENABLED: bool = False
BI_EXPORT_ENABLED: bool = False
BaseSettings (from craft_easy.settings) is a Pydantic BaseSettings subclass that reads from environment variables and a .env file automatically. Every field defined in BaseSettings — MONGODB_URI, AUTH_ENABLED, ACCESS_CONTROL_ENABLED, LOG_LEVEL, and so on — is inherited and available without repeating it here.
The class name is derived by stripping spaces from project_name: My API → MyAPISettings, Order Service → OrderServiceSettings.
Module flags (e.g. JOBS_ENABLED) are baked in as False at generation time when you answered no to the corresponding prompt. They can still be overridden at runtime via environment variables — the generated .env.example includes commented-out entries for each flag.
The generated app.py¶
from craft_easy import create_app
from craft_easy.core.crud import Resource
from .models.example import Example
from .settings import MyAPISettings
def create_my_api_app(settings=None):
if settings is None:
settings = MyAPISettings()
app = create_app(settings)
# --- Resources ---
app.include_router(Resource(Example, "examples").build())
# Add more resources here:
# from .models.your_model import YourModel
# app.include_router(Resource(YourModel, "your-models").build())
return app
# Uvicorn entry point: uvicorn my_api.app:app --factory
app = create_my_api_app
Resource(model, path).build() returns a FastAPI router with standard CRUD, pagination, and ETag concurrency wired up. Add more resource routers the same way. See Resources for the full API.
The generated example.py model¶
from typing import Optional
from craft_easy.models.base import BaseDocument
from pydantic import Field
class Example(BaseDocument):
name: str = Field(description="Name of the example item")
description: Optional[str] = Field(default=None, description="Optional description")
is_active: bool = Field(default=True, description="Whether the item is active")
class TenantConfig:
tenant_scoped = True
class Settings:
name = "examples"
use_revision = True
indexes = [
[("name", 1), ("tenant_id", 1)],
]
Replace or delete this model — it exists purely to show the pattern and to give the health-check test a concrete resource to exercise.
Post-generation hook¶
After cookiecutter writes the project tree, a post-generation hook runs automatically:
- Cloud module copy — if
cloud_providerisazureorgcp, the hook locatescraft-easy-template/shared/infrastructure-modules/in the template source tree and copies the relevant modules intoinfrastructure/modules/. Cloudflare modules are always included alongside the cloud-provider modules. - Workflow pruning — deployment workflows for clouds you did not select are deleted. An azure project keeps only
deploy-azure.yml; a gcp project keeps onlydeploy-gcp.yml; anoneproject removes both. - Infrastructure removal — if
cloud_provider=none, the entireinfrastructure/directory and both deployment workflows are removed. - Jobs deployment pruning — if
enable_jobs=noorenable_jobs_deployment=no, theinfrastructure/jobs.tffile is removed so Terraform does not reference undefined job resources.
Running the generated project¶
cd my-api
# Create a virtual environment and install dependencies
python3.12 -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
# Start MongoDB (or bring up the full stack)
docker compose up -d mongodb
# — or —
docker compose up # starts both mongodb and api with hot-reload
# Configure local environment
cp .env.example .env
# Edit .env if you need non-default values
# Run the API directly (when using docker compose up -d mongodb)
uvicorn my_api.app:app --factory --host 0.0.0.0 --port 8000
Open http://localhost:8000/docs for the interactive OpenAPI UI.
Verifying the result¶
# Health check
curl http://localhost:8000/health
# → {"status": "ok"}
# List examples resource (returns empty array on a fresh install)
curl http://localhost:8000/api/v1/examples
# → {"items": [], "total": 0, ...}
# Run the test suite
pytest -v
Task management¶
Every Craft Easy project is expected to track work in a tasks/ directory. The template repository ships a reference structure at craft-easy-template/tasks/ — including BACKLOG.md and re-usable task spec templates in tasks/templates/. Copy this directory into your generated project at creation time and commit it:
From that point forward, create and track new work items in tasks/BACKLOG.md using the templates in tasks/templates/.
The frontend template¶
Scaffolds an Expo app with Expo Router navigation, JWT or API-key authentication, schema-driven resource views, and a pre-configured Craft Easy API client.
Prompts¶
| Prompt | Default | Description |
|---|---|---|
project_name |
My App |
Human-readable app name, written to app.json and package.json. |
project_slug |
Derived from project_name |
Directory and npm package name. Spaces become hyphens. |
api_url |
http://localhost:5001 |
Default API base URL wired into the generated API client. |
auth_method |
jwt | api_key |
Selects which auth flow is pre-wired in AuthContext.tsx. |
description |
A Craft Easy frontend application |
Written to package.json. |
author_name |
Developer |
Written to package.json. |
cloud_provider |
azure | gcp | none |
Selects hosting infrastructure for the web build. |
cloud_region_azure |
swedencentral |
Azure region for Static Web Apps. |
cloud_region_gcp |
europe-north1 |
GCP region for Cloud Run / Storage. |
Post-generation hook¶
The frontend hook prunes deployment workflows for the clouds you did not select, identical to the API template hook.
What you get¶
my-app/
├── .gitignore
├── app.json # Expo configuration (name, slug, icon, etc.)
├── package.json
├── tsconfig.json # TypeScript strict mode
├── app/ # Expo Router file-system routes
│ ├── _layout.tsx # Root layout — wraps AuthContext and ThemeContext
│ ├── index.tsx # Redirects to /login or /(main)
│ ├── login.tsx # Login screen (JWT token exchange or API key entry)
│ └── (main)/
│ ├── _layout.tsx # Tab bar navigator
│ ├── index.tsx # Home / dashboard
│ └── [resource]/
│ ├── index.tsx # Resource list, driven by /admin/schema
│ └── [id].tsx # Resource detail and inline edit
└── src/
├── context/
│ ├── AuthContext.tsx # Auth state, token storage, refresh logic
│ └── ThemeContext.tsx # Light / dark palette with system-preference detection
├── hooks/
│ └── useResource.ts # CRUD hook backed by API client and local cache
├── lib/
│ ├── api-client.ts # fetch wrapper with auth, refresh, ETag, request IDs
│ └── schema.ts # /admin/schema loader and in-memory cache
└── types/
└── schema.ts # TypeScript types for the Craft Easy schema envelope
The API client¶
src/lib/api-client.ts is a typed fetch wrapper that handles:
- Authentication — attaches
Authorization: Bearer <token>from secure storage forjwt, or the API key header forapi_key. - Token refresh — on 401, calls the refresh endpoint and retries the original request once.
- ETag concurrency — captures
ETagheaders on GET and sendsIf-Matchon the next PATCH/PUT/DELETE for the same document. - Request IDs — forwards
X-Request-Idto the API for server-side log correlation. - Error surface — rejects with a typed
ApiErrorcarryingcode,message, anddetailsfrom the Craft Easy error envelope.
The useResource hook¶
import { useResource } from "@/src/hooks/useResource";
export function ProductList() {
const { items, isLoading, refresh, create, update, remove } = useResource("products");
if (isLoading) return <ActivityIndicator />;
return <FlatList data={items} renderItem={...} />;
}
useResource calls the Craft Easy REST API, exposes loading and error states, and paginates automatically. Multiple components calling useResource("products") share the same in-memory cache.
Schema-driven views¶
The generated [resource]/index.tsx and [resource]/[id].tsx screens call GET /admin/schema on startup to discover resource names, field types, and display labels. They render list and detail views without any resource-specific UI code. As you add resources to the API, the frontend discovers them automatically.
Running the generated project¶
cd my-app
npm install
npm run web # Expo web (browser)
npm run ios # iOS simulator
npm run android # Android emulator
Updating a generated project¶
The templates are a starting point, not a framework you inherit from. Once cookiecutter has run, the generated code is yours — edit it freely. If craft-easy-template ships new features later, pull them in selectively rather than regenerating the whole project.
Source reference¶
| File | Purpose |
|---|---|
api/cookiecutter.json |
API template prompts and defaults |
api/{{cookiecutter.project_slug}}/ |
API template body |
api/hooks/pre_gen_project.py |
Input validation (slug, package name, cloud provider) |
api/hooks/post_gen_project.py |
Cloud module copy, workflow and infrastructure pruning |
frontend/cookiecutter.json |
Frontend template prompts and defaults |
frontend/{{cookiecutter.project_slug}}/ |
Frontend template body |
frontend/hooks/post_gen_project.py |
Deployment workflow pruning |
shared/infrastructure-modules/ |
Shared Terraform modules (Azure, GCP, Cloudflare) |
tasks/ |
Reference BACKLOG.md and task spec templates |