README
¶
kiln
Schema in. API out.
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
- Examples
- Why kiln?
- Typical Workflow
- Sample Schema
- What Gets Generated
- Generated Code Example
- Validation
- Database Support
- Adopt What You Need
- Schema Evolution
- How It Works
- Bob Plugin Mode
- Configuration
- CLI
- Philosophy
- Contributing
- Roadmap
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 & sortingPOST /api/v1/users- create a user with validationGET /api/v1/users/{id}- get a user by IDPATCH /api/v1/users/{id}- update a userDELETE /api/v1/users/{id}- delete a userGET /api/v1/posts- list posts (soft-deleted posts excluded automatically)GET /api/v1/users/{id}/posts- list posts by user ← generated from the FKPOST /api/v1/posts/{id}/tags- link a tag to a post ← generated from junction tableDELETE /api/v1/posts/{id}/tags/{tagId}- unlink a tag from a postGET /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:
- Reads the existing file's checksum
- Recomputes the hash of the file's current contents
- If they differ (you edited the file), skips it with a warning
- 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 NULLto exclude soft-deleted records - UPDATE prevents modifying soft-deleted records
- Response types exclude the
deleted_atfield (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:
- Update the relevant generator in
internal/generator/:types/- generatesgenerated/models/(request/response structs)store/- generatesgenerated/store/andgenerated/store/mappers/handlers/- generatesgenerated/handlers/andhelpers.gorouter/- generatesgenerated/router.goopenapi/- generatesdocs/openapi.yamlauth/- generatesgenerated/auth/middleware.gomainfile/- generatescmd/server/main.go
- Add unit tests in the generator's package or
internal/generator/generator_test.go - Add parser tests in
internal/parser/bob/bob_test.goif changing type resolution - Run
make e2e/postgresto verify end to end - 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_atcolumn) - 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 doctordiagnostic 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_idauto-injection)
Inspired By
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. |