kiln

module
v0.1.12 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 9, 2026 License: MIT

README

kiln

Go Version Go Report Card Docs Release

Schema in. API out.

Documentation

Turn your database schema into a complete Go HTTP API - models, validation, handlers, routing, and OpenAPI.

CREATE TABLE users (
  id    UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  email TEXT NOT NULL UNIQUE,
  name  TEXT NOT NULL,
  role  TEXT NOT NULL DEFAULT 'member' CHECK (role IN ('member', 'admin'))
);
kiln generate
GET    /api/v1/users           List (paginated, filterable, sortable)
POST   /api/v1/users           Create (validated)
GET    /api/v1/users/{id}      Get
PATCH  /api/v1/users/{id}      Update (partial)
DELETE /api/v1/users/{id}      Delete
generated/
  models/users.go              Request/response structs with validation
  store/users.go               Type-safe database operations
  handlers/users.go            HTTP handlers
  router.go                    Route registration
docs/openapi.yaml              OpenAPI 3.0 spec
cmd/server/main.go             Runnable server
DB Schema ──► kiln generate ──► Generated Go API ──► Running server

The generated code uses bob (a query builder) for database access. There is no runtime dependency on kiln - you can remove kiln after generation and continue using the code as a normal Go project.

Contents

Quick Start

Install - pick one:

# Homebrew (macOS/Linux)
brew install fisayoafolayan/kiln

# Binary (no Go required) - see all platforms at Releases
curl -sSfL https://github.com/fisayoafolayan/kiln/releases/latest/download/kiln_$(uname -s)_$(uname -m).tar.gz | tar xz
sudo mv kiln /usr/local/bin/

# Or with Go
go install github.com/fisayoafolayan/kiln/cmd/kiln@latest
kiln init        # interactive setup, done in seconds
kiln generate    # generates your full API
go run cmd/server/main.go
curl http://localhost:8080/api/v1/users | jq
[
  { "id": "a1b2c3...", "email": "alice@example.com", "name": "Alice", "role": "admin" }
]

Examples

Example Description
Blog API Full CRUD API with filtering, soft deletes, M2M, enums
Team Task Tracker Bob plugin mode example
Try the example
git clone https://github.com/fisayoafolayan/demo-blog-api.git
cd demo-blog-api
cp .env.example .env
make setup && make run

Why kiln?

Stop writing by hand:

  • CRUD handlers, request/response structs, validation tags
  • Pagination, filtering, sorting boilerplate
  • OpenAPI specs that drift from reality
  • Route wiring for every table and relationship

kiln generates all of it from your schema - and regenerates safely as your schema evolves (checksums protect your edits). Plain Go output, no lock-in.

It does not generate business logic, workflows, or cross-table invariants. Structural API layer only.

Perfect for
  • Internal tools and admin APIs
  • CRUD-heavy SaaS backends
  • B2B APIs where schema drives the domain
  • Rapid prototyping with real database schemas
Not ideal for
  • Heavy domain logic (payments, approval workflows, state machines)
  • Event-driven architectures (Kafka, queues, async pipelines)
  • APIs where endpoints don't map cleanly to tables

Typical Workflow

1. Update your schema (add column, new table, change constraint)
2. Migrate the database
3. Run: kiln generate          ← takes seconds
4. Restart server
# Example: add a column, regenerate
ALTER TABLE users ADD COLUMN avatar_url TEXT;
kiln generate

Some files are generated once and never overwritten (mappers, helpers, main.go) - these are yours to customize freely.

Additive schema changes (new columns, new tables) just work. Destructive changes (renames, type changes) regenerate correctly, but your write-once files may reference old names - update them by hand. kiln doctor helps catch stale generated files.


Requirements

  • Go 1.26 or later
  • Docker (for running a local database during development)
  • A Postgres, MySQL/MariaDB, or SQLite database

No other tools need to be installed manually - kiln sets up everything it needs when you run kiln generate.


Sample Schema

Start with a Postgres schema like this (MySQL and SQLite equivalents work too):

CREATE TABLE users (
  id         UUID        PRIMARY KEY DEFAULT gen_random_uuid(),
  email      TEXT        NOT NULL UNIQUE,
  name       TEXT        NOT NULL,
  bio        TEXT,
  role       TEXT        NOT NULL DEFAULT 'member'
                         CHECK (role IN ('member', 'moderator', 'admin')),
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE posts (
  id           UUID        PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id      UUID        NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  title        TEXT        NOT NULL,
  body         TEXT        NOT NULL,
  status       TEXT        NOT NULL DEFAULT 'draft'
                           CHECK (status IN ('draft', 'published', 'archived')),
  published_at TIMESTAMPTZ,
  created_at   TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at   TIMESTAMPTZ NOT NULL DEFAULT now(),
  deleted_at   TIMESTAMPTZ           -- nullable = soft delete enabled
);

CREATE TABLE tags (
  id   UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name TEXT NOT NULL UNIQUE
);

CREATE TABLE post_tags (
  post_id UUID NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
  tag_id  UUID NOT NULL REFERENCES tags(id)  ON DELETE CASCADE,
  PRIMARY KEY (post_id, tag_id)
);

Run kiln generate and you immediately get:

  • GET /api/v1/users - list users with pagination, filtering & sorting
  • POST /api/v1/users - create a user with validation
  • GET /api/v1/users/{id} - get a user by ID
  • PATCH /api/v1/users/{id} - update a user
  • DELETE /api/v1/users/{id} - delete a user
  • GET /api/v1/posts - list posts (soft-deleted posts excluded automatically)
  • GET /api/v1/users/{id}/posts - list posts by user ← generated from the FK
  • POST /api/v1/posts/{id}/tags - link a tag to a post ← generated from junction table
  • DELETE /api/v1/posts/{id}/tags/{tagId} - unlink a tag from a post
  • GET /api/v1/posts/{id}/tags - list tags linked to a post

Relationships in your database automatically become API routes. Many-to-many relationships via junction tables generate link/unlink endpoints on both sides.

All with an OpenAPI spec, type-safe store, and a running server. No boilerplate written by hand.


What Gets Generated

File Contents
generated/models/users.go Request/response structs with validation tags
generated/store/users.go Type-safe Store implementation
generated/store/mappers/users.go Type mapper - yours to customise (write-once)
generated/handlers/users.go Full CRUD HTTP handlers
generated/handlers/helpers.go Error helpers, pagination, validator (write-once)
generated/auth/middleware.go Auth middleware - JWT or API key (write-once)
generated/router.go Route registration, including FK-derived nested routes
docs/openapi.yaml OpenAPI 3.0 spec, always in sync
cmd/server/main.go Wired-up server, ready to run (write-once)
Custom Logic

Need business logic beyond CRUD? kiln stays out of your way:

  • Transform data in mappers (store/mappers/) - add computed fields, hide sensitive data
  • Custom error handling in helpers (handlers/helpers.go) - change formats, add logging
  • Wrap or replace handlers - the store uses interfaces, mock or swap anything
  • Disable generated endpoints per table and write your own
  • Delete and replace anything - it's your code, not a framework
// Disable the generated "update" for posts, add your own with business logic:
func (h *PostHandler) Publish(w http.ResponseWriter, r *http.Request) {
    // custom validation, state transitions, notifications - your code
}

You're never locked in. The generated code is just Go.


Generated Code Example

Here's what kiln generates for the users table. The generated code is intentionally boring - no abstractions, no magic. Just idiomatic Go you'd write by hand:

// generated/handlers/users.go - Code generated by kiln. DO NOT EDIT.
// Re-generated on each run. Customise helpers.go or disable operations in kiln.yaml.
package handlers

func (h *UserHandler) Create(w http.ResponseWriter, r *http.Request) {
    var req models.CreateUserRequest
    if !decodeJSON(w, r, &req) {
        return
    }
    if !validateRequest(w, req) {
        return
    }
    row, err := h.store.Create(r.Context(), req)
    if err != nil {
        handleStoreError(w, err, "users", "create")
        return
    }
    writeJSON(w, http.StatusCreated, row)
}
// generated/models/users.go - Code generated by kiln. DO NOT EDIT.
// Re-generated on each run. Use hidden_fields/readonly_fields in kiln.yaml to customise.
package models

type User struct {
    ID        uuid.UUID  `json:"id"`
    Email     string     `json:"email"`
    Name      string     `json:"name"`
    Bio       *string    `json:"bio,omitempty"`
    Role      string     `json:"role"`
    CreatedAt time.Time  `json:"created_at"`
    UpdatedAt time.Time  `json:"updated_at"`
}

type CreateUserRequest struct {
    Email string  `json:"email" validate:"required,email"`
    Name  string  `json:"name"  validate:"required,min=1,max=255"`
    Role  string  `json:"role"  validate:"omitempty,oneof=member admin"`
}

Validation

kiln generates request structs with validate tags and wires up go-playground/validator automatically. Invalid requests return structured error responses:

curl -X POST http://localhost:8080/api/v1/users \
  -H "Content-Type: application/json" \
  -d '{"name": ""}'
{
  "error": "validation failed",
  "fields": {
    "email": "is required",
    "name": "must be at least 1 characters"
  }
}
Error Responses

All generated handlers use consistent error shapes. The error logic lives in helpers.go (write-once) so you can customize formats, add logging, or integrate with your observability stack.

Scenario Status Response
Validation failure 400 {"error": "validation failed", "fields": {"name": "is required"}}
Invalid filter/sort 400 {"error": "invalid value for created_at: expected RFC3339 format"}
Malformed JSON 400 {"error": "invalid request body"}
Body too large 413 {"error": "request body too large"}
Not found 404 {"error": "user not found"}
Unique violation 409 {"error": "user already exists"}
FK violation 422 {"error": "referenced user does not exist"}
Server error 500 {"error": "internal server error"}

Error detection uses database error codes (not string matching) so it works correctly across Postgres, MySQL, and SQLite regardless of locale.


Database Support

kiln supports Postgres, MySQL/MariaDB, and SQLite out of the box.

Postgres

database:
  driver: postgres
  dsn: "postgres://user:pass@localhost:5432/mydb?sslmode=disable"

MySQL / MariaDB

database:
  driver: mysql
  dsn: "user:pass@tcp(localhost:3306)/mydb?parseTime=true"

SQLite

database:
  driver: sqlite
  dsn: "./mydb.db"

Use an environment variable instead of hardcoding the DSN:

database:
  driver: postgres
  dsn_env: DATABASE_URL

Adopt What You Need

Every layer is optional. Most teams generate models, store, and OpenAPI - handlers are optional. Handlers are the layer closest to business logic, and in practice most non-trivial tables outgrow generated handlers quickly as they pick up conditional writes, cross-table invariants, or side effects. Generate handlers for the genuinely simple CRUD tables, write your own for everything else:

generate:
  models: true     # request/response structs
  store: true      # type-safe DB operations
  handlers: false  # write your own handlers
  router: false    # wire routes yourself
  openapi: true    # free OpenAPI spec, always in sync

This also works per-table - generate handlers for most tables, disable specific operations on the ones that need custom logic:

overrides:
  payments:
    disable: [create, update, delete]  # kiln generates list/get, you handle mutations

Your custom handlers call the same generated store:

// your code - sits alongside generated handlers
func (h *PaymentHandler) Charge(w http.ResponseWriter, r *http.Request) {
    var req models.CreatePaymentRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        http.Error(w, "invalid request", http.StatusBadRequest)
        return
    }

    // your business logic
    if err := h.billing.Authorize(r.Context(), req.Amount); err != nil {
        http.Error(w, "payment declined", http.StatusUnprocessableEntity)
        return
    }

    // then use kiln's generated store
    payment, err := h.store.Create(r.Context(), req)
    if err != nil {
        http.Error(w, "internal error", http.StatusInternalServerError)
        return
    }
    json.NewEncoder(w).Encode(payment)
}

This is what a mature kiln project looks like: generated store and models everywhere, generated handlers for simple CRUD tables, custom handlers for anything with business logic.

Write-Once Files

Mapper files (store/mappers/) are generated once then handed to you. Customize them freely - kiln will never overwrite them.

// store/mappers/users.go - THIS FILE IS YOURS
package mappers

func UserToType(m *dbmodels.User) *models.User {
    return &models.User{
        ID:       m.ID,
        Email:    m.Email,
        Name:     m.Name,
        // Add computed fields, hide sensitive data, transform values:
        FullName: m.FirstName + " " + m.LastName,
        IsAdmin:  m.Role == "admin",
    }
}

Change your schema, regenerate - your mapper is untouched.

For replacing store implementations, bypassing generated queries, or mixing manual and generated routes, see Escape Hatches.


Schema Evolution

This is where kiln differs from one-time scaffolding tools. Your schema changes over time - kiln keeps your API in sync.

The workflow
1. Change your schema (add column, rename field, add table)
2. Migrate the database (goose, atlas, golang-migrate, raw SQL)
3. Run: kiln generate
4. Done. API matches the new schema.
What happens on regenerate

Auto-generated files are updated to match the new schema. Each file has an embedded SHA-256 checksum in a comment on line 2. On regeneration, kiln:

  1. Reads the existing file's checksum
  2. Recomputes the hash of the file's current contents
  3. If they differ (you edited the file), skips it with a warning
  4. If they match (untouched), overwrites with the new version
  ⚠ SKIPPED  generated/store/users.go (user-modified; use --force to overwrite)
  ✓ generated/models/users.go
  ✓ generated/handlers/users.go
  ✓ generated/router.go
  ✓ docs/openapi.yaml

Any content change counts as a modification - even a comment or whitespace. --force overrides the check and regenerates everything. There is no undo, so use kiln diff first to review what would change.

Write-once files (mappers, helpers, main.go) are never touched. You update those manually when needed.

Preview before regenerating

Not sure what will change? Preview first:

kiln diff
  + generated/models/users.go
  + generated/models/posts.go
  + generated/store/users.go
  + generated/store/posts.go
  + generated/handlers/users.go
  + generated/handlers/posts.go
  + generated/router.go
  + docs/openapi.yaml

No files written. No risk. See exactly what kiln would generate, then decide.

Tools for safe evolution
Command Use when
kiln diff Preview what would be generated (no files written)
kiln generate Regenerate, skip edited files
kiln generate --force Overwrite everything, including edits
kiln generate --table users Regenerate only one table
Example: adding a column end-to-end
# 1. Add the column to your database
psql "$DATABASE_URL" -c "ALTER TABLE users ADD COLUMN avatar_url TEXT;"

# 2. Regenerate
kiln generate

# 3. Commit
git add -A && git commit -m "add avatar_url to users"

kiln updates the response struct, create/update requests, store queries, handler filters, and OpenAPI spec. One migration, one command, everything in sync.

Testing generated code

kiln doesn't generate tests - testing strategies are too project-specific. But the generated code is designed for testability:

Handlers depend on a store interface, not a concrete type. Mock the interface:

type mockUserStore struct{}

func (m *mockUserStore) Get(ctx context.Context, id uuid.UUID) (*models.User, error) {
    return &models.User{Name: "Alice"}, nil
}

Store methods accept bob.Executor, which both bob.DB and bob.Tx satisfy. This means you can pass a transaction for test isolation or multi-step operations:

db := bob.NewDB(testDB) // bob.DB satisfies bob.Executor
store := store.NewUserStore(db)
user, err := store.Get(ctx, id)

// Or wrap in a transaction:
tx, _ := testDB.BeginTx(ctx, nil)
txStore := store.NewUserStore(bob.NewTx(tx))
// both calls in one transaction

How It Works

Your Database Schema
      │
      ├──► bob reads schema ──► ./models/              (query builders - internal)
      │
      └──► kiln generates ──► ./generated/
                                  models/              (request/response structs)
                                  store/               (DB operations)
                                  store/mappers/       (type mappers - yours to edit)
                                  handlers/            (HTTP handlers)
                                  router.go            (route registration)
                               ./docs/openapi.yaml    (API spec)
                               ./cmd/server/main.go   (runnable server - yours to edit)

kiln uses bob (v0.42.0+) for schema introspection and as the query builder in generated store code. Bob is a runtime dependency of the generated code - your go.mod will include it. Bob runs in-process during generation - no external binary to install, no bobgen.yaml to manage. Just kiln generate.

What bob handles: schema introspection, column type resolution, foreign key detection, and Go model generation (./models/). If you hit a type that kiln doesn't recognise or a column that behaves unexpectedly, bob's generated dbinfo/ files are the place to look - they contain the raw schema metadata kiln reads from.

Foreign key resolution: kiln reads bob's generated relationship structs to accurately resolve FK relationships - even when column names don't match table names (e.g. author_id → users). This is how nested routes like GET /users/{id}/posts are discovered automatically.

Primary key handling:

  • Single-column PKs generate standard /{id} routes
  • Composite PKs generate multi-param routes (e.g. /tenant-users/{tenantId}/{userId})
  • Junction tables (composite PK + exactly 2 FKs) generate M2M link/unlink endpoints instead of CRUD

Known limitations:

  • Postgres array types (text[]) require bob v0.42.0+ for correct Go type mapping
  • Custom Postgres types (ltree, ranges) are mapped to string

Advanced: bob plugin mode. For teams already using bob's code generation pipeline, kiln is also available as a bob plugin. See Bob Plugin Mode below.


Bob Plugin Mode

By default, kiln generate handles everything - most projects should use that. For teams already using bob's code generation pipeline, kiln is also available as a bob plugin. One command generates bob models and kiln's API layer together.

Install
go get github.com/fisayoafolayan/kiln@latest
Create a custom gen/main.go

Instead of running bobgen-psql directly, create a small entrypoint that loads kiln as a plugin:

package main

import (
    "context"
    "log"
    "os"
    "os/signal"

    "github.com/stephenafamo/bob/gen"
    "github.com/stephenafamo/bob/gen/bobgen-psql/driver"
    "github.com/stephenafamo/bob/gen/plugins"
    kilnplugin "github.com/fisayoafolayan/kiln/plugin"
)

func main() {
    ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
    defer cancel()

    driverConfig := driver.Config{DSN: os.Getenv("DATABASE_URL")}
    config := gen.Config{}

    bobPlugins := plugins.Setup[any, any, driver.IndexExtra](
        plugins.Config{}, gen.PSQLTemplates,
    )

    kiln := kilnplugin.New[any, any, driver.IndexExtra](kilnplugin.Options{
        ConfigPath: "kiln.yaml",
    })

    state := &gen.State[any]{Config: config}
    allPlugins := append(bobPlugins, kiln)

    if err := gen.Run(ctx, state, driver.New(driverConfig), allPlugins...); err != nil {
        log.Fatal(err)
    }
}
export DATABASE_URL="postgres://..."
go run gen/main.go

kiln implements bob's DBInfoPlugin interface - bob calls it after schema introspection, and kiln generates the same output as kiln generate.

For the full guide including MySQL/SQLite setup and plugin options, see the Bob Plugin Mode documentation. For a complete working example, see the Team Task Tracker demo.


Why Not sqlc?

sqlc kiln
Input Hand-written SQL queries Database schema
Output Go types + query functions Types + store + handlers + router + OpenAPI
You write SQL + handlers + router kiln.yaml
Runtime dependency None bob (query builder)

Use sqlc when you need fine-grained control over every query. Use kiln when you want a working API from your schema with minimal code.


Configuration

kiln.yaml reference:

version: 1

database:
  driver: "postgres"            # postgres | mysql | sqlite
  dsn: "postgres://..."         # direct DSN
  # dsn_env: DATABASE_URL       # or use an env var (takes precedence over dsn)

output:
  dir: "./generated"            # where generated code goes
  package: generated            # Go package name for generated code

api:
  base_path: "/api/v1"          # prefix for all routes
  framework: stdlib             # stdlib | chi

auth:
  strategy: none                # none | jwt | api_key
  header: Authorization         # header to read (X-API-Key for api_key strategy)

bob:
  enabled: true                 # false = skip DB introspection, use existing models
  models_dir: "./models"        # where bob writes its query builder models

generate:                       # toggle individual layers for brownfield adoption
  models: true
  store: true
  handlers: true
  router: true
  openapi: true

openapi:
  enabled: true
  output: "./docs/openapi.yaml"
  title: "My API"
  version: "1.0.0"
  description: ""               # optional

tables:
  include: []                   # if set, ONLY generate these tables
  exclude:                      # skip these tables (mutually exclusive with include)
    - schema_migrations

overrides:
  users:
    endpoint: members           # override URL path: /api/v1/members instead of /api/v1/users
    hidden_fields:              # excluded from all response types
      - password_hash
    readonly_fields:            # excluded from Create/Update request types
      - created_at
      - updated_at
    disable:                    # disable specific operations: create|update|delete|list|get|link|unlink
      - delete
    filterable_fields:          # allowlist for query filters; empty = all columns
      - email
      - role
      - created_at
    sortable_fields:            # allowlist for sorting; empty = all columns
      - created_at
      - name
    enums:                      # allowed values for string columns
      role: [member, moderator, admin]
    # disable_filters: true     # opt-out of filtering entirely
    # disable_sorting: true     # opt-out of sorting entirely
Filtering & Sorting

List endpoints support filtering via query parameters and sorting via the sort parameter:

# Filter by exact match
GET /api/v1/users?role=admin

# Filter with operators
GET /api/v1/users?created_at[gte]=2024-01-01T00:00:00Z&created_at[lt]=2025-01-01T00:00:00Z

# Sort ascending
GET /api/v1/users?sort=created_at

# Sort descending (prefix with -)
GET /api/v1/users?sort=-created_at

# Combine filters, sorting, and pagination
GET /api/v1/users?role=admin&sort=-created_at&page=2&page_size=10

Supported operators: eq (default), neq, gt, gte, lt, lte. Range operators (gt/gte/lt/lte) are available for numeric and timestamp columns.

By default, all non-hidden columns are filterable and sortable. For production APIs, you should lock this down. Unrestricted filtering can expose internal fields and hit unindexed columns. Use filterable_fields / sortable_fields in overrides to explicitly control which columns are exposed:

overrides:
  users:
    filterable_fields: [email, role, created_at]   # only these columns
    sortable_fields: [created_at, name]
    # disable_filters: true                        # or opt out entirely
Enum Validation

If your schema uses CHECK constraints, kiln auto-detects allowed values and generates validation:

CREATE TABLE posts (
  status TEXT NOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'published', 'archived'))
);

This generates validate:"oneof=draft published archived" automatically - no config needed.

For columns without CHECK constraints, specify allowed values in kiln.yaml:

overrides:
  posts:
    enums:
      status: [draft, published, archived]

Invalid values return a structured error:

{
  "error": "validation failed",
  "fields": {
    "status": "must be one of: draft published archived"
  }
}

Config values always take precedence over auto-detected constraints.

Soft Deletes

If a table has a nullable deleted_at timestamp column, kiln automatically generates soft delete behavior:

CREATE TABLE posts (
  id         UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  title      TEXT NOT NULL,
  deleted_at TIMESTAMPTZ  -- nullable = soft delete enabled
);

No config needed. Kiln detects deleted_at and:

  • DELETE sets deleted_at = now() instead of removing the row
  • GET/LIST adds WHERE deleted_at IS NULL to exclude soft-deleted records
  • UPDATE prevents modifying soft-deleted records
  • Response types exclude the deleted_at field (it's internal)
Authentication

Set strategy: api_key or strategy: jwt in kiln.yaml. kiln generates a write-once middleware file at generated/auth/middleware.go with a skeleton you fill in:

// generated/auth/middleware.go - THIS FILE IS YOURS
func Middleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        key := r.Header.Get("X-API-Key")
        if key == "" {
            writeError(w, http.StatusUnauthorized, "missing api key")
            return
        }
        // TODO: validate the key against your database or config
        next.ServeHTTP(w, r)
    })
}

kiln deliberately does not generate token validation, secret management, or JWT parsing. These are application-specific. The middleware gives you the hook point - you add your logic. This is consistent with kiln's philosophy: generate the structure, you own the decisions.


CLI

kiln init                  Create kiln.yaml interactively
kiln generate              Generate your API (runs schema introspection first)
kiln generate --table X    Regenerate only one table (useful for large schemas)
kiln generate --no-bob     Skip schema reading, use existing models
kiln generate --force      Overwrite files even if they have been manually edited
kiln diff                  Preview what would be generated (no files written)
kiln doctor                Check project health: config, schema, generated files
kiln introspect            Print the parsed schema (text format)
kiln introspect --format json   Print as JSON (for scripting)
kiln version               Print kiln version

All commands accept --config path/to/kiln.yaml (default: kiln.yaml).

Running kiln generate without a kiln.yaml file will fail with a clear error pointing you to kiln init.


Philosophy

  • Schema is the source of truth for structure. Your database already describes your data shape, constraints, and relationships. Kiln derives everything structural from it - types, validation, routes, OpenAPI. Behavior, workflows, and permissions are yours.
  • Correctness over speed. Generation is fast, but the real value is that your API never drifts from your schema. Change the schema, regenerate, done.
  • You own the output. No runtime dependency on kiln. The generated code depends on bob for query building - a standard Go library, not a framework. Fork and forget.
  • Escape hatches everywhere. Write-once files are yours forever. Checksums protect your edits. Nothing is locked down.
  • Idiomatic Go. Output looks like code a senior Go dev wrote by hand.
  • Adopt what you need. Every layer is optional. Most teams use models + store + OpenAPI. Handlers are there when they fit, easy to skip when they don't.
What kiln is not
  • Not an ORM - no runtime query layer
  • Not a framework - no runtime control, no hidden magic
  • Not one-shot scaffolding - regenerate safely as your schema evolves

kiln is a compiler for APIs. Schema in, Go code out. Delete kiln and the code still compiles.


Contributing

git clone https://github.com/fisayoafolayan/kiln
cd kiln

# Run unit tests
make test

# Run end-to-end tests against Postgres (requires Docker)
make e2e/postgres

# Run against all databases
make e2e/all

# See all available commands
make help

The e2e tests spin up Docker containers automatically - no manual database setup required.

When adding a new feature:

  1. Update the relevant generator in internal/generator/:
    • types/ - generates generated/models/ (request/response structs)
    • store/ - generates generated/store/ and generated/store/mappers/
    • handlers/ - generates generated/handlers/ and helpers.go
    • router/ - generates generated/router.go
    • openapi/ - generates docs/openapi.yaml
    • auth/ - generates generated/auth/middleware.go
    • mainfile/ - generates cmd/server/main.go
  2. Add unit tests in the generator's package or internal/generator/generator_test.go
  3. Add parser tests in internal/parser/bob/bob_test.go if changing type resolution
  4. Run make e2e/postgres to verify end to end
  5. Open a PR with a clear description of what changed and why

Roadmap

Shipped:

  • Postgres, MySQL/MariaDB, SQLite support
  • Full CRUD handler generation
  • OpenAPI 3.0 spec generation
  • FK-derived nested routes (from foreign keys)
  • Brownfield layer adoption (toggle individual layers)
  • go-playground/validator integration
  • Enum validation (auto-detected from CHECK constraints + config)
  • Soft deletes (auto-detected from deleted_at column)
  • MaxLength extraction from varchar(N)
  • Chi and stdlib router support
  • Filtering, sorting & pagination
  • Authentication middleware (JWT and API key)
  • Checksum-based regeneration safety
  • Locale-independent database error classification
  • Bob plugin support (use kiln as a bob plugin or standalone CLI)
  • In-process schema reading (no external bob binary needed)
  • Many-to-many link/unlink endpoints (via junction tables)
  • Filter and sort validation (400 on invalid values)
  • kiln doctor diagnostic command
  • Store accepts bob.Executor (enables transactions without generated code changes)

Next:

  • Cursor-based pagination
  • Relationship loading (?include=author)

Later:

  • Gin framework support
  • Transaction support in store
  • Batch create/update/delete endpoints
  • Test scaffolding (generated test helpers and fixtures)
  • Multi-tenancy / row-level scoping (tenant_id auto-injection)

Inspired By

  • bob - schema-driven query building for Go
  • sqlboiler - the codegen philosophy

License

MIT - see LICENSE

Directories

Path Synopsis
cmd
kiln command
internal
bobadapter
Package bobadapter converts bob's drivers.DBInfo into kiln's ir.Schema.
Package bobadapter converts bob's drivers.DBInfo into kiln's ir.Schema.
cmd
Package cmd defines the kiln CLI commands.
Package cmd defines the kiln CLI commands.
config
Package config loads and validates kiln.yaml.
Package config loads and validates kiln.yaml.
generator
Package generator orchestrates all code generators.
Package generator orchestrates all code generators.
generator/auth
Package auth generates authentication middleware.
Package auth generates authentication middleware.
generator/genopt
Package genopt defines the Options type shared across all generators.
Package genopt defines the Options type shared across all generators.
generator/handlers
Package handlers generates HTTP handlers for each table.
Package handlers generates HTTP handlers for each table.
generator/mainfile
Package mainfile generates a write-once main.go that wires up the generated stores, handlers, and router into a runnable HTTP server.
Package mainfile generates a write-once main.go that wires up the generated stores, handlers, and router into a runnable HTTP server.
generator/openapi
Package openapi generates an OpenAPI 3.0 spec from the schema IR.
Package openapi generates an OpenAPI 3.0 spec from the schema IR.
generator/router
Package router generates the route registration file.
Package router generates the route registration file.
generator/store
Package store generates the bob-powered Store implementation for each table.
Package store generates the bob-powered Store implementation for each table.
generator/types
Package types generates request/response model structs for each table.
Package types generates request/response model structs for each table.
ir
Package ir defines the Internal Representation of a database schema.
Package ir defines the Internal Representation of a database schema.
parser/bob
Package bob implements the kiln parser by reading bob's generated models directory and converting them into kiln's IR.
Package bob implements the kiln parser by reading bob's generated models directory and converting them into kiln's IR.
Package plugin provides a bob DBInfoPlugin that generates a complete Go REST API using kiln's generators.
Package plugin provides a bob DBInfoPlugin that generates a complete Go REST API using kiln's generators.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL