A runnable Ktor application with one vertical slice: two entities, a repository query, a service, two endpoints, and the tests that cover them. Runs on H2 out of the box; PostgreSQL is a configuration change.
A minimal, runnable Ktor application on Storm: two entities, one repository query, one service, two endpoints and the tests that cover them. It is a starting point, not a demo — there is nothing here to delete before writing your own code.
./gradlew run # http://localhost:8080
./gradlew test
Both commands work on a fresh clone: the application runs on an H2 database in ./data, created by
the Flyway migration on first start, and the tests run on H2 of their own.
src/main/kotlin/com/example/app/
├── Application.kt one install(Storm) — pool, repositories, schema validation, migration
├── model/ City and User, immutable data classes with @PK and @FK
├── repository/ EntityRepository interfaces; Storm implements them
├── service/ UserService, where the transactions are
└── web/ routes and the request and response types
src/main/resources/
├── application.conf port and the storm.datasource section
└── db/migration/ V1 creates the schema, V2 seeds two rows
src/test/kotlin/com/example/app/
├── UserRepositoryTest.kt @StormTest on H2, asserting the SQL through SqlCapture
└── EntitySchemaValidationTest.kt every entity checked against the migration
GET /users?city=Amsterdam reads users and their city in one statement; the metamodel path
User_.city.name navigates the foreign key, so the query never names the join.
POST /users registers a user, creating the city on first use. Both writes run in one
transaction { }, which is coroutine-native: everything inside it, on any dispatcher, runs on the
transaction's connection and commits or rolls back together.
curl 'http://localhost:8080/users?city=Amsterdam'
curl -X POST http://localhost:8080/users -H 'Content-Type: application/json' \
-d '{"email":"grace@example.com","street":"Oudegracht 3","postalCode":"3511 AA","city":"Utrecht"}'
com.example.app package and the rootProject.name in settings.gradle.kts.V1__create_schema.sql with your schema and the entities with your tables. Storm
validates the two against each other at startup, so a mismatch fails the application rather than
the first request that hits it.V2__sample_data.sql.The application runs on H2 so that a fresh clone needs nothing installed. To use PostgreSQL instead,
start it and swap the storm.datasource block in application.conf for the commented one below it:
docker compose up -d
Both dialect modules and both drivers are already on the runtime classpath, so nothing else changes.