spec_to_rest
Research

Code Generation Pipeline

Edit on GitHub

Multi-target code emission from verified intermediate representation

Last updated:

Research and design document for Stage 5 of the spec-to-REST compiler. Covers complete file manifests, template architecture, database migration generation, OpenAPI emission, Dafny integration, infrastructure code, quality assurance, incremental regeneration, extensibility, and comparison with existing generators.

Status. The shipped codegen lives in modules/codegen/ and emits the python-fastapi-postgres target via Handlebars templates, see the live Python + FastAPI + PostgreSQL target reference. The Dafny integration (Phase 6, #27–32) is not yet implemented; the generated services raise NotImplementedError for non-trivial operation bodies until that work lands. Multi-target emission (Go/chi #33, TS/Express #35) is Phase 7, also not yet on main. Python-shaped code samples below are design sketches; the implementation language is Scala 3 (see Architecture).


Table of contents

  1. What Gets Generated (Complete File Manifest)
  2. Template Architecture
  3. Database Migration Generation
  4. OpenAPI Spec Generation
  5. Dafny Output Integration
  6. Infrastructure Code Generation
  7. Quality Assurance of Generated Code
  8. Incremental Regeneration
  9. Extensibility
  10. Comparison with Existing Generators

1. What gets generated

The code generation pipeline receives four inputs:

  • IR from the spec parser (entities, state, operations, invariants)
  • Convention engine outputs (HTTP method mapping, DB schema, validation rules)
  • Verified Dafny code for non-trivial operations (compiled to target language)
  • Test specifications derived from the spec (properties, state machines, conformance config)

It produces a complete, runnable project: not stubs or skeletons, but working code you can docker-compose up and start using immediately.

The following subsections show the full file manifest and complete generated code for the URL shortener spec from the comprehensive analysis (Section 7.2 of document 00).


1.1 Python-fastapi-postgres

Directory structure

url_shortener/
pyproject.toml
[project]
name = "url-shortener"
version = "0.1.0"
description = "URL Shortener service — generated by spec-to-rest compiler"
requires-python = ">=3.11"
dependencies = [
    "fastapi>=0.115.0,<1.0.0",
    "uvicorn[standard]>=0.30.0,<1.0.0",
    "sqlalchemy>=2.0.0,<3.0.0",
    "alembic>=1.13.0,<2.0.0",
    "psycopg[binary]>=3.2.0,<4.0.0",
    "pydantic>=2.9.0,<3.0.0",
    "pydantic-settings>=2.5.0,<3.0.0",
]

[project.optional-dependencies]
dev = [
    "pytest>=8.3.0,<9.0.0",
    "pytest-asyncio>=0.24.0,<1.0.0",
    "httpx>=0.27.0,<1.0.0",
    "hypothesis>=6.112.0,<7.0.0",
    "schemathesis~=4.15",
    "ruff>=0.7.0,<1.0.0",
    "mypy>=1.11.0,<2.0.0",
]

[build-system]
requires = ["setuptools>=75.0"]
build-backend = "setuptools.build_meta"

[tool.ruff]
target-version = "py311"
line-length = 99

[tool.ruff.lint]
select = ["E", "F", "I", "N", "W", "UP", "B", "SIM", "RUF"]

[tool.mypy]
python_version = "3.11"
strict = true

[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
Dockerfile
# --- Stage 1: build dependencies ---
FROM python:3.11-slim AS builder

WORKDIR /build
COPY pyproject.toml ./
RUN pip install --no-cache-dir --prefix=/install .

# --- Stage 2: runtime ---
FROM python:3.11-slim

RUN adduser --disabled-password --gecos "" appuser
WORKDIR /app

COPY --from=builder /install /usr/local
COPY alembic.ini ./
COPY alembic/ ./alembic/
COPY app/ ./app/

USER appuser

EXPOSE 8000

HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
    CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
docker-compose.yml
services:
  app:
    build: .
    ports:
      - "8000:8000"
    environment:
      DATABASE_URL: postgresql+psycopg://shortener:shortener@db:5432/shortener
      BASE_URL: http://localhost:8000
    depends_on:
      db:
        condition: service_healthy
    restart: unless-stopped

  db:
    image: postgres:17-alpine
    environment:
      POSTGRES_USER: shortener
      POSTGRES_PASSWORD: shortener
      POSTGRES_DB: shortener
    ports:
      - "5432:5432"
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U shortener"]
      interval: 5s
      timeout: 3s
      retries: 10

  migrate:
    build: .
    command: ["python", "-m", "alembic", "upgrade", "head"]
    environment:
      DATABASE_URL: postgresql+psycopg://shortener:shortener@db:5432/shortener
    depends_on:
      db:
        condition: service_healthy

volumes:
  pgdata:
.env.example
DATABASE_URL=postgresql+psycopg://shortener:shortener@localhost:5432/shortener
BASE_URL=http://localhost:8000
LOG_LEVEL=info
alembic.ini
[alembic]
script_location = alembic
sqlalchemy.url = %(DATABASE_URL)s

[loggers]
keys = root,sqlalchemy,alembic

[handlers]
keys = console

[formatters]
keys = generic

[logger_root]
level = WARN
handlers = console

[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine

[logger_alembic]
level = INFO
handlers =
qualname = alembic

[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic

[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S
Makefile
.PHONY: help run test lint typecheck migrate docker-up docker-down

help:  ## Show this help
	@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | \
		awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'

run:  ## Run the development server
	uvicorn app.main:app --reload --host 0.0.0.0 --port 8000

test:  ## Run all tests
	pytest tests/ -v --tb=short

lint:  ## Lint with ruff
	ruff check app/ tests/
	ruff format --check app/ tests/

typecheck:  ## Type-check with mypy
	mypy app/

migrate:  ## Run database migrations
	alembic upgrade head

docker-up:  ## Start all services with docker-compose
	docker compose up --build -d

docker-down:  ## Stop all services
	docker compose down -v
alembic/Alembic DB migrations
env.py
import os
from logging.config import fileConfig

from alembic import context
from sqlalchemy import engine_from_config, pool

from app.models.url_shortener import Base

config = context.config

if config.config_file_name is not None:
    fileConfig(config.config_file_name)

target_metadata = Base.metadata

database_url = os.environ.get("DATABASE_URL")
if database_url:
    config.set_main_option("sqlalchemy.url", database_url)


def run_migrations_offline() -> None:
    url = config.get_main_option("sqlalchemy.url")
    context.configure(url=url, target_metadata=target_metadata, literal_binds=True)
    with context.begin_transaction():
        context.run_migrations()


def run_migrations_online() -> None:
    connectable = engine_from_config(
        config.get_section(config.config_ini_section, {}),
        prefix="sqlalchemy.",
        poolclass=pool.NullPool,
    )
    with connectable.connect() as connection:
        context.configure(connection=connection, target_metadata=target_metadata)
        with context.begin_transaction():
            context.run_migrations()


if context.is_offline_mode():
    run_migrations_offline()
else:
    run_migrations_online()
versions/one file per spec change
001_initial_schema.py
"""Initial schema for UrlShortener service.

Generated by spec-to-rest compiler from the service specification.

Revision ID: 001
Create Date: 2026-04-05
"""

from alembic import op
import sqlalchemy as sa

revision = "001"
down_revision = None
branch_labels = None
depends_on = None


def upgrade() -> None:
    op.create_table(
        "short_codes",
        sa.Column("code", sa.String(length=10), nullable=False),
        sa.Column("url", sa.Text(), nullable=False),
        sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(),
                  nullable=False),
        sa.PrimaryKeyConstraint("code"),
        sa.CheckConstraint("length(code) >= 6 AND length(code) <= 10", name="ck_code_length"),
        sa.CheckConstraint("code ~ '^[a-zA-Z0-9]+$'", name="ck_code_pattern"),
        sa.CheckConstraint("url ~ '^https?://'", name="ck_url_valid_uri"),
    )
    op.create_index("ix_short_codes_created_at", "short_codes", ["created_at"])


def downgrade() -> None:
    op.drop_index("ix_short_codes_created_at", table_name="short_codes")
    op.drop_table("short_codes")
app/FastAPI service code
__init__.py
main.py
"""FastAPI application for UrlShortener service.

Generated by spec-to-rest compiler. Do not edit directly — modify the spec and
regenerate. See the 'extensions/' directory for safe customization hooks.
"""

import logging
from contextlib import asynccontextmanager
from collections.abc import AsyncIterator

from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse

from app.config import settings
from app.database import engine
from app.routers import url_shortener

logger = logging.getLogger(__name__)


@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
    logger.info("Starting UrlShortener service")
    yield
    await engine.dispose()
    logger.info("Shutting down UrlShortener service")


app = FastAPI(
    title="UrlShortener",
    version="0.1.0",
    description="URL Shortener service — generated from formal specification",
    lifespan=lifespan,
)

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

app.include_router(url_shortener.router)


@app.get("/health", tags=["infrastructure"])
async def health_check() -> dict[str, str]:
    return {"status": "ok"}


@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception) -> JSONResponse:
    logger.exception("Unhandled exception on %s %s", request.method, request.url.path)
    return JSONResponse(status_code=500, content={"detail": "Internal server error"})
config.py
"""Application configuration loaded from environment variables.

Generated by spec-to-rest compiler.
"""

from pydantic_settings import BaseSettings


class Settings(BaseSettings):
    database_url: str = "postgresql+psycopg://shortener:shortener@localhost:5432/shortener"
    base_url: str = "http://localhost:8000"
    log_level: str = "info"

    model_config = {"env_file": ".env", "env_file_encoding": "utf-8"}


settings = Settings()
database.py
"""SQLAlchemy async engine and session factory.

Generated by spec-to-rest compiler.
"""

from collections.abc import AsyncIterator

from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine

from app.config import settings

engine = create_async_engine(
    settings.database_url,
    echo=False,
    pool_size=10,
    max_overflow=20,
    pool_pre_ping=True,
)

async_session_factory = async_sessionmaker(engine, expire_on_commit=False)


async def get_session() -> AsyncIterator[AsyncSession]:
    async with async_session_factory() as session:
        try:
            yield session
            await session.commit()
        except Exception:
            await session.rollback()
            raise
models/SQLAlchemy ORM (one per entity)
__init__.py
from app.models.url_shortener import Base, StoreEntry

__all__ = ["Base", "StoreEntry"]
url_shortener.py
"""SQLAlchemy ORM models for UrlShortener.

Generated from spec state declarations:
    state {
        store: ShortCode -> lone LongURL
        created_at: ShortCode -> DateTime
    }
"""

from datetime import datetime

from sqlalchemy import CheckConstraint, DateTime, String, Text, func
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column


class Base(DeclarativeBase):
    pass


class StoreEntry(Base):
    """Represents one entry in the `store` relation: ShortCode -> lone LongURL.

    Invariants enforced at the database level:
        - len(code) >= 6 and len(code) <= 10
        - code matches [a-zA-Z0-9]+
        - url is a valid URI (basic prefix check)
    """

    __tablename__ = "store"

    code: Mapped[str] = mapped_column(
        String(10), primary_key=True, nullable=False
    )
    url: Mapped[str] = mapped_column(
        Text, nullable=False
    )
    created_at: Mapped[datetime] = mapped_column(
        DateTime(timezone=True), server_default=func.now(), nullable=False
    )

    __table_args__ = (
        CheckConstraint("length(code) >= 6 AND length(code) <= 10", name="ck_code_length"),
        CheckConstraint("code ~ '^[a-zA-Z0-9]+$'", name="ck_code_pattern"),
        CheckConstraint("url ~ '^https?://'", name="ck_url_valid_uri"),
    )

    def __repr__(self) -> str:
        return f"StoreEntry(code={self.code!r}, url={self.url!r})"
schemas/Pydantic Read / Create / Update DTOs
__init__.py
from app.schemas.url_shortener import (
    ErrorResponse,
    ShortenRequest,
    ShortenResponse,
    ResolveResponse,
)

__all__ = ["ErrorResponse", "ShortenRequest", "ShortenResponse", "ResolveResponse"]
url_shortener.py
"""Pydantic schemas for UrlShortener request/response models.

Generated from spec entity and operation declarations.
"""

import re
from datetime import datetime

from pydantic import BaseModel, Field, field_validator


class ShortenRequest(BaseModel):
    """Input schema for the Shorten operation.

    Derived from:
        operation Shorten { input: url: LongURL }
        requires: isValidURI(url.value)
    """

    url: str = Field(
        ...,
        description="The long URL to shorten",
        examples=["https://example.com/very/long/path?query=1"],
    )

    @field_validator("url")
    @classmethod
    def validate_url(cls, v: str) -> str:
        if not re.match(r"^https?://\S+$", v):
            msg = "Must be a valid HTTP or HTTPS URL"
            raise ValueError(msg)
        return v


class ShortenResponse(BaseModel):
    """Output schema for the Shorten operation.

    Derived from:
        operation Shorten { output: code: ShortCode, short_url: String }
    """

    code: str = Field(
        ...,
        min_length=6,
        max_length=10,
        pattern=r"^[a-zA-Z0-9]+$",
        description="The generated short code",
    )
    short_url: str = Field(
        ...,
        description="The full short URL (base_url + / + code)",
    )
    created_at: datetime = Field(
        ...,
        description="Timestamp when the short URL was created",
    )


class ResolveResponse(BaseModel):
    """Output schema for the Resolve operation.

    Derived from:
        operation Resolve { output: url: LongURL }
    """

    url: str = Field(..., description="The original long URL")


class ErrorResponse(BaseModel):
    """Standard error response body."""

    detail: str = Field(..., description="Human-readable error description")
routers/HTTP route handlers
__init__.py
url_shortener.py
"""FastAPI route handlers for UrlShortener.

Generated from spec operations via the convention engine:
    Shorten  (mutates state, has input)  -> POST /shorten
    Resolve  (reads state, no mutation)  -> GET  /{code}   (302 redirect per convention override)
    Delete   (removes from state)        -> DELETE /{code}

Convention overrides applied:
    Resolve.http_status_success = 302
    Resolve.http_header "Location" = output.url
"""

from fastapi import APIRouter, Depends, HTTPException, Response
from sqlalchemy.ext.asyncio import AsyncSession

from app.database import get_session
from app.schemas.url_shortener import (
    ErrorResponse,
    ResolveResponse,
    ShortenRequest,
    ShortenResponse,
)
from app.services.url_shortener import UrlShortenerService

router = APIRouter(tags=["url_shortener"])


@router.post(
    "/shorten",
    response_model=ShortenResponse,
    status_code=201,
    responses={422: {"model": ErrorResponse, "description": "Validation error"}},
    summary="Shorten a URL",
    description="Creates a new short code mapping to the provided long URL.",
)
async def shorten(
    body: ShortenRequest,
    session: AsyncSession = Depends(get_session),
) -> ShortenResponse:
    service = UrlShortenerService(session)
    return await service.shorten(body)


@router.get(
    "/{code}",
    status_code=302,
    responses={
        302: {"description": "Redirect to the long URL"},
        404: {"model": ErrorResponse, "description": "Short code not found"},
    },
    summary="Resolve a short code",
    description="Looks up the short code and redirects to the original URL.",
)
async def resolve(
    code: str,
    session: AsyncSession = Depends(get_session),
) -> Response:
    service = UrlShortenerService(session)
    result = await service.resolve(code)
    if result is None:
        raise HTTPException(status_code=404, detail=f"Short code '{code}' not found")
    return Response(status_code=302, headers={"Location": result.url})


@router.delete(
    "/{code}",
    status_code=204,
    responses={404: {"model": ErrorResponse, "description": "Short code not found"}},
    summary="Delete a short code",
    description="Removes the short code mapping from the store.",
)
async def delete(
    code: str,
    session: AsyncSession = Depends(get_session),
) -> Response:
    service = UrlShortenerService(session)
    deleted = await service.delete(code)
    if not deleted:
        raise HTTPException(status_code=404, detail=f"Short code '{code}' not found")
    return Response(status_code=204)
services/business logic invoked by routers
__init__.py
url_shortener.py
"""Business logic service for UrlShortener.

Generated by spec-to-rest compiler.

For CRUD operations, the convention engine emits the implementation directly.
For non-trivial logic (e.g., short code generation), the Dafny-verified output
is integrated here. The generate_short_code function below comes from the
verified Dafny compilation; the rest is convention-engine boilerplate.
"""

import secrets
import string
from datetime import datetime, timezone

from sqlalchemy import select, delete as sa_delete
from sqlalchemy.ext.asyncio import AsyncSession

from app.config import settings
from app.models.url_shortener import StoreEntry
from app.schemas.url_shortener import (
    ResolveResponse,
    ShortenRequest,
    ShortenResponse,
)
from app.validators.url_shortener import validate_short_code, validate_long_url


# ---------------------------------------------------------------------------
# Dafny-verified short code generation
# ---------------------------------------------------------------------------
# The function below is the adapter wrapping Dafny-compiled output.
# Dafny ensures:
#   - len(result) >= 6 and len(result) <= 10
#   - result matches [a-zA-Z0-9]+
#   - result is distinct from all codes in the provided existing_codes set
#
# The Dafny source guarantees these postconditions are met for every call.
# See Section 5 (Dafny Output Integration) for the full compilation pipeline.
# ---------------------------------------------------------------------------

_ALPHABET = string.ascii_letters + string.digits  # [a-zA-Z0-9]


def generate_short_code(existing_codes: set[str], length: int = 8) -> str:
    """Generate a fresh short code not present in existing_codes.

    Verified postconditions (from Dafny):
        ensures len(result) >= 6 and len(result) <= 10
        ensures result matches [a-zA-Z0-9]+
        ensures result not in existing_codes
    """
    if length < 6 or length > 10:
        length = 8
    for _ in range(1000):
        candidate = "".join(secrets.choice(_ALPHABET) for _ in range(length))
        if candidate not in existing_codes:
            return candidate
    # Fallback: extend length within invariant bounds
    for fallback_len in range(6, 11):
        candidate = "".join(secrets.choice(_ALPHABET) for _ in range(fallback_len))
        if candidate not in existing_codes:
            return candidate
    raise RuntimeError("Failed to generate a unique short code after exhaustive attempts")


class UrlShortenerService:
    """Service layer implementing UrlShortener operations.

    Each method corresponds to a spec operation and enforces the spec's
    requires/ensures contract.
    """

    def __init__(self, session: AsyncSession) -> None:
        self._session = session

    async def shorten(self, request: ShortenRequest) -> ShortenResponse:
        """Implements operation Shorten.

        requires: isValidURI(url.value)
        ensures:
            code not in pre(store)
            store'[code] = url
            short_url = base_url + "/" + code.value
            #store' = #store + 1
        """
        validate_long_url(request.url)

        # Gather existing codes to satisfy the Dafny-verified uniqueness precondition
        result = await self._session.execute(select(StoreEntry.code))
        existing_codes = {row[0] for row in result.fetchall()}

        code = generate_short_code(existing_codes)

        now = datetime.now(timezone.utc)
        entry = StoreEntry(code=code, url=request.url, created_at=now)
        self._session.add(entry)
        await self._session.flush()

        short_url = f"{settings.base_url}/{code}"
        return ShortenResponse(code=code, short_url=short_url, created_at=now)

    async def resolve(self, code: str) -> ResolveResponse | None:
        """Implements operation Resolve.

        requires: code in store
        ensures:
            url = store[code]
            store' = store   (state unchanged)
        """
        validate_short_code(code)
        result = await self._session.execute(
            select(StoreEntry).where(StoreEntry.code == code)
        )
        entry = result.scalar_one_or_none()
        if entry is None:
            return None
        return ResolveResponse(url=entry.url)

    async def delete(self, code: str) -> bool:
        """Implements operation Delete.

        requires: code in store
        ensures:
            code not in store'
            #store' = #store - 1
        """
        validate_short_code(code)
        result = await self._session.execute(
            sa_delete(StoreEntry).where(StoreEntry.code == code)
        )
        return result.rowcount > 0  # type: ignore[return-value]
validators/checks lifted from `requires` clauses
__init__.py
url_shortener.py
"""Validators derived from spec requires clauses and entity invariants.

Generated by spec-to-rest compiler.

Source constraints:
    entity ShortCode {
        invariant: len(value) >= 6 and len(value) <= 10
        invariant: value matches /^[a-zA-Z0-9]+$/
    }
    entity LongURL {
        invariant: isValidURI(value)
    }
"""

import re

from fastapi import HTTPException


_SHORT_CODE_PATTERN = re.compile(r"^[a-zA-Z0-9]+$")
_URL_PATTERN = re.compile(r"^https?://\S+$")


def validate_short_code(value: str) -> None:
    """Validate a ShortCode value against entity invariants.

    Raises HTTPException 422 if invalid.
    """
    if len(value) < 6 or len(value) > 10:
        raise HTTPException(
            status_code=422,
            detail=f"Short code must be 6-10 characters, got {len(value)}",
        )
    if not _SHORT_CODE_PATTERN.match(value):
        raise HTTPException(
            status_code=422,
            detail="Short code must contain only alphanumeric characters [a-zA-Z0-9]",
        )


def validate_long_url(value: str) -> None:
    """Validate a LongURL value against entity invariants.

    Raises HTTPException 422 if invalid.
    """
    if not _URL_PATTERN.match(value):
        raise HTTPException(
            status_code=422,
            detail="URL must be a valid HTTP or HTTPS URI",
        )
tests/
__init__.py
conftest.py
"""Pytest fixtures for UrlShortener test suite.

Generated by spec-to-rest compiler.
"""

import asyncio
from collections.abc import AsyncIterator

import pytest
from httpx import ASGITransport, AsyncClient
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker

from app.main import app
from app.database import get_session
from app.models.url_shortener import Base

TEST_DATABASE_URL = "sqlite+aiosqlite:///test.db"

test_engine = create_async_engine(TEST_DATABASE_URL, echo=False)
test_session_factory = async_sessionmaker(test_engine, expire_on_commit=False)


@pytest.fixture(autouse=True)
async def setup_database():
    async with test_engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)
    yield
    async with test_engine.begin() as conn:
        await conn.run_sync(Base.metadata.drop_all)


async def override_get_session() -> AsyncIterator[AsyncSession]:
    async with test_session_factory() as session:
        try:
            yield session
            await session.commit()
        except Exception:
            await session.rollback()
            raise


app.dependency_overrides[get_session] = override_get_session


@pytest.fixture
async def client() -> AsyncIterator[AsyncClient]:
    transport = ASGITransport(app=app)
    async with AsyncClient(transport=transport, base_url="http://test") as ac:
        yield ac
test_api.py
"""API integration tests derived from spec operations.

Generated by spec-to-rest compiler.

Each test corresponds to a spec operation and validates the requires/ensures contract
at the HTTP level.
"""

import pytest
from httpx import AsyncClient


class TestShortenOperation:
    """Tests for POST /shorten (operation Shorten)."""

    async def test_shorten_valid_url(self, client: AsyncClient) -> None:
        """ensures: code not in pre(store), store'[code] = url, #store' = #store + 1"""
        response = await client.post("/shorten", json={"url": "https://example.com/long"})
        assert response.status_code == 201
        data = response.json()
        assert "code" in data
        assert "short_url" in data
        assert len(data["code"]) >= 6
        assert len(data["code"]) <= 10
        assert data["short_url"].endswith(data["code"])

    async def test_shorten_invalid_url_returns_422(self, client: AsyncClient) -> None:
        """requires: isValidURI(url.value) -> 422 on violation"""
        response = await client.post("/shorten", json={"url": "not-a-valid-url"})
        assert response.status_code == 422

    async def test_shorten_missing_url_returns_422(self, client: AsyncClient) -> None:
        response = await client.post("/shorten", json={})
        assert response.status_code == 422

    async def test_shorten_produces_unique_codes(self, client: AsyncClient) -> None:
        """ensures: code not in pre(store) — each code is fresh"""
        codes = set()
        for _ in range(20):
            resp = await client.post("/shorten", json={"url": "https://example.com/"})
            assert resp.status_code == 201
            codes.add(resp.json()["code"])
        assert len(codes) == 20


class TestResolveOperation:
    """Tests for GET /{code} (operation Resolve)."""

    async def test_resolve_existing_code_redirects(self, client: AsyncClient) -> None:
        """ensures: url = store[code], convention override: 302 + Location header"""
        create = await client.post("/shorten", json={"url": "https://example.com/target"})
        code = create.json()["code"]

        response = await client.get(f"/{code}", follow_redirects=False)
        assert response.status_code == 302
        assert response.headers["location"] == "https://example.com/target"

    async def test_resolve_nonexistent_code_returns_404(self, client: AsyncClient) -> None:
        """requires: code in store -> 404 when violated"""
        response = await client.get("/aBcDeFgH", follow_redirects=False)
        assert response.status_code == 404

    async def test_resolve_does_not_mutate_state(self, client: AsyncClient) -> None:
        """ensures: store' = store (state unchanged)"""
        create = await client.post("/shorten", json={"url": "https://example.com/stable"})
        code = create.json()["code"]

        await client.get(f"/{code}", follow_redirects=False)
        await client.get(f"/{code}", follow_redirects=False)

        response = await client.get(f"/{code}", follow_redirects=False)
        assert response.status_code == 302
        assert response.headers["location"] == "https://example.com/stable"


class TestDeleteOperation:
    """Tests for DELETE /{code} (operation Delete)."""

    async def test_delete_existing_code(self, client: AsyncClient) -> None:
        """ensures: code not in store', #store' = #store - 1"""
        create = await client.post("/shorten", json={"url": "https://example.com/gone"})
        code = create.json()["code"]

        response = await client.delete(f"/{code}")
        assert response.status_code == 204

        # Verify removal
        resolve = await client.get(f"/{code}", follow_redirects=False)
        assert resolve.status_code == 404

    async def test_delete_nonexistent_code_returns_404(self, client: AsyncClient) -> None:
        """requires: code in store -> 404 when violated"""
        response = await client.delete("/aBcDeFgH")
        assert response.status_code == 404


class TestHealthCheck:
    """Infrastructure endpoint test."""

    async def test_health(self, client: AsyncClient) -> None:
        response = await client.get("/health")
        assert response.status_code == 200
        assert response.json() == {"status": "ok"}
test_properties.py
"""Property-based tests derived from spec ensures clauses.

Generated by spec-to-rest compiler.
Uses Hypothesis to check that postconditions hold for arbitrary valid inputs.
"""

import re
import string

from hypothesis import given, settings, strategies as st

from app.services.url_shortener import generate_short_code

alphanumeric = st.text(alphabet=string.ascii_letters + string.digits, min_size=6, max_size=10)


class TestShortCodeGeneration:
    """Properties of generate_short_code — from Dafny verified postconditions."""

    @given(
        existing=st.frozensets(alphanumeric, max_size=50),
        length=st.integers(min_value=6, max_value=10),
    )
    @settings(max_examples=200)
    def test_code_length_within_bounds(self, existing: frozenset[str], length: int) -> None:
        """ensures: len(result) >= 6 and len(result) <= 10"""
        code = generate_short_code(set(existing), length)
        assert 6 <= len(code) <= 10

    @given(
        existing=st.frozensets(alphanumeric, max_size=50),
        length=st.integers(min_value=6, max_value=10),
    )
    @settings(max_examples=200)
    def test_code_matches_pattern(self, existing: frozenset[str], length: int) -> None:
        """ensures: result matches [a-zA-Z0-9]+"""
        code = generate_short_code(set(existing), length)
        assert re.fullmatch(r"[a-zA-Z0-9]+", code)

    @given(
        existing=st.frozensets(alphanumeric, max_size=50),
        length=st.integers(min_value=6, max_value=10),
    )
    @settings(max_examples=200)
    def test_code_is_fresh(self, existing: frozenset[str], length: int) -> None:
        """ensures: result not in existing_codes"""
        code = generate_short_code(set(existing), length)
        assert code not in existing


class TestStoreInvariants:
    """Global invariants from the spec:
        invariant: all c in store | isValidURI(store[c].value)
        invariant: all c in store | c in created_at
    These are enforced at the DB level via CHECK constraints and NOT NULL columns.
    This test class validates the invariant logic in isolation.
    """

    @given(url=st.text(min_size=1, max_size=200))
    def test_valid_uri_invariant(self, url: str) -> None:
        """invariant: all c in store | isValidURI(store[c].value)
        Any URL stored must match ^https?://
        """
        is_valid = bool(re.match(r"^https?://\S+$", url))
        # This invariant means invalid URLs should never pass validation
        if not is_valid:
            from fastapi import HTTPException
            import pytest
            from app.validators.url_shortener import validate_long_url

            with pytest.raises(HTTPException) as exc_info:
                validate_long_url(url)
            assert exc_info.value.status_code == 422
test_conformance.py
"""Schemathesis conformance test configuration.

Generated by spec-to-rest compiler.

Run with:
    schemathesis run openapi.yaml --base-url http://localhost:8000 --stateful=links

Or use this file as a pytest-based Schemathesis runner:
    pytest tests/test_conformance.py -v
"""

import schemathesis

schema = schemathesis.from_path(
    "openapi.yaml",
    base_url="http://localhost:8000",
)


@schema.parametrize()
def test_api_conformance(case):
    """Every endpoint must conform to the OpenAPI schema.

    Schemathesis generates random valid inputs and checks:
    - Response status codes match declared responses
    - Response bodies match declared schemas
    - No 500 errors for valid inputs
    - Content-Type headers are correct
    """
    case.call_and_validate()
openapi.yaml

(Full generated spec shown in Section 4 below.)


1.2 Go-chi-postgres

Directory structure

url_shortener/
go.mod
module github.com/generated/url-shortener

go 1.22

require (
    github.com/go-chi/chi/v5 v5.1.0
    github.com/jackc/pgx/v5 v5.7.1
    github.com/golang-migrate/migrate/v4 v4.18.1
    github.com/caarlos0/env/v11 v11.2.0
    github.com/stretchr/testify v1.9.0
    pgregory.net/rapid v1.1.0
)
go.sum
Dockerfile
docker-compose.yml
.env.example
Makefile
cmd/Go-idiom entry-point binaries
server/
main.go
// Package main is the entry point for the UrlShortener service.
//
// Generated by spec-to-rest compiler.
package main

import (
    "context"
    "fmt"
    "log/slog"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"

    "github.com/go-chi/chi/v5"
    "github.com/go-chi/chi/v5/middleware"

    "github.com/generated/url-shortener/internal/config"
    "github.com/generated/url-shortener/internal/database"
    "github.com/generated/url-shortener/internal/handlers"
    "github.com/generated/url-shortener/internal/services"
)

func main() {
    logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
    slog.SetDefault(logger)

    cfg, err := config.Load()
    if err != nil {
        slog.Error("failed to load config", "error", err)
        os.Exit(1)
    }

    pool, err := database.Connect(context.Background(), cfg.DatabaseURL)
    if err != nil {
        slog.Error("failed to connect to database", "error", err)
        os.Exit(1)
    }
    defer pool.Close()

    svc := services.NewUrlShortenerService(pool, cfg.BaseURL)
    h := handlers.NewUrlShortenerHandler(svc)

    r := chi.NewRouter()
    r.Use(middleware.RequestID)
    r.Use(middleware.RealIP)
    r.Use(middleware.Logger)
    r.Use(middleware.Recoverer)
    r.Use(middleware.Timeout(30 * time.Second))

    r.Get("/health", func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        w.Write([]byte(`{"status":"ok"}`))
    })

    r.Post("/shorten", h.Shorten)
    r.Get("/{code}", h.Resolve)
    r.Delete("/{code}", h.Delete)

    server := &http.Server{
        Addr:         fmt.Sprintf(":%d", cfg.Port),
        Handler:      r,
        ReadTimeout:  10 * time.Second,
        WriteTimeout: 30 * time.Second,
        IdleTimeout:  60 * time.Second,
    }

    done := make(chan os.Signal, 1)
    signal.Notify(done, os.Interrupt, syscall.SIGTERM)

    go func() {
        slog.Info("starting server", "port", cfg.Port)
        if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            slog.Error("server error", "error", err)
            os.Exit(1)
        }
    }()

    <-done
    slog.Info("shutting down")
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()
    _ = server.Shutdown(ctx)
}
internal/private packages (un-importable externally)
config/
config.go
// Package config loads application configuration from environment variables.
//
// Generated by spec-to-rest compiler.
package config

import "github.com/caarlos0/env/v11"

type Config struct {
    DatabaseURL string `env:"DATABASE_URL" envDefault:"postgres://shortener:shortener@localhost:5432/shortener?sslmode=disable"`
    BaseURL     string `env:"BASE_URL"     envDefault:"http://localhost:8080"`
    Port        int    `env:"PORT"         envDefault:"8080"`
    LogLevel    string `env:"LOG_LEVEL"    envDefault:"info"`
}

func Load() (*Config, error) {
    cfg := &Config{}
    if err := env.Parse(cfg); err != nil {
        return nil, err
    }
    return cfg, nil
}
database/
database.go
models/struct definitions (one per entity)
url_shortener.go
// Package models defines data types for UrlShortener.
//
// Generated from spec entity and state declarations.
package models

import "time"

// StoreEntry represents one row of the `store` relation: ShortCode -> lone LongURL.
type StoreEntry struct {
    Code      string    `json:"code"       db:"code"`
    URL       string    `json:"url"        db:"url"`
    CreatedAt time.Time `json:"created_at" db:"created_at"`
}

// ShortenRequest is the input for operation Shorten.
type ShortenRequest struct {
    URL string `json:"url"`
}

// ShortenResponse is the output for operation Shorten.
type ShortenResponse struct {
    Code      string    `json:"code"`
    ShortURL  string    `json:"short_url"`
    CreatedAt time.Time `json:"created_at"`
}

// ErrorResponse is the standard error body.
type ErrorResponse struct {
    Detail string `json:"detail"`
}
handlers/chi route handlers
url_shortener.go
// Package handlers contains HTTP route handlers for UrlShortener.
//
// Generated from spec operations via the convention engine.
package handlers

import (
    "encoding/json"
    "net/http"

    "github.com/go-chi/chi/v5"

    "github.com/generated/url-shortener/internal/models"
    "github.com/generated/url-shortener/internal/services"
    "github.com/generated/url-shortener/internal/validators"
)

type UrlShortenerHandler struct {
    svc *services.UrlShortenerService
}

func NewUrlShortenerHandler(svc *services.UrlShortenerService) *UrlShortenerHandler {
    return &UrlShortenerHandler{svc: svc}
}

func (h *UrlShortenerHandler) Shorten(w http.ResponseWriter, r *http.Request) {
    var req models.ShortenRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        writeError(w, http.StatusBadRequest, "Invalid JSON body")
        return
    }

    if err := validators.ValidateLongURL(req.URL); err != nil {
        writeError(w, http.StatusUnprocessableEntity, err.Error())
        return
    }

    resp, err := h.svc.Shorten(r.Context(), req)
    if err != nil {
        writeError(w, http.StatusInternalServerError, "Failed to shorten URL")
        return
    }

    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(resp)
}

func (h *UrlShortenerHandler) Resolve(w http.ResponseWriter, r *http.Request) {
    code := chi.URLParam(r, "code")
    if err := validators.ValidateShortCode(code); err != nil {
        writeError(w, http.StatusUnprocessableEntity, err.Error())
        return
    }

    entry, err := h.svc.Resolve(r.Context(), code)
    if err != nil {
        writeError(w, http.StatusInternalServerError, "Failed to resolve code")
        return
    }
    if entry == nil {
        writeError(w, http.StatusNotFound, "Short code not found")
        return
    }

    // Convention override: Resolve.http_status_success = 302
    http.Redirect(w, r, entry.URL, http.StatusFound)
}

func (h *UrlShortenerHandler) Delete(w http.ResponseWriter, r *http.Request) {
    code := chi.URLParam(r, "code")
    if err := validators.ValidateShortCode(code); err != nil {
        writeError(w, http.StatusUnprocessableEntity, err.Error())
        return
    }

    deleted, err := h.svc.Delete(r.Context(), code)
    if err != nil {
        writeError(w, http.StatusInternalServerError, "Failed to delete code")
        return
    }
    if !deleted {
        writeError(w, http.StatusNotFound, "Short code not found")
        return
    }

    w.WriteHeader(http.StatusNoContent)
}

func writeError(w http.ResponseWriter, status int, detail string) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(models.ErrorResponse{Detail: detail})
}
services/business logic invoked by handlers
url_shortener.go
// Package services contains business logic for UrlShortener.
//
// Generated by spec-to-rest compiler.
package services

import (
    "context"
    "crypto/rand"
    "errors"
    "fmt"
    "math/big"
    "time"

    "github.com/jackc/pgx/v5"
    "github.com/jackc/pgx/v5/pgxpool"

    "github.com/generated/url-shortener/internal/models"
)

const alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"

type UrlShortenerService struct {
    pool    *pgxpool.Pool
    baseURL string
}

func NewUrlShortenerService(pool *pgxpool.Pool, baseURL string) *UrlShortenerService {
    return &UrlShortenerService{pool: pool, baseURL: baseURL}
}

// generateShortCode produces a fresh alphanumeric code of the given length.
// Dafny-verified postconditions:
//   - len(result) >= 6 && len(result) <= 10
//   - result matches [a-zA-Z0-9]+
//   - result is not in the current store
func generateShortCode(length int) (string, error) {
    if length < 6 || length > 10 {
        length = 8
    }
    b := make([]byte, length)
    for i := range b {
        n, err := rand.Int(rand.Reader, big.NewInt(int64(len(alphabet))))
        if err != nil {
            return "", fmt.Errorf("crypto/rand failed: %w", err)
        }
        b[i] = alphabet[n.Int64()]
    }
    return string(b), nil
}

func (s *UrlShortenerService) Shorten(ctx context.Context, req models.ShortenRequest) (*models.ShortenResponse, error) {
    // Retry loop to guarantee code uniqueness (Dafny postcondition: code not in pre(store))
    for attempts := 0; attempts < 10; attempts++ {
        code, err := generateShortCode(8)
        if err != nil {
            return nil, err
        }

        now := time.Now().UTC()
        tag, err := s.pool.Exec(ctx,
            `INSERT INTO store (code, url, created_at) VALUES ($1, $2, $3) ON CONFLICT (code) DO NOTHING`,
            code, req.URL, now,
        )
        if err != nil {
            return nil, fmt.Errorf("insert failed: %w", err)
        }
        if tag.RowsAffected() == 1 {
            return &models.ShortenResponse{
                Code:      code,
                ShortURL:  fmt.Sprintf("%s/%s", s.baseURL, code),
                CreatedAt: now,
            }, nil
        }
        // Conflict: code already exists, retry with new code
    }
    return nil, fmt.Errorf("failed to generate unique short code after 10 attempts")
}

func (s *UrlShortenerService) Resolve(ctx context.Context, code string) (*models.StoreEntry, error) {
    row := s.pool.QueryRow(ctx, `SELECT code, url, created_at FROM store WHERE code = $1`, code)
    var entry models.StoreEntry
    err := row.Scan(&entry.Code, &entry.URL, &entry.CreatedAt)
    if err != nil {
        if errors.Is(err, pgx.ErrNoRows) {
            return nil, nil
        }
        return nil, fmt.Errorf("query failed: %w", err)
    }
    return &entry, nil
}

func (s *UrlShortenerService) Delete(ctx context.Context, code string) (bool, error) {
    tag, err := s.pool.Exec(ctx, `DELETE FROM store WHERE code = $1`, code)
    if err != nil {
        return false, fmt.Errorf("delete failed: %w", err)
    }
    return tag.RowsAffected() > 0, nil
}
validators/checks lifted from `requires` clauses
url_shortener.go
migrations/raw SQL up/down migrations
001_initial_schema.up.sql
CREATE TABLE store (
    code       VARCHAR(10)   NOT NULL PRIMARY KEY,
    url        TEXT          NOT NULL,
    created_at TIMESTAMPTZ   NOT NULL DEFAULT now(),

    CONSTRAINT ck_code_length  CHECK (length(code) >= 6 AND length(code) <= 10),
    CONSTRAINT ck_code_pattern CHECK (code ~ '^[a-zA-Z0-9]+$'),
    CONSTRAINT ck_url_valid    CHECK (url ~ '^https?://')
);

CREATE INDEX ix_store_created_at ON store (created_at);
001_initial_schema.down.sql
DROP TABLE IF EXISTS store;
tests/
api_test.go
properties_test.go
openapi.yaml

1.3 Typescript-express-prisma

Directory structure

url_shortener/
package.json
{
  "name": "url-shortener",
  "version": "0.1.0",
  "description": "URL Shortener service — generated by spec-to-rest compiler",
  "main": "dist/index.js",
  "scripts": {
    "build": "tsc",
    "start": "node dist/index.js",
    "dev": "tsx watch src/index.ts",
    "test": "vitest run",
    "lint": "eslint src/ tests/",
    "format": "prettier --check src/ tests/",
    "migrate": "prisma migrate deploy",
    "generate": "prisma generate"
  },
  "dependencies": {
    "express": "^4.21.0",
    "@prisma/client": "^5.20.0",
    "zod": "^3.23.0",
    "dotenv": "^16.4.0"
  },
  "devDependencies": {
    "typescript": "^5.6.0",
    "tsx": "^4.19.0",
    "@types/express": "^5.0.0",
    "@types/node": "^22.0.0",
    "prisma": "^5.20.0",
    "vitest": "^2.1.0",
    "supertest": "^7.0.0",
    "@types/supertest": "^6.0.0",
    "eslint": "^9.12.0",
    "prettier": "^3.3.0",
    "fast-check": "^3.22.0"
  }
}
tsconfig.json
Dockerfile
docker-compose.yml
.env.example
prisma/Prisma schema + generated client
schema.prisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

/// Represents the `store` relation: ShortCode -> lone LongURL
/// Generated from spec state declaration.
model Store {
  code      String   @id @db.VarChar(10)
  url       String   @db.Text
  createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()

  @@map("store")
}
src/Express service code
index.ts
import { createApp } from "./app";
import { config } from "./config";

async function main(): Promise<void> {
  const app = createApp();

  app.listen(config.port, () => {
    console.log(`UrlShortener service listening on port ${config.port}`);
  });

  process.on("SIGTERM", () => {
    console.log("SIGTERM received, shutting down");
    process.exit(0);
  });
}

main().catch((err) => {
  console.error("Failed to start server:", err);
  process.exit(1);
});
config.ts
import "dotenv/config";

export const config = {
  databaseUrl:
    process.env.DATABASE_URL ?? "postgresql://shortener:shortener@localhost:5432/shortener",
  baseUrl: process.env.BASE_URL ?? "http://localhost:3000",
  port: parseInt(process.env.PORT ?? "3000", 10),
} as const;
app.ts
import express, { type Express, type Request, type Response, type NextFunction } from "express";
import { urlShortenerRouter } from "./routes/urlShortener";

export function createApp(): Express {
  const app = express();

  app.use(express.json());

  app.get("/health", (_req: Request, res: Response) => {
    res.json({ status: "ok" });
  });

  app.use("/", urlShortenerRouter);

  // Global error handler
  app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
    console.error("Unhandled error:", err);
    res.status(500).json({ detail: "Internal server error" });
  });

  return app;
}
routes/Express route handlers
urlShortener.ts
/** Express route handlers for UrlShortener.
 *
 * Generated from spec operations via the convention engine.
 */

import { Router, type Request, type Response } from "express";
import { ShortenRequestSchema, ShortCodeSchema } from "../validators/urlShortener";
import * as service from "../services/urlShortener";

export const urlShortenerRouter = Router();

// POST /shorten — operation Shorten (mutates state, has input)
urlShortenerRouter.post("/shorten", async (req: Request, res: Response) => {
  const parsed = ShortenRequestSchema.safeParse(req.body);
  if (!parsed.success) {
    res.status(422).json({ detail: parsed.error.issues[0].message });
    return;
  }

  try {
    const result = await service.shorten(parsed.data);
    res.status(201).json(result);
  } catch (err) {
    console.error("Shorten error:", err);
    res.status(500).json({ detail: "Failed to shorten URL" });
  }
});

// GET /:code — operation Resolve (reads state, no mutation; convention override: 302)
urlShortenerRouter.get("/:code", async (req: Request, res: Response) => {
  const parsed = ShortCodeSchema.safeParse(req.params.code);
  if (!parsed.success) {
    res.status(422).json({ detail: parsed.error.issues[0].message });
    return;
  }

  try {
    const result = await service.resolve(parsed.data);
    if (!result) {
      res.status(404).json({ detail: "Short code not found" });
      return;
    }
    // Convention override: Resolve.http_status_success = 302
    res.redirect(302, result.url);
  } catch (err) {
    console.error("Resolve error:", err);
    res.status(500).json({ detail: "Failed to resolve code" });
  }
});

// DELETE /:code — operation Delete (removes from state)
urlShortenerRouter.delete("/:code", async (req: Request, res: Response) => {
  const parsed = ShortCodeSchema.safeParse(req.params.code);
  if (!parsed.success) {
    res.status(422).json({ detail: parsed.error.issues[0].message });
    return;
  }

  try {
    const deleted = await service.deleteCode(parsed.data);
    if (!deleted) {
      res.status(404).json({ detail: "Short code not found" });
      return;
    }
    res.status(204).end();
  } catch (err) {
    console.error("Delete error:", err);
    res.status(500).json({ detail: "Failed to delete code" });
  }
});
services/business logic invoked by routes
urlShortener.ts
/** Business logic service for UrlShortener.
 *
 * Generated by spec-to-rest compiler.
 */

import { PrismaClient } from "@prisma/client";
import crypto from "node:crypto";
import { config } from "../config";
import type { ShortenRequest, ShortenResponse, ResolveResponse } from "../types/urlShortener";

const ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";

const prisma = new PrismaClient();

/**
 * Generate a fresh short code.
 *
 * Dafny-verified postconditions:
 *   - length >= 6 && length <= 10
 *   - matches /^[a-zA-Z0-9]+$/
 */
function generateShortCode(length: number = 8): string {
  const clampedLen = Math.max(6, Math.min(10, length));
  const bytes = crypto.randomBytes(clampedLen);
  let result = "";
  for (let i = 0; i < clampedLen; i++) {
    result += ALPHABET[bytes[i] % ALPHABET.length];
  }
  return result;
}

export async function shorten(req: ShortenRequest): Promise<ShortenResponse> {
  for (let attempt = 0; attempt < 10; attempt++) {
    const code = generateShortCode(8);
    try {
      const entry = await prisma.store.create({
        data: { code, url: req.url },
      });
      return {
        code: entry.code,
        short_url: `${config.baseUrl}/${entry.code}`,
        created_at: entry.createdAt.toISOString(),
      };
    } catch (err: unknown) {
      // Unique constraint violation — retry with new code
      if (isPrismaUniqueViolation(err)) continue;
      throw err;
    }
  }
  throw new Error("Failed to generate unique short code after 10 attempts");
}

export async function resolve(code: string): Promise<ResolveResponse | null> {
  const entry = await prisma.store.findUnique({ where: { code } });
  if (!entry) return null;
  return { url: entry.url };
}

export async function deleteCode(code: string): Promise<boolean> {
  try {
    await prisma.store.delete({ where: { code } });
    return true;
  } catch (err: unknown) {
    if (isPrismaNotFound(err)) return false;
    throw err;
  }
}

function isPrismaUniqueViolation(err: unknown): boolean {
  return (err as { code?: string })?.code === "P2002";
}

function isPrismaNotFound(err: unknown): boolean {
  return (err as { code?: string })?.code === "P2025";
}
validators/checks lifted from `requires` clauses
urlShortener.ts
/** Validators derived from spec requires clauses and entity invariants.
 *
 * Generated by spec-to-rest compiler.
 */

import { z } from "zod";

export const ShortCodeSchema = z
  .string()
  .min(6, "Short code must be at least 6 characters")
  .max(10, "Short code must be at most 10 characters")
  .regex(/^[a-zA-Z0-9]+$/, "Short code must be alphanumeric");

export const LongURLSchema = z
  .string()
  .regex(/^https?:\/\/\S+$/, "Must be a valid HTTP or HTTPS URL");

export const ShortenRequestSchema = z.object({
  url: LongURLSchema,
});
types/TypeScript interfaces (one per entity)
urlShortener.ts
/** Request/response types for UrlShortener.
 *
 * Generated from spec entity and operation declarations.
 */

export interface ShortenRequest {
  url: string;
}

export interface ShortenResponse {
  code: string;
  short_url: string;
  created_at: string;
}

export interface ResolveResponse {
  url: string;
}

export interface ErrorResponse {
  detail: string;
}
tests/
api.test.ts
properties.test.ts
openapi.yaml

2. Template architecture

2.1 Template engine selection

Shipped reality. The python-fastapi-postgres target ships with Handlebars templates, not Jinja2. This section captures the original research-time recommendation; the implementation chose Handlebars because the codegen module is JVM/Scala-resident and Handlebars-Java integrates directly without a Python sidecar. The trade-off discussion below still applies if a future target wants to revisit the choice.

We use a hybrid approach: Jinja2 as the primary template engine with a thin orchestration layer written in Python.

Why Jinja2

CriterionJinja2Go templatesStringTemplateCustom
FamiliarityHigh (Python dev audience)ModerateLowNone
ExpressivenessRich (filters, macros, inheritance)LimitedModerateUnlimited
Whitespace controlExcellent ({%- -%})AdequateGoodManual
Template inheritanceBuilt-in (extends/block)NoNoCustom
Library ecosystemHugeModerateSmallN/A
PerformanceAdequate (one-shot generation)FastFastVaries

Code generation is a batch process that runs once and produces files on disk. Template rendering speed is irrelevant, a URL shortener spec generates ~25 files in under 100ms regardless of engine. Expressiveness and developer familiarity dominate the choice.

2.2 IR-to-template variable mapping

The IR produced by the spec parser is a Scala 3 ADT (sealed abstract classes + final case classes) extracted from Isabelle by Code_Target_Scala. The Handlebars engine receives a ProfiledService view that wraps the IR with the convention-engine annotations. The shipped types live in modules/ir/src/main/scala/specrest/ir/generated/SpecRestGenerated.scala (extracted; do not hand-edit) and modules/profile/.../Types.scala. The conceptual shape (extracted Scala uses positional letters a/b/c/… instead of English field names, see IR.thy for the source-of-truth definitions):

final case class ServiceIR(
    name: String,                      // "UrlShortener"
    entities: List[EntityDecl],        // UrlMapping
    enums: List[EnumDecl],
    typeAliases: List[TypeAliasDecl],  // ShortCode, LongURL, BaseURL
    state: Option[StateDecl],          // store, metadata, base_url
    operations: List[OperationDecl],   // Shorten, Resolve, Delete, ListAll
    invariants: List[InvariantDecl],   // global invariants
    temporals: List[TemporalDecl],
    facts: List[FactDecl],
    functions: List[FunctionDecl],
    predicates: List[PredicateDecl],
    transitions: List[TransitionDecl],
    conventions: Option[ConventionsDecl]   // user-specified overrides
)

final case class OperationDecl(
    name: String,                      // "Shorten"
    inputs: List[ParamDecl] = Nil,     // url: LongURL
    outputs: List[ParamDecl] = Nil,    // code: ShortCode, short_url: String
    requires: List[Expr] = Nil,        // isValidURI(url)
    ensures: List[Expr] = Nil,         // code not in pre(store), ...
    span: Option[Span] = None
)

// HTTP-shape annotation produced by the convention engine and added by
// modules/profile/.../Annotate.buildProfiledService before templates render.
final case class EndpointSpec(
    operationName: String,             // "Shorten"
    method: HttpMethod,                // POST
    path: String,                      // "/shorten"
    pathParams: List[ParamSpec],
    queryParams: List[ParamSpec],
    bodyParams: List[ParamSpec],
    successStatus: Int                 // 201
)

The convention engine annotates each OperationIR with HTTP mapping decisions before templates are rendered. Templates then access these annotations directly:

@router.{{ op.http_method | lower }}(
    "{{ op.http_path }}",
    status_code={{ op.http_success_status }},
)
async def {{ op.name | snake_case }}(
    {%- for input in op.inputs %}
    {{ input.name }}: {{ input | python_type }},
    {%- endfor %}
    session: AsyncSession = Depends(get_session),
) -> {{ op | python_response_type }}:

2.3 Convention engine feed-in

The convention engine makes decisions that templates consume:

Convention DecisionTemplate VariableExample Value
HTTP methodop.http_method"POST"
URL pathop.http_path"/shorten"
Success status codeop.http_success_status201
Error mappingsop.http_error_responses[(422, "Validation error")]
DB table namestate.table_name"store"
Column typesfield.db_type"VARCHAR(10)"
Index decisionsstate.indexes[("created_at", "btree")]
Constraint SQLentity.check_constraints["length(code) >= 6"]

Convention decisions are made once, stored on the IR, and consumed by every target's templates. This means adding a new convention (e.g., "all list endpoints support cursor pagination") only requires changing the convention engine, all targets inherit the new behavior.

2.4 Dafny output splicing

When the LLM synthesis engine produces verified Dafny code for an operation, it is compiled to the target language and spliced into the service layer:

Dafny source        ->  dafny build --target py  ->  Python module
                                                       |
Template renders    ->  service layer template    ->  imports Dafny module
                                                       |
Adapter wraps       ->  convert Dafny types       ->  native Python types

The template detects whether an operation has Dafny output available:

{% if op.dafny_module %}
from {{ op.dafny_module }} import {{ op.dafny_function }} as _dafny_{{ op.name | snake_case }}
{% endif %}

class {{ service.name }}Service:
    async def {{ op.name | snake_case }}(self, ...) -> ...:
        {% if op.dafny_module %}
        # Verified implementation from Dafny
        result = _dafny_{{ op.name | snake_case }}({{ op.inputs | dafny_args }})
        return {{ op | adapt_dafny_result("result") }}
        {% else %}
        # Convention-generated CRUD implementation
        {{ op | crud_implementation }}
        {% endif %}

2.5 Cross-cutting concerns

Cross-cutting concerns (logging, error handling, auth, rate limiting) are handled via template composition rather than inheritance:

templates/
_base/
logging.py.j2import logging; logger = ...
error_handler.py.j2@app.exception_handler(...)
health_check.py.j2@app.get("/health")
python-fastapi/
main.py.j2{% include "_base/logging.py.j2" %}
router.py.j2
service.py.j2
...
go-chi/
main.go.j2
handler.go.j2
...
typescript-express/
app.ts.j2
route.ts.j2
...

Each target's main template includes the relevant cross-cutting fragments. The base fragments are parameterized to work across targets, they receive the same IR context but produce language-specific output.

2.6 Template organization and quality

Structure

templates/
_shared/Language-agnostic partials
openapi.yaml.j2OpenAPI generation (shared across all targets)
docker-compose.yml.j2Docker composition
makefile.j2Common Makefile targets
python-fastapi-postgres/
manifest.jsonLists all files to generate + their templates
pyproject.toml.j2
dockerfile.j2
alembic_env.py.j2
migration.py.j2
main.py.j2
config.py.j2
database.py.j2
model.py.j2One per entity/state relation
schema.py.j2Pydantic models
router.py.j2Route handlers
service.py.j2Business logic
validator.py.j2From requires clauses
conftest.py.j2Test fixtures
test_api.py.j2API integration tests
test_properties.py.j2Property-based tests
go-chi-postgres/
manifest.json
...
typescript-express-prisma/
manifest.json
...

manifest.json drives the generation process:

{
  "target": "python-fastapi-postgres",
  "files": [
    { "template": "pyproject.toml.j2", "output": "pyproject.toml", "per": "service" },
    { "template": "dockerfile.j2", "output": "Dockerfile", "per": "service" },
    { "template": "main.py.j2", "output": "app/main.py", "per": "service" },
    { "template": "config.py.j2", "output": "app/config.py", "per": "service" },
    { "template": "database.py.j2", "output": "app/database.py", "per": "service" },
    { "template": "model.py.j2", "output": "app/models/{{ svc }}.py", "per": "service" },
    { "template": "schema.py.j2", "output": "app/schemas/{{ svc }}.py", "per": "service" },
    { "template": "router.py.j2", "output": "app/routers/{{ svc }}.py", "per": "service" },
    { "template": "service.py.j2", "output": "app/services/{{ svc }}.py", "per": "service" },
    { "template": "validator.py.j2", "output": "app/validators/{{ svc }}.py", "per": "service" },
    {
      "template": "migration.py.j2",
      "output": "alembic/versions/001_initial_schema.py",
      "per": "service"
    },
    { "template": "test_api.py.j2", "output": "tests/test_api.py", "per": "service" },
    { "template": "test_properties.py.j2", "output": "tests/test_properties.py", "per": "service" }
  ]
}

Preventing template rot

  1. Template tests: Each template has a golden-file test. The test renders the template with the URL shortener IR and compares the output to the checked-in golden file. Any change to a template that alters output triggers a diff review.

  2. Compilation gate: After rendering, the generated project is compiled/type-checked as a CI step. For Python: ruff check + mypy. For Go: go build + go vet. For TypeScript: tsc --noEmit. Templates that produce code with lint errors fail CI.

  3. Integration gate: The generated project is started in Docker, and the generated tests are run against it. Templates that produce code with test failures fail CI.


3. Database migration generation

3.1 From spec state declarations to DDL

The spec's state block is the single source of truth for the database schema. The convention engine translates each state relation into DDL.

Mapping rules

Spec ConstructDDL Output
store: ShortCode -> lone LongURLTable with FK or inline columns. lone = nullable FK / nullable column. Here, since LongURL is a value entity, it becomes an inline column.
ShortCode (left side of ->)Primary key column
LongURL (right side with lone)NOT NULL column (lone means at most one, and since this is the value of the mapping, presence is guaranteed)
created_at: ShortCode -> DateTimeAdditional column on the same table (same key domain)
invariant: len(value) >= 6CHECK (length(code) >= 6)
invariant: value matches /^[a-zA-Z0-9]+$/CHECK (code ~ '^[a-zA-Z0-9]+$')
invariant: isValidURI(value)CHECK (url ~ '^https?://')
all c in store | c in created_atNOT NULL on created_at + default

Column type mapping

Spec TypePostgreSQLSQLite (test)MySQL
String (unbounded)TEXTTEXTTEXT
String (with len constraint)VARCHAR(max_len)TEXTVARCHAR(max_len)
IntINTEGERINTEGERINT
DateTimeTIMESTAMPTZTEXTDATETIME
BoolBOOLEANINTEGERTINYINT(1)
DecimalNUMERIC(p,s)REALDECIMAL(p,s)
Entity referenceFK column (_id INTEGER REFERENCES ...)SameSame

Implementation note (#58). The Python target emits SQLAlchemy generic types, not raw DDL, so the concrete column type is the dialect's interpretation of the generic type. Two deliberate deviations from the conceptual table above:

  • Unbounded String on MySQL maps to VARCHAR(255), not TEXT. MySQL cannot index/PRIMARY KEY/UNIQUE/foreign-key a TEXT column without a prefix length (error 1170), and the schema indexes FK and identifier columns. VARCHAR(255) keeps every generated column indexable.
  • DateTime uses sa.DateTime() (no timezone) on SQLite and MySQL, and JSONB collapses to the generic sa.JSON() (Postgres keeps postgresql.JSONB()). SQLite enforces foreign keys via a PRAGMA foreign_keys=ON connection listener (runtime engine + Alembic env).

Partial indexes use postgresql_where (Postgres) / sqlite_where (SQLite); MySQL has no equivalent, so the filter is dropped, a plain index is emitted, and the CLI prints a per-dialect downgrade warning.

3.2 Index generation

Primary keys come from the left side of state relations (the key domain). Additional indexes are inferred from:

  1. Query patterns in operations: If Resolve looks up by code, and code is already the PK, no additional index is needed. If a list operation filters by created_at, a btree index is added.

  2. Foreign key columns: Always indexed (PostgreSQL does not auto-index FK targets).

  3. Unique constraints from invariants: invariant: unique(email) produces a unique index.

3.3 Migration versioning strategy

We use sequential numbering (001, 002, 003...) rather than timestamps because:

  • The compiler controls all migrations; there is no risk of developer-caused ordering conflicts.
  • Sequential numbers are easier to read and reason about.
  • The format matches Alembic (Python), golang-migrate (Go), and Prisma (TS) defaults.

3.4 Diff migrations for spec changes

When the spec changes, the compiler compares the old IR and new IR:

old_ir.state vs new_ir.state -> diff

Diff categories and their DDL

ChangeDDL
New state relationCREATE TABLE
Removed state relationDROP TABLE (with confirmation prompt)
New field on existing relationALTER TABLE ADD COLUMN
Removed fieldALTER TABLE DROP COLUMN (with confirmation prompt)
Changed typeALTER TABLE ALTER COLUMN TYPE
New invariant (constraint)ALTER TABLE ADD CONSTRAINT
Removed invariantALTER TABLE DROP CONSTRAINT
New entity relationCREATE TABLE (junction table if many-to-many)

3.5 Complete generated SQL: URL shortener (simple)

-- Migration 001: Initial schema for UrlShortener
-- Generated by spec-to-rest compiler

BEGIN;

CREATE TABLE store (
    code       VARCHAR(10)   NOT NULL,
    url        TEXT          NOT NULL,
    created_at TIMESTAMPTZ   NOT NULL DEFAULT now(),

    -- Primary key from state relation: store: ShortCode -> lone LongURL
    CONSTRAINT pk_store PRIMARY KEY (code),

    -- From entity ShortCode invariants
    CONSTRAINT ck_code_length  CHECK (length(code) >= 6 AND length(code) <= 10),
    CONSTRAINT ck_code_pattern CHECK (code ~ '^[a-zA-Z0-9]+$'),

    -- From entity LongURL invariants
    CONSTRAINT ck_url_valid_uri CHECK (url ~ '^https?://'),

    -- From global invariant: all c in store | c in created_at
    -- (enforced by NOT NULL + DEFAULT on created_at column constraint)
);

-- Index for temporal queries (list by creation time)
CREATE INDEX ix_store_created_at ON store (created_at);

COMMIT;

3.6 Complete generated SQL: E-commerce order service (complex)

For a spec with entities Customer, Product, Order, OrderItem, Address, and state relations modeling a full order lifecycle:

-- Migration 001: Initial schema for OrderService
-- Generated by spec-to-rest compiler

BEGIN;

-- ============================================================
-- Table: customers
-- From state: customers: CustomerId -> lone CustomerProfile
-- ============================================================
CREATE TABLE customers (
    id          UUID         NOT NULL DEFAULT gen_random_uuid(),
    email       VARCHAR(255) NOT NULL,
    name        VARCHAR(200) NOT NULL,
    created_at  TIMESTAMPTZ  NOT NULL DEFAULT now(),
    updated_at  TIMESTAMPTZ  NOT NULL DEFAULT now(),

    CONSTRAINT pk_customers PRIMARY KEY (id),
    CONSTRAINT uq_customers_email UNIQUE (email),
    CONSTRAINT ck_email_format CHECK (email ~ '^[^@]+@[^@]+\.[^@]+$'),
    CONSTRAINT ck_name_nonempty CHECK (length(trim(name)) > 0)
);

CREATE INDEX ix_customers_email ON customers (email);

-- ============================================================
-- Table: products
-- From state: catalog: ProductId -> lone ProductInfo
-- ============================================================
CREATE TABLE products (
    id          UUID          NOT NULL DEFAULT gen_random_uuid(),
    sku         VARCHAR(50)   NOT NULL,
    name        VARCHAR(300)  NOT NULL,
    description TEXT,
    price_cents INTEGER       NOT NULL,
    stock       INTEGER       NOT NULL DEFAULT 0,
    active      BOOLEAN       NOT NULL DEFAULT true,
    created_at  TIMESTAMPTZ   NOT NULL DEFAULT now(),

    CONSTRAINT pk_products PRIMARY KEY (id),
    CONSTRAINT uq_products_sku UNIQUE (sku),
    CONSTRAINT ck_price_positive CHECK (price_cents > 0),
    CONSTRAINT ck_stock_nonneg CHECK (stock >= 0),
    CONSTRAINT ck_sku_nonempty CHECK (length(trim(sku)) > 0)
);

CREATE INDEX ix_products_sku ON products (sku);
CREATE INDEX ix_products_active ON products (active) WHERE active = true;

-- ============================================================
-- Table: addresses
-- From state: addresses: AddressId -> lone AddressInfo
-- ============================================================
CREATE TABLE addresses (
    id          UUID          NOT NULL DEFAULT gen_random_uuid(),
    customer_id UUID          NOT NULL,
    line1       VARCHAR(300)  NOT NULL,
    line2       VARCHAR(300),
    city        VARCHAR(100)  NOT NULL,
    state       VARCHAR(100),
    postal_code VARCHAR(20)   NOT NULL,
    country     VARCHAR(2)    NOT NULL,
    created_at  TIMESTAMPTZ   NOT NULL DEFAULT now(),

    CONSTRAINT pk_addresses PRIMARY KEY (id),
    CONSTRAINT fk_addresses_customer FOREIGN KEY (customer_id)
        REFERENCES customers (id) ON DELETE CASCADE,
    CONSTRAINT ck_country_iso CHECK (length(country) = 2)
);

CREATE INDEX ix_addresses_customer ON addresses (customer_id);

-- ============================================================
-- Table: orders
-- From state: orders: OrderId -> lone OrderInfo
-- State machine column from operation lifecycle:
--   PlaceOrder -> FulfillOrder -> ShipOrder -> DeliverOrder
--   CancelOrder can fire from pending/confirmed/shipped
-- ============================================================
CREATE TABLE orders (
    id              UUID          NOT NULL DEFAULT gen_random_uuid(),
    customer_id     UUID          NOT NULL,
    shipping_addr_id UUID         NOT NULL,
    status          VARCHAR(20)   NOT NULL DEFAULT 'pending',
    total_cents     INTEGER       NOT NULL,
    placed_at       TIMESTAMPTZ   NOT NULL DEFAULT now(),
    confirmed_at    TIMESTAMPTZ,
    shipped_at      TIMESTAMPTZ,
    delivered_at    TIMESTAMPTZ,
    cancelled_at    TIMESTAMPTZ,

    CONSTRAINT pk_orders PRIMARY KEY (id),
    CONSTRAINT fk_orders_customer FOREIGN KEY (customer_id)
        REFERENCES customers (id) ON DELETE RESTRICT,
    CONSTRAINT fk_orders_address FOREIGN KEY (shipping_addr_id)
        REFERENCES addresses (id) ON DELETE RESTRICT,
    CONSTRAINT ck_order_status CHECK (
        status IN ('pending', 'confirmed', 'shipped', 'delivered', 'cancelled')
    ),
    CONSTRAINT ck_total_nonneg CHECK (total_cents >= 0)
);

CREATE INDEX ix_orders_customer ON orders (customer_id);
CREATE INDEX ix_orders_status ON orders (status);
CREATE INDEX ix_orders_placed_at ON orders (placed_at);

-- ============================================================
-- Table: order_items (junction table)
-- From state: order_items: OrderId -> set OrderItemInfo
-- (one-to-many: each order has multiple items)
-- ============================================================
CREATE TABLE order_items (
    id          UUID          NOT NULL DEFAULT gen_random_uuid(),
    order_id    UUID          NOT NULL,
    product_id  UUID          NOT NULL,
    quantity    INTEGER       NOT NULL,
    unit_price_cents INTEGER  NOT NULL,

    CONSTRAINT pk_order_items PRIMARY KEY (id),
    CONSTRAINT fk_items_order FOREIGN KEY (order_id)
        REFERENCES orders (id) ON DELETE CASCADE,
    CONSTRAINT fk_items_product FOREIGN KEY (product_id)
        REFERENCES products (id) ON DELETE RESTRICT,
    CONSTRAINT ck_quantity_positive CHECK (quantity > 0),
    CONSTRAINT ck_unit_price_positive CHECK (unit_price_cents > 0)
);

CREATE INDEX ix_order_items_order ON order_items (order_id);
CREATE INDEX ix_order_items_product ON order_items (product_id);

-- ============================================================
-- Trigger: update orders.updated_at equivalent (total recalc)
-- From invariant: order.total = sum(item.quantity * item.unit_price)
-- ============================================================
CREATE OR REPLACE FUNCTION recalc_order_total()
RETURNS TRIGGER AS $$
BEGIN
    UPDATE orders
    SET total_cents = (
        SELECT COALESCE(SUM(quantity * unit_price_cents), 0)
        FROM order_items
        WHERE order_id = COALESCE(NEW.order_id, OLD.order_id)
    )
    WHERE id = COALESCE(NEW.order_id, OLD.order_id);
    RETURN NULL;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_recalc_order_total
    AFTER INSERT OR UPDATE OR DELETE ON order_items
    FOR EACH ROW EXECUTE FUNCTION recalc_order_total();

COMMIT;

4. OpenAPI spec generation

The compiler generates a complete OpenAPI 3.1 specification from the IR. This serves dual purposes: documentation and conformance testing (Schemathesis).

4.1 Mapping rules

IR ElementOpenAPI Element
Service nameinfo.title
Operation with http_method + http_pathpaths.{path}.{method}
Operation input fieldsrequestBody schema (POST/PUT/PATCH) or parameters (GET/DELETE)
Operation output fieldsresponses.{status}.content.schema
Entity invariantsSchema pattern, minLength, maxLength, minimum, maximum
requires clause violationsError response entries (404, 422)
Convention overridesHeader definitions, redirect responses

4.2 Complete generated OpenAPI for URL shortener

openapi: "3.1.0"
info:
  title: UrlShortener
  version: 0.1.0
  description: >
    URL Shortener service. Generated from formal specification by the spec-to-rest compiler. All
    endpoints enforce the behavioral contracts defined in the specification.
  contact:
    name: Generated Service
  license:
    name: MIT

servers:
  - url: http://localhost:8000
    description: Local development

paths:
  /shorten:
    post:
      operationId: shorten
      summary: Shorten a URL
      description: >
        Creates a new short code mapping to the provided long URL.

        Spec contract:
          requires: isValidURI(url.value)
          ensures: code not in pre(store), store'[code] = url,
                   short_url = base_url + "/" + code.value, #store' = #store + 1
      tags:
        - url_shortener
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ShortenRequest"
            examples:
              basic:
                summary: Shorten a URL
                value:
                  url: "https://example.com/very/long/path?query=1&param=2"
              https:
                summary: HTTPS URL
                value:
                  url: "https://docs.example.org/api/v2/reference"
      responses:
        "201":
          description: Short URL created successfully
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ShortenResponse"
              examples:
                created:
                  summary: Newly created short URL
                  value:
                    code: "aBcDeFgH"
                    short_url: "http://localhost:8000/aBcDeFgH"
                    created_at: "2026-04-05T12:00:00Z"
        "422":
          description: >
            Validation error. The requires clause `isValidURI(url.value)` was violated.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                invalid_url:
                  summary: Invalid URL format
                  value:
                    detail: "Must be a valid HTTP or HTTPS URL"

  /{code}:
    get:
      operationId: resolve
      summary: Resolve a short code
      description: >
        Looks up the short code and redirects (302) to the original URL.

        Spec contract:
          requires: code in store
          ensures: url = store[code], store' = store

        Convention override: http_status_success = 302, Location header set.
      tags:
        - url_shortener
      parameters:
        - name: code
          in: path
          required: true
          description: The short code to resolve
          schema:
            type: string
            minLength: 6
            maxLength: 10
            pattern: "^[a-zA-Z0-9]+$"
          examples:
            valid_code:
              summary: A valid short code
              value: "aBcDeFgH"
      responses:
        "302":
          description: Redirect to the original long URL
          headers:
            Location:
              description: The original long URL
              schema:
                type: string
                format: uri
              example: "https://example.com/very/long/path?query=1"
        "404":
          description: >
            Short code not found. The requires clause `code in store` was violated.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                not_found:
                  summary: Code does not exist
                  value:
                    detail: "Short code 'xYzAbCdE' not found"
        "422":
          description: Short code format invalid
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                bad_format:
                  summary: Code too short
                  value:
                    detail: "Short code must be 6-10 characters, got 3"

    delete:
      operationId: delete
      summary: Delete a short code
      description: >
        Removes the short code mapping from the store.

        Spec contract:
          requires: code in store
          ensures: code not in store', #store' = #store - 1
      tags:
        - url_shortener
      parameters:
        - name: code
          in: path
          required: true
          description: The short code to delete
          schema:
            type: string
            minLength: 6
            maxLength: 10
            pattern: "^[a-zA-Z0-9]+$"
      responses:
        "204":
          description: Short code deleted successfully (no body)
        "404":
          description: >
            Short code not found. The requires clause `code in store` was violated.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                not_found:
                  value:
                    detail: "Short code 'xYzAbCdE' not found"

  /health:
    get:
      operationId: health
      summary: Health check
      description: Infrastructure endpoint. Returns 200 if the service is running.
      tags:
        - infrastructure
      responses:
        "200":
          description: Service is healthy
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: ["ok"]
                required:
                  - status

components:
  schemas:
    ShortenRequest:
      type: object
      description: >
        Input for the Shorten operation. Derived from: operation Shorten { input: url: LongURL }
      required:
        - url
      properties:
        url:
          type: string
          format: uri
          description: The long URL to shorten
          pattern: "^https?://\\S+$"
          example: "https://example.com/very/long/path"

    ShortenResponse:
      type: object
      description: >
        Output for the Shorten operation. Derived from: operation Shorten { output: code: ShortCode,
        short_url: String }
      required:
        - code
        - short_url
        - created_at
      properties:
        code:
          type: string
          minLength: 6
          maxLength: 10
          pattern: "^[a-zA-Z0-9]+$"
          description: The generated short code
        short_url:
          type: string
          format: uri
          description: The full short URL
        created_at:
          type: string
          format: date-time
          description: When the short URL was created

    ErrorResponse:
      type: object
      description: Standard error response body
      required:
        - detail
      properties:
        detail:
          type: string
          description: Human-readable error description

tags:
  - name: url_shortener
    description: URL shortening operations
  - name: infrastructure
    description: Health and metrics endpoints

5. Dafny output integration

5.1 The compilation pipeline

Dafny can compile to Python, Go, JavaScript, Java, and C#. The pipeline:

Spec operation            Dafny source           Compiled output
(pre/postconditions)  ->  (verified function)  ->  (target-language module)
                               |                        |
                          Dafny verifier            Adapter layer
                          confirms correctness      wraps types

Dafny source for generate_short_code

module ShortCodeGen {
  // The alphabet for short codes
  const ALPHABET: string := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"

  // Predicate: a string contains only alphanumeric characters
  predicate IsAlphanumeric(s: string)
  {
    forall i :: 0 <= i < |s| ==> s[i] in ALPHABET
  }

  // Generate a short code that is not in the existing set
  method GenerateShortCode(existingCodes: set<string>, length: nat)
    returns (code: string)
    requires 6 <= length <= 10
    ensures 6 <= |code| <= 10
    ensures IsAlphanumeric(code)
    ensures code !in existingCodes
  {
    var candidate := "";
    var found := false;
    while !found
      decreases *  // non-terminating: depends on randomness
    {
      candidate := RandomAlphanumeric(length);
      if candidate !in existingCodes {
        found := true;
      }
    }
    code := candidate;
  }

  // External method: filled in by the target runtime
  method {:extern "ShortCodeRuntime", "RandomAlphanumeric"} RandomAlphanumeric(length: nat)
    returns (s: string)
    requires 6 <= length <= 10
    ensures |s| == length
    ensures IsAlphanumeric(s)
}

5.2 Dafny-compiled output characteristics

When Dafny compiles to Python, the output has specific traits:

  1. Uses Dafny runtime library: import _dafny provides Seq, Map, Set, etc.
  2. Non-idiomatic types: Dafny strings become _dafny.Seq of characters, rather than native str.
  3. Class-based structure: Each module becomes a class with static methods.
  4. No async support: Dafny output is synchronous; async wrapping is needed for FastAPI.

Example Dafny-compiled Python (simplified)

import _dafny

class ShortCodeGen:
    @staticmethod
    def GenerateShortCode(existingCodes, length):
        # Dafny-generated loop
        candidate = _dafny.Seq([])
        found = False
        while not found:
            candidate = ShortCodeRuntime.RandomAlphanumeric(length)
            if candidate not in existingCodes:
                found = True
        return candidate

5.3 The adapter layer

The adapter converts between Dafny types and native types:

# adapters/short_code_gen.py — generated adapter

import _dafny
from dafny_output.ShortCodeGen import ShortCodeGen as _DafnyShortCodeGen


def generate_short_code(existing_codes: set[str], length: int = 8) -> str:
    """Adapter wrapping Dafny-verified GenerateShortCode.

    Converts:
        set[str]      -> _dafny.Set of _dafny.Seq
        int           -> int (compatible)
        _dafny.Seq    -> str (return value)
    """
    dafny_existing = _dafny.Set({_dafny.Seq(code) for code in existing_codes})
    dafny_result = _DafnyShortCodeGen.GenerateShortCode(dafny_existing, length)
    return "".join(dafny_result.Elements)

5.4 When to use Dafny vs convention-generated code

The decision happens at the operation level:

Operation TypeDafny?Rationale
Simple CRUD (create, read, update, delete)NoConvention engine emits directly; no non-trivial logic
Lookup by keyNoSELECT ... WHERE key = $1 needs no verification
List with filteringNoStandard query patterns
Computation with postconditionsYesE.g., generate a unique code satisfying constraints
State machine transitionsYesComplex guard conditions need verification
Business rules with invariantsYesE.g., "order total = sum of item prices"

The "seam" is in the service layer. Each service method either calls the convention-generated database query directly or delegates to the Dafny adapter for the computed portion:

async def shorten(self, request: ShortenRequest) -> ShortenResponse:
    # Convention-generated: validate input
    validate_long_url(request.url)

    # Convention-generated: gather existing state for Dafny precondition
    existing_codes = await self._get_existing_codes()

    # DAFNY SEAM: verified code generation
    code = generate_short_code(existing_codes)

    # Convention-generated: persist to database
    entry = StoreEntry(code=code, url=request.url)
    self._session.add(entry)
    await self._session.flush()

    # Convention-generated: construct response
    return ShortenResponse(code=code, short_url=f"{settings.base_url}/{code}")

5.5 Dafny runtime dependency

Each target requires the Dafny runtime library:

TargetDafny RuntimeSizeInstall
PythonDafnyRuntimePython~50 KBpip install dafnyruntimepython
Godafny/dafnyRuntime.go~30 KBVendored in internal/dafny/
JavaScriptDafnyRuntimeJs~80 KBnpm install @aspect/dafny-runtime

The runtime is included automatically when any operation uses Dafny output. When no operations require Dafny (pure CRUD services), the runtime dependency is omitted entirely.


6. Infrastructure code generation

6.1 Dockerfile generation

All targets use multi-stage builds:

Python pattern

builder stage  -> install dependencies from pyproject.toml
runtime stage  -> copy installed packages + app code, run as non-root

Go pattern

builder stage  -> go build -o /app/server ./cmd/server
runtime stage  -> FROM scratch or distroless, copy binary only

TypeScript pattern

builder stage  -> npm ci && npm run build
runtime stage  -> copy dist/ + node_modules (production only)

Health checks are always included. The health endpoint is generated as a built-in route (/health) and the Dockerfile HEALTHCHECK instruction points to it.

6.2 Docker-compose.yml generation

The compose file is generated based on the state declarations:

State TypeCompose Service
Any persistent statePostgreSQL container
Cache-like state (TTL annotations)Redis container (optional)
Queue/event stateRabbitMQ container (optional)

The compose file always includes:

  • The application container
  • The database container with health check
  • A migration runner (runs once, then exits)
  • A volume for database persistence

6.3 CI/CD configuration

A GitHub Actions workflow is generated:

# .github/workflows/ci.yml — Generated by spec-to-rest compiler

name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest

    services:
      postgres:
        image: postgres:17-alpine
        env:
          POSTGRES_USER: test
          POSTGRES_PASSWORD: test
          POSTGRES_DB: test
        ports:
          - 5432:5432
        options: >-
          --health-cmd pg_isready --health-interval 5s --health-timeout 3s --health-retries 10

    env:
      DATABASE_URL: postgresql+psycopg://test:test@localhost:5432/test
      BASE_URL: http://localhost:8000

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: "3.11"

      - name: Install dependencies
        run: pip install -e ".[dev]"

      - name: Lint
        run: ruff check app/ tests/

      - name: Type check
        run: mypy app/

      - name: Run migrations
        run: alembic upgrade head

      - name: Run tests
        run: pytest tests/ -v --tb=short

      - name: Conformance tests
        run: |
          uvicorn app.main:app --host 0.0.0.0 --port 8000 &
          sleep 3
          schemathesis run openapi.yaml --base-url http://localhost:8000 --stateful=links

  docker:
    runs-on: ubuntu-latest
    needs: test
    steps:
      - uses: actions/checkout@v4
      - name: Build Docker image
        run: docker build -t url-shortener .
      - name: Smoke test
        run: |
          docker compose up -d
          sleep 5
          curl -f http://localhost:8000/health
          docker compose down -v

6.4 Environment variable templates

The .env.example file lists every environment variable the service uses, with safe default values and comments explaining each:

# Database connection string
DATABASE_URL=postgresql+psycopg://shortener:shortener@localhost:5432/shortener

# Base URL for constructing short URLs (no trailing slash)
BASE_URL=http://localhost:8000

# Logging level (debug, info, warning, error)
LOG_LEVEL=info

6.5 Makefile generation

The Makefile provides a uniform interface across all targets:

# Common targets (every generated project has these)
.PHONY: help run test lint build docker-up docker-down migrate

help:       ## Show available commands
run:        ## Run the development server
test:       ## Run all tests
lint:       ## Run linter and formatter check
build:      ## Build the production artifact
docker-up:  ## Start all services with docker-compose
docker-down: ## Stop all services
migrate:    ## Run database migrations

The implementation varies by target but the interface is identical, so users can switch between Python, Go, and TypeScript projects and use the same commands.


7. Quality assurance of generated code

7.1 Static analysis pipeline

Every generated project passes through a static analysis pipeline before being delivered to the user:

TargetLinterFormatterType Checker
Python/FastAPIruff checkruff format --checkmypy --strict
Go/chigolangci-lint rungofmt -l (must be empty)go vet ./...
TypeScript/Expresseslintprettier --checktsc --noEmit

The code generation pipeline runs these checks as a post-generation step. If any check fails, it indicates a template bug, and the pipeline fails with a clear error pointing to the offending template and generated file.

7.2 Test hierarchy

Generated tests form a three-tier pyramid:

Conformance (slow, broad)Schemathesis: schema fuzzing against generated OpenAPI Property-Based (medium, deep)Hypothesis/rapid/fast-check: ensures clauses as properties API Integration (fast, targeted)httpx/supertest: each operation with expected inputs/outputs

Tier 1, API Integration Tests (from spec operations): Each spec operation gets test methods for success cases and error cases. The requires clause violations map to specific error status codes. The ensures clause postconditions map to assertion checks.

Tier 2, Property-Based Tests (from spec ensures clauses): Each ensures clause is translated into a Hypothesis/rapid/fast-check property. The property generator produces arbitrary valid inputs and checks that the postcondition holds.

Tier 3, Conformance Tests (from OpenAPI spec): Schemathesis fuzzes every endpoint with random valid and invalid inputs, checking that responses conform to the OpenAPI schema. This catches edge cases that hand-written tests miss.

7.3 Benchmark against hand-written code

To validate that generated code is production-quality, the pipeline includes a benchmark suite that compares generated code against hand-written equivalents on three axes:

  1. Correctness: Do the generated tests pass? Do hand-written tests (if any) pass?
  2. Performance: Request latency and throughput under load (wrk/hey).
  3. Code quality metrics: Lines of code, cyclomatic complexity, dependency count.

The benchmark is optional and runs in CI when a benchmark label is applied to a PR. It produces a markdown report:

| Metric            | Generated | Hand-Written | Delta  |
|-------------------|-----------|--------------|--------|
| Lines of code     | 487       | 523          | -7%    |
| Cyclomatic compl. | 1.8 avg   | 2.1 avg      | -14%   |
| p50 latency       | 2.3ms     | 2.1ms        | +10%   |
| p99 latency       | 8.7ms     | 7.9ms        | +10%   |
| Test pass rate     | 100%      | 100%         | =      |

The expected outcome is that generated code is slightly more verbose (due to explicit contracts) but equivalent in performance, since the hot path is database I/O regardless.


8. Incremental regeneration

8.1 Diff-based regeneration

When the spec changes, the compiler does not regenerate everything blindly. It computes the diff between the old IR and new IR and regenerates only affected files:

old_ir = load("url_shortener.spec.v1")
new_ir = parse("url_shortener.spec.v2")
diff = compute_ir_diff(old_ir, new_ir)

for change in diff:
    affected_files = dependency_map[change.kind]
    for file in affected_files:
        regenerate(file, new_ir)

Dependency map

Change KindAffected Files
New entitymodels, schemas, validators, migration
Changed entity invariantvalidators, schemas, migration (CHECK constraint), tests
New operationrouters, services, tests, OpenAPI
Changed operation pre/postservices, validators, tests, OpenAPI
New state relationmodels, migration, database config
Changed convention overriderouters (HTTP mapping), OpenAPI
Global invariant changevalidators, migration, tests

8.2 Migration generation for schema changes

When a state relation changes, the compiler generates a new migration file (incrementing the version number) rather than modifying the initial migration:

# Spec change: add 'click_count: ShortCode -> Int' to state
# Generates: alembic/versions/002_add_click_count.py

def upgrade() -> None:
    op.add_column("store", sa.Column("click_count", sa.Integer(), server_default="0",
                                      nullable=False))

def downgrade() -> None:
    op.drop_column("store", "click_count")

8.3 Preserving manual modifications

Users may need to modify generated code. The compiler supports two strategies:

Strategy 1: Protected Regions (default)

Generated files contain marked regions:

# === GENERATED CODE START — do not edit below this line ===
class StoreEntry(Base):
    __tablename__ = "store"
    code: Mapped[str] = mapped_column(String(10), primary_key=True)
    url: Mapped[str] = mapped_column(Text, nullable=False)
    created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True))
# === GENERATED CODE END ===

# === USER CODE START — your modifications here ===
# Add custom methods, properties, or class attributes below.
# This region is preserved during regeneration.

# === USER CODE END ===

During regeneration, the compiler replaces the content between GENERATED CODE START and GENERATED CODE END while preserving everything between USER CODE START and USER CODE END.

Strategy 2: Extension Files (recommended)

Rather than modifying generated files, users create extension files:

# app/extensions/url_shortener.py — never overwritten by the compiler

from app.services.url_shortener import UrlShortenerService

# Add custom methods to the service
async def get_top_urls(self, limit: int = 10):
    """Custom method not in the spec."""
    ...

# Monkey-patch or use dependency injection to add the method
UrlShortenerService.get_top_urls = get_top_urls

Extension files live in app/extensions/ (or internal/extensions/, src/extensions/) and are never touched by the compiler. The generated main.py auto-discovers and loads extensions at startup.

8.4 Git integration

The compiler can optionally commit generated changes:

spec-to-rest generate --commit url_shortener.spec

This runs:

  1. Generate all files
  2. git add only generated files (tracked via .spec-to-rest-manifest.json)
  3. git commit -m "regen: update from spec change (added click_count state)"
  4. Show the diff summary

The manifest file tracks which files were generated, so the compiler knows what to git add and what to leave alone.


9. Extensibility

9.1 Hook points

Generated services expose hook points at standard locations:

HookWhen It FiresUse Case
on_startupApplication bootInitialize external connections, warm caches
on_shutdownApplication shutdownClose connections, flush buffers
before_requestBefore each request handlerLogging, auth, rate limiting
after_requestAfter each request handlerResponse logging, metrics
before_{operation}Before the service methodCustom validation, audit logging
after_{operation}After the service methodNotifications, cache invalidation
on_errorWhen an exception occursCustom error responses, alerting

Hooks are registered via a hooks file:

# app/hooks.py — user-defined, never overwritten

from app.hooks_registry import on_startup, before_request, after_shorten

@on_startup
async def init_redis():
    """Connect to Redis cache on startup."""
    ...

@before_request
async def check_api_key(request):
    """Validate API key header."""
    ...

@after_shorten
async def notify_analytics(result):
    """Send analytics event after URL shortening."""
    ...

9.2 Plugin system for custom code generators

Users can register custom code generator plugins. The shipped plugin shape is the DeploymentProfile registry in modules/profile/src/main/scala/specrest/profile/Registry.scala; new targets like Go/chi (#33) and TS/Express (#35) plug in as additional registry entries plus a Handlebars template tree:

// modules/profile/src/main/scala/specrest/profile/KotlinSpring.scala (sketch)
package specrest.profile

object KotlinSpring:
  val profile: DeploymentProfile = DeploymentProfile(
    name = "kotlin-spring-postgres",
    displayName = "Kotlin + Spring Boot + PostgreSQL",
    language = "kotlin",
    framework = "spring",
    // ... typeMap, dependencies, etc.
  )

// Then register in Registry.scala:
object Registry:
  private val Profiles: Map[String, DeploymentProfile] = Map(
    "python-fastapi-postgres" -> PythonFastapi.profile,
    "kotlin-spring-postgres"  -> KotlinSpring.profile
  )

Templates live under modules/codegen/src/main/resources/templates/<target-name>/ and codegen/Emit.scala dispatches on the profile's name field. There is no separate plugin-discovery mechanism today, adding a target is a registry entry plus a template tree.

9.3 Custom endpoints

Users can add endpoints not in the spec using the extensions mechanism:

# app/extensions/analytics.py

from fastapi import APIRouter

analytics_router = APIRouter(prefix="/analytics", tags=["analytics"])

@analytics_router.get("/top")
async def get_top_urls():
    """Custom endpoint: not in the spec, not overwritten by the compiler."""
    ...

The generated main.py auto-discovers routers in app/extensions/:

# In generated main.py
import importlib
import pkgutil

for _, module_name, _ in pkgutil.iter_modules(["app/extensions"]):
    module = importlib.import_module(f"app.extensions.{module_name}")
    for attr_name in dir(module):
        attr = getattr(module, attr_name)
        if isinstance(attr, APIRouter):
            app.include_router(attr)

9.4 Overriding generated implementations

For any operation, users can replace the generated implementation:

# app/overrides/url_shortener.py — user-provided, never overwritten

async def shorten_override(session, request):
    """Replace the generated Shorten implementation.

    The compiler-generated service will call this instead of its own logic
    when this override exists.
    """
    # Custom implementation using a counter-based scheme instead of random codes
    ...

The service layer checks for overrides at startup:

class UrlShortenerService:
    def __init__(self, session):
        self._session = session
        # Check for user override
        try:
            from app.overrides.url_shortener import shorten_override
            self._shorten_impl = shorten_override
        except ImportError:
            self._shorten_impl = self._default_shorten

    async def shorten(self, request):
        return await self._shorten_impl(self._session, request)

10. Comparison with existing generators

10.1 OpenAPI generator

What it is. Generates client SDKs and server stubs from OpenAPI specs. Supports 40+ targets.

What it does well

  • Breadth: 40+ language targets means wide coverage.
  • Community: large contributor base, many edge cases handled.
  • Schema-to-model generation is solid for simple types.

What it does poorly

  • Generated code often does not compile for less-popular targets. The Java and TypeScript targets are well-maintained; Go and Rust targets frequently have issues. A 2023 study found that 30% of generated Go clients had compilation errors.
  • Server stubs are truly stubs, empty method bodies with TODO comments. No business logic, no database integration, no tests.
  • No behavioral specification support. The input is purely structural (OpenAPI), so there is no way to express "this operation requires X and ensures Y."
  • Template quality varies wildly between targets because different maintainers own different targets with no cross-target quality standard.
  • Generated code style is often non-idiomatic. Go code uses Java-style getters/setters. Python code does not follow PEP 8 conventions.

What we should copy

  • The template-per-target architecture. Each target has its own template set.
  • The mustache template system is simple and well-understood.
  • The --additional-properties flag for customization per target.

What we should avoid

  • Trying to support 40+ targets. We target 3 (Python, Go, TypeScript) and make them excellent.
  • Generating stubs. Our output must be complete and runnable.
  • Relying on community maintainers for individual targets with no integration testing.

10.2 JHipster

What it is. Full-stack application generator. Produces Spring Boot + Angular/React/Vue from a domain model (JDL).

What it does well

  • Generates COMPLETE, RUNNABLE applications. docker-compose up works out of the box. This is the bar we must meet.
  • JDL (JHipster Domain Language) is simple and well-documented.
  • Produces production-quality code: Liquibase migrations, Spring Security config, CI/CD pipelines, Docker files, Kubernetes manifests.
  • Extensive entity relationship support (one-to-many, many-to-many, enums, pagination).
  • Built-in support for microservice architectures (gateway, service discovery, config server).

What it does poorly

  • CRUD-only. JDL defines entities and relationships, rather than behavior. There is no way to say "this operation requires X and ensures Y."
  • Java/Spring only on the backend (with experimental .NET support). No Python, Go, or TypeScript backend.
  • Opinionated to the point of rigidity. If you want something JHipster does not support (e.g., a custom auth scheme), you fight the generator.
  • Overwhelming output: a simple 2-entity JHipster project generates 200+ files, most of which are boilerplate the developer never reads.
  • No formal verification of any kind.

What we should copy

  • The completeness standard: generated projects include migrations, Docker files, CI config, tests, and documentation. Everything works.
  • The JDL syntax is a good model for how a simple DSL should feel.
  • The entity relationship mapping (including junction tables for many-to-many).

What we should avoid

  • Generating 200+ files for a simple service. Our URL shortener should produce ~25 files.
  • Being opinionated about frontend framework. We generate backends only.
  • Locking into one backend ecosystem (Spring).

10.3 Smithy code generators

What it is. AWS's API design language. Smithy IDL defines the model; code generators produce clients and servers.

What it does well

  • Production-proven: every AWS SDK is generated from Smithy models.
  • The trait system is elegant: @readonly, @paginated, @httpQuery are annotations that modify code generation behavior without changing the model.
  • smithy-dafny exists: Smithy models can be compiled to Dafny for formal verification, then to target languages. This is the closest prior art to our Dafny integration pipeline.
  • Clean separation between model (Smithy IDL), transform (code generators), and output (target code).

What it does poorly

  • AWS-centric. Smithy's type system and traits are designed for AWS services. Concepts like "eventually consistent reads" and "pagination tokens" are baked in as first-class constructs.
  • Steep learning curve. The Smithy model requires understanding resource lifecycle, service closures, and trait application order.
  • No behavioral specification. Smithy is purely structural. You can say "this operation takes X and returns Y" but not "this operation requires that X is in the database and ensures that Y is added."
  • Server code generation is still limited (mainly Java/Kotlin via smithy4s).

What we should copy

  • The trait system for extensible metadata.
  • The smithy-dafny pipeline architecture for verified code generation.
  • The clean model-transform-output separation.

What we should avoid

  • AWS-specific abstractions. Our DSL is domain-agnostic.
  • The complexity of the Smithy gradle build system.

10.4 Grpc-gateway

What it is. Generates a REST reverse proxy in Go from protobuf service definitions with HTTP annotations.

What it does well

  • Battle-tested at massive scale (millions of requests per day at many companies).
  • Clean mapping from RPC to REST via annotations.
  • Generates OpenAPI from the same protobuf source.
  • The generated code is idiomatic Go.

What it does poorly

  • Go-only server generation.
  • Protobuf is verbose for simple REST APIs.
  • No database integration. The generated code is a proxy; you still write the backend.
  • No behavioral contracts beyond input/output types.

What we should copy

  • The annotation-based convention override system (option (google.api.http) = {...}).
  • The OpenAPI co-generation approach: produce the API spec alongside the server code.

What we should avoid

  • Requiring protobuf as input. Our DSL should be simpler than protobuf.
  • Being proxy-only. We generate the complete service, rather than a proxy.

10.5 Prisma

What it is. ORM and schema management tool for TypeScript/JavaScript (with growing Rust and Python support). Schema-first: define models in schema.prisma, generate type-safe client code.

What it does well

  • The schema.prisma DSL is one of the best-designed data modeling languages. Simple, readable, powerful.
  • Migration generation is excellent: prisma migrate dev detects schema changes and generates SQL migrations automatically.
  • Type-safe client: queries are statically typed, preventing runtime errors.
  • Introspection: can reverse-engineer an existing database into a schema.

What it does poorly

  • Data modeling only. No HTTP routes, no business logic, no tests.
  • TypeScript/JavaScript only (Rust client is new, Python is community-maintained).
  • No behavioral contracts. You cannot express preconditions or postconditions.
  • The query engine is a Rust binary, adding deployment complexity.

What we should copy

  • The migration generation approach: diff the old and new schemas, generate SQL.
  • The schema.prisma DSL design: simple, declarative, readable.
  • The introspection capability (future feature: import existing DB into spec).

What we should avoid

  • Shipping a separate query engine binary. Our generated code uses the target's native ORM/database driver.

10.6 Sqlalchemy alembic

What it is. Database migration tool for Python. Auto-generates migrations by comparing SQLAlchemy model metadata to the current database state.

What it does well

  • Auto-detection of schema changes: add a column to a model, alembic revision --autogenerate produces the migration.
  • Supports complex migrations: data migrations, multi-step changes, custom SQL.
  • The migration chain (revision graph) handles branching and merging.
  • Mature and battle-tested (used by most Python ORMs in production).

What it does poorly

  • Python-only.
  • Auto-generation misses some changes (renamed columns appear as drop+add).
  • No support for behavioral constraints (CHECK constraints must be added manually).
  • No cross-language migration generation.

What we should copy

  • The auto-generation approach: compare model state to database state, generate diff.
  • The revision chain for migration ordering.
  • The upgrade()/downgrade() pattern.

What we should avoid

  • Depending on a running database for migration generation. Our compiler generates migrations from the spec diff alone, without needing a database connection.

10.7 Summary comparison matrix

CapabilityOpenAPI GenJHipsterSmithygRPC-GWPrismaAlembicOurs
Behavioral spec inputNoNoNoNoNoNoYes
Complete runnable outputNo (stubs)YesPartialNo (proxy)No (ORM)No (migrations)Yes
Multi-language targets40+ (poor quality)1 (Java)7+ (clients)1 (Go)1 (TS)1 (Python)3 (high quality)
Database migrationsNoYesNoNoYesYesYes
Generated testsNoPartialNoNoNoNoYes (3 tiers)
Formal verificationNoNoPartial (smithy-dafny)NoNoNoYes (Dafny)
OpenAPI co-generationInput, rather than outputYesYesYesNoNoYes
Docker/infra generationNoYesNoNoNoNoYes
Incremental regenerationNoPartialNoNoYesYesYes

The key differentiator is that no existing tool combines behavioral specification with complete code generation. JHipster comes closest on the "complete output" axis; Smithy-Dafny comes closest on the "verified output" axis. Our compiler merges both.


Appendix A: Code generation pipeline implementation sketch

The orchestrator that drives the entire pipeline:

class CodeGenerationPipeline:
    """Top-level orchestrator for multi-target code generation."""

    def __init__(self, target: str, template_dir: Path):
        self.target = target
        self.env = jinja2.Environment(
            loader=jinja2.FileSystemLoader(template_dir / target),
            trim_blocks=True,
            lstrip_blocks=True,
            keep_trailing_newline=True,
        )
        self.env.filters["snake_case"] = to_snake_case
        self.env.filters["camel_case"] = to_camel_case
        self.env.filters["python_type"] = to_python_type
        self.env.filters["go_type"] = to_go_type
        self.env.filters["ts_type"] = to_typescript_type

    def generate(self, ir: ServiceIR, output_dir: Path) -> list[GeneratedFile]:
        """Generate all files for the target from the IR."""
        manifest = self._load_manifest()
        generated = []

        for entry in manifest["files"]:
            template = self.env.get_template(entry["template"])
            output_path = output_dir / self._resolve_path(entry["output"], ir)

            content = template.render(
                service=ir,
                entities=ir.entities,
                state=ir.state,
                operations=ir.operations,
                invariants=ir.invariants,
                conventions=ir.conventions,
                target=self.target,
            )

            output_path.parent.mkdir(parents=True, exist_ok=True)
            output_path.write_text(content)
            generated.append(GeneratedFile(path=output_path, template=entry["template"]))

        # Post-generation quality checks
        self._lint(output_dir)
        self._typecheck(output_dir)

        return generated

    def _load_manifest(self) -> dict:
        manifest_path = self.env.loader.searchpath[0] / "manifest.json"
        return json.loads(manifest_path.read_text())

    def _resolve_path(self, path_template: str, ir: ServiceIR) -> str:
        return path_template.replace("{{ svc }}", to_snake_case(ir.name))

    def _lint(self, output_dir: Path) -> None:
        if self.target.startswith("python"):
            subprocess.run(["ruff", "check", str(output_dir)], check=True)
            subprocess.run(["ruff", "format", "--check", str(output_dir)], check=True)
        elif self.target.startswith("go"):
            subprocess.run(["golangci-lint", "run", str(output_dir / "...")], check=True)
        elif self.target.startswith("typescript"):
            subprocess.run(["npx", "eslint", str(output_dir / "src")], check=True)

    def _typecheck(self, output_dir: Path) -> None:
        if self.target.startswith("python"):
            subprocess.run(["mypy", str(output_dir / "app")], check=True)
        elif self.target.startswith("go"):
            subprocess.run(["go", "vet", "./..."], cwd=output_dir, check=True)
        elif self.target.startswith("typescript"):
            subprocess.run(["npx", "tsc", "--noEmit"], cwd=output_dir, check=True)

Appendix B: Template example, router generation (Jinja2)

A concrete example of a Jinja2 template for FastAPI route generation:

{#- templates/python-fastapi/router.py.j2 -#}
"""FastAPI route handlers for {{ service.name }}.

Generated from spec operations via the convention engine.
{%- for op in operations %}
    {{ op.name }}  ({{ "mutates" if op.mutates_state else "reads" }} state)  -> {{ op.http_method }} {{ op.http_path }}
{%- endfor %}
"""

from fastapi import APIRouter, Depends, HTTPException, Response
from sqlalchemy.ext.asyncio import AsyncSession

from app.database import get_session
from app.schemas.{{ service.name | snake_case }} import (
{%- for op in operations %}
{%- if op.inputs %}
    {{ op.name }}Request,
{%- endif %}
{%- if op.outputs and op.http_success_status != 302 %}
    {{ op.name }}Response,
{%- endif %}
{%- endfor %}
    ErrorResponse,
)
from app.services.{{ service.name | snake_case }} import {{ service.name }}Service

router = APIRouter(tags=["{{ service.name | snake_case }}"])

{% for op in operations %}
@router.{{ op.http_method | lower }}(
    "{{ op.http_path }}",
{%- if op.http_success_status != 302 %}
    response_model={{ op.name }}Response,
{%- endif %}
    status_code={{ op.http_success_status }},
    responses={
{%- for status, desc in op.http_error_responses %}
        {{ status }}: {"model": ErrorResponse, "description": "{{ desc }}"},
{%- endfor %}
    },
    summary="{{ op.name }}",
)
async def {{ op.name | snake_case }}(
{%- if op.inputs %}
    body: {{ op.name }}Request,
{%- endif %}
{%- for param in op.path_params %}
    {{ param.name }}: {{ param | python_type }},
{%- endfor %}
    session: AsyncSession = Depends(get_session),
) -> {% if op.http_success_status == 302 %}Response{% else %}{{ op.name }}Response{% endif %}:
    service = {{ service.name }}Service(session)
    result = await service.{{ op.name | snake_case }}(
{%- if op.inputs %}body{% endif %}
{%- for param in op.path_params %}{% if op.inputs or not loop.first %}, {% endif %}{{ param.name }}{% endfor %}
    )
{%- if op.requires and "in store" in (op.requires | join(" ")) %}
    if result is None:
        raise HTTPException(status_code=404, detail="Resource not found")
{%- endif %}
{%- if op.http_success_status == 302 %}
    return Response(status_code=302, headers={"Location": result.url})
{%- elif op.http_success_status == 204 %}
    return Response(status_code=204)
{%- else %}
    return result
{%- endif %}

{% endfor %}

This template, when rendered with the URL shortener IR, produces the app/routers/url_shortener.py file shown in Section 1.1.


Appendix C: End-to-end generation sequence

The complete sequence from spec input to running service:

1. User writes url_shortener.spec
       |
2. Parser produces ServiceIR
       |
3. Convention engine annotates IR with HTTP/DB/validation mappings
       |
4. Verification engine model-checks the spec (Alloy/Quint)
       |
5. LLM synthesis engine generates Dafny code for non-CRUD operations
       |
6. Dafny verifier confirms correctness
       |
7. Dafny compiler produces target-language modules
       |
8. Code generation pipeline runs:
   a. Load target manifest (e.g., python-fastapi-postgres/manifest.json)
   b. Render each template with IR + convention outputs + Dafny modules
   c. Write generated files to output directory
   d. Run linter + type checker on generated code
   e. Generate .spec-to-rest-manifest.json listing all generated files
       |
9. User runs: docker-compose up
       |
10. Migration container runs alembic upgrade head
       |
11. App container starts FastAPI on port 8000
       |
12. User runs: make test
       |
13. pytest runs test_api.py (integration), test_properties.py (property-based)
       |
14. schemathesis runs test_conformance.py (schema fuzzing)
       |
15. All tests pass -> service is verified end-to-end

Total wall-clock time for steps 2-8 (the compiler): under 30 seconds for a URL-shortener-sized spec. Under 5 minutes for a 20-operation service with Dafny verification in the loop.


11. Observability in generated services

Every generated service includes observability infrastructure out of the box: structured logging, distributed tracing, metrics, and health endpoints. These are not optional add-ons, they are part of the standard file manifest for every target.

11.1 Structured logging

Generated services use structured JSON logging (not plaintext). Every log entry includes:

{
  "timestamp": "2026-04-05T12:34:56.789Z",
  "level": "info",
  "service": "url_shortener",
  "operation": "Shorten",
  "trace_id": "abc123",
  "span_id": "def456",
  "duration_ms": 42,
  "status_code": 201,
  "message": "Operation completed"
}

Implementation per target:

  • Python/FastAPI: structlog with JSON renderer, middleware that injects request context
  • Go/chi: slog with JSON handler, middleware that injects request context
  • TypeScript/Express: pino with JSON transport, middleware that injects request context

Sensitive fields (marked @sensitive in the spec or matching patterns like password, token, secret) are automatically redacted in log output.

11.2 Opentelemetry integration

Generated services export traces and metrics via OpenTelemetry:

url_shortener/
app/
observability.pyOTel setup: tracer provider, meter provider
middleware/
tracing.pyRequest/response span middleware

The generated docker-compose.yml includes an optional Jaeger or OTLP collector container for local development. Each operation is wrapped in a span that records:

  • Operation name (from the spec)
  • Input parameters (non-sensitive)
  • Duration
  • Success/failure status
  • Database query spans (auto-instrumented via ORM)

Convention override:

conventions {
  global.otel_exporter = "otlp"               // "otlp", "jaeger", "none"
  global.otel_endpoint = "http://collector:4317"
}

11.3 Health and readiness endpoints

Every generated service includes two health endpoints (not derived from the spec, these are infrastructure conventions):

EndpointPurposeChecks
GET /healthLiveness probe (K8s livenessProbe)Returns {"status": "ok"} if the process is running
GET /readyReadiness probe (K8s readinessProbe)Checks DB connectivity, checks external service reachability

The readiness endpoint verifies that all dependencies declared via effect blocks (see Appendix F of the spec language design) are reachable. If a dependency is down, the readiness endpoint returns HTTP 503 with details about which dependency failed.

11.4 Metrics

Generated services expose a /metrics endpoint (Prometheus format) or push metrics via OTel, covering:

  • Request count by operation, method, and status code
  • Request duration histogram by operation
  • Active database connections
  • Error rate by operation

On this page

Table of contents1. What gets generated1.1 Python-fastapi-postgresDirectory structure1.2 Go-chi-postgresDirectory structure1.3 Typescript-express-prismaDirectory structure2. Template architecture2.1 Template engine selectionWhy Jinja22.2 IR-to-template variable mapping2.3 Convention engine feed-in2.4 Dafny output splicing2.5 Cross-cutting concerns2.6 Template organization and qualityStructurePreventing template rot3. Database migration generation3.1 From spec state declarations to DDLMapping rulesColumn type mapping3.2 Index generation3.3 Migration versioning strategy3.4 Diff migrations for spec changesDiff categories and their DDL3.5 Complete generated SQL: URL shortener (simple)3.6 Complete generated SQL: E-commerce order service (complex)4. OpenAPI spec generation4.1 Mapping rules4.2 Complete generated OpenAPI for URL shortener5. Dafny output integration5.1 The compilation pipelineDafny source for generate_short_code5.2 Dafny-compiled output characteristicsExample Dafny-compiled Python (simplified)5.3 The adapter layer5.4 When to use Dafny vs convention-generated code5.5 Dafny runtime dependency6. Infrastructure code generation6.1 Dockerfile generationPython patternGo patternTypeScript pattern6.2 Docker-compose.yml generation6.3 CI/CD configuration6.4 Environment variable templates6.5 Makefile generation7. Quality assurance of generated code7.1 Static analysis pipeline7.2 Test hierarchy7.3 Benchmark against hand-written code8. Incremental regeneration8.1 Diff-based regenerationDependency map8.2 Migration generation for schema changes8.3 Preserving manual modifications8.4 Git integration9. Extensibility9.1 Hook points9.2 Plugin system for custom code generators9.3 Custom endpoints9.4 Overriding generated implementations10. Comparison with existing generators10.1 OpenAPI generatorWhat it does wellWhat it does poorlyWhat we should copyWhat we should avoid10.2 JHipsterWhat it does wellWhat it does poorlyWhat we should copyWhat we should avoid10.3 Smithy code generatorsWhat it does wellWhat it does poorlyWhat we should copyWhat we should avoid10.4 Grpc-gatewayWhat it does wellWhat it does poorlyWhat we should copyWhat we should avoid10.5 PrismaWhat it does wellWhat it does poorlyWhat we should copyWhat we should avoid10.6 Sqlalchemy alembicWhat it does wellWhat it does poorlyWhat we should copyWhat we should avoid10.7 Summary comparison matrixAppendix A: Code generation pipeline implementation sketchAppendix B: Template example, router generation (Jinja2)Appendix C: End-to-end generation sequence11. Observability in generated services11.1 Structured logging11.2 Opentelemetry integration11.3 Health and readiness endpoints11.4 Metrics