Imported from anderssv/kotlin-htmx (
AGENTS.md). Install upstream withnpx skills add anderssv/kotlin-htmx. Copyright stays with the author.
Project Guidelines
Application functionality
This is a demo application for showcasing plain HTML, CSS, HTMX and Kotlin.
Development Workflow
IMPORTANT: Always follow the plan in docs/plan.md when working on tasks.
When the user says "go":
- Find the next unmarked test in
docs/plan.md - Implement the test following TDD principles
- Implement only enough code to make that test pass
- Mark the test as completed in
docs/plan.md - Run all tests to ensure nothing is broken
- Format and verify code with
./gradlew ktlintFormat(this both formats and checks in one step) - Update
docs/features.mdwhen completing significant features (features.md is for developers learning techniques)
This ensures systematic, incremental progress through planned features.
TDD Note: Compile errors are NOT a proxy for failing tests. A test must compile and execute, then fail with a meaningful assertion error to be considered a proper "Red" phase in TDD. Add necessary dependencies and imports first to get a proper failing test.
Tech Stack
- Kotlin on the JVM
- Gradle (Kotlin DSL) for build management
- JUnit and AssertJ for testing
- Jackson for JSON handling
Running Tests
# Run all tests
./gradlew test
# Run specific test class
./gradlew test --tests "fully.qualified.TestClassName"
Dependencies
Dependencies are managed in multiple places. When upgrading, check all of them:
Gradle dependencies (build.gradle.kts / gradle.properties)
Use ./gradlew dependencyUpdates to check for new versions of Gradle-managed dependencies.
Non-Gradle dependencies
| Tool / Library | Where | How to check latest |
|---|---|---|
| Gradle wrapper | gradle/wrapper/gradle-wrapper.properties |
./gradlew dependencyUpdates (shown at bottom of report) |
| Rust | .mise.toml |
mise ls-remote rust | tail -5 |
| LightningCSS (cargo) | .mise.toml |
mise ls-remote cargo:lightningcss | tail -5 |
| LightningCSS (npm binary) | Dockerfile, .github/workflows/main.yml |
curl -s https://registry.npmjs.org/lightningcss-cli-linux-x64-gnu | python3 -c "import sys,json; print(json.load(sys.stdin)['dist-tags']['latest'])" |
CDN-loaded frontend libraries
These are referenced as CDN URLs in Kotlin templates and JS source files:
| Library | Pinned version | File |
|---|---|---|
| htmx.org | exact | src/main/kotlin/.../pages/PageTemplates.kt |
| htmx-ext-json-enc | exact | src/main/kotlin/.../pages/PageTemplates.kt |
| htmx-ext-preload | exact | src/main/kotlin/.../pages/PageTemplates.kt |
| htmx-ext-sse | exact | src/main/kotlin/.../pages/PageTemplates.kt |
| idiomorph | exact | src/main/kotlin/.../plugins/LiveReload.kt |
| @lit/task | exact | src/main/resources/script/lit-script.js |
| @picocss/pico | major only | src/main/kotlin/.../pages/PageTemplates.kt |
| Lit | major only | src/main/resources/script/lit-script.js |
| React / ReactDOM | major only | src/main/resources/script/react-script.js |
Check latest versions with: curl -s "https://registry.npmjs.org/<package>/latest" | python3 -c "import sys,json; print(json.load(sys.stdin)['version'])"
Tools
# Print new versions of dependencies
./gradlew dependencyUpdates
# Verify version upgrades by building and running the shadow jar
# Always run this after upgrading dependencies to ensure everything works
./gradlew shadowJar && java -jar build/libs/kotlin-htmx-all.jar
# Run the application. This starts the server and is a blocking operation.
./gradlew run
# Start dev server with auto-reload + LiveReload (recommended)
./start-dev-server.sh # Blocking, random port, opens browser
./start-dev-server.sh stop # Stop running server and watch
./start-dev-server.sh status # Show running server info
PORT=8080 ./start-dev-server.sh # Specific port
NO_BROWSER=1 ./start-dev-server.sh # Don't open browser
BACKGROUND=1 ./start-dev-server.sh # Return immediately
# Or run with auto-reload manually (two terminals):
# Terminal 1: continuous build
./gradlew -t build -x test -i
# Terminal 2: run the application
./gradlew run
# Format and check code with ktlint (recommended - does both formatting and checking)
./gradlew ktlintFormat
# Check code formatting with ktlint only (without auto-formatting)
./gradlew ktlintCheck
Note: Prefer ktlintFormat over ktlintCheck as it both formats the code and verifies it in one step.
Auto-reload: Development mode is enabled via the ktor block in build.gradle.kts. The application module must be a suspend function reference (suspend fun Application.module()) for auto-reload to work with Ktor > 3.2. Lambda initializers and blocking function references are not supported.
Testing After Dependency Updates
When updating dependencies (especially Gradle/Kotlin dependencies), follow this verification process:
- Run all tests:
./gradlew test - Build the shadow jar:
./gradlew shadowJar - Start the application:
java -jar build/libs/kotlin-htmx-all.jar & - Wait for startup (watch for "Responding at http://0.0.0.0:8080")
- Test CSS endpoint (uses LightningCSS processing):
curl -s http://localhost:8080/css/styles.css | head -30 - Kill the test server:
pkill -f "kotlin-htmx-all.jar"
The CSS endpoint test verifies that LightningCSS is processing CSS correctly (nested selectors flattened, custom properties preserved).
Development Setup
-
Install prerequisites:
- Mise version manager
- Java 21 (via ASDF)
- Git
-
Build project:
./gradlew build
Best Practices
-
Testing
-
Write tests for all new features
-
Write tests first (TDD approach)
-
Use fakes instead of mocks when possible
-
Follow Arrange-Act-Assert pattern
-
Use test data builders (usually objects with valid() methods, a variant of the Object Mother pattern)
-
Run all tests after finishing a task
-
Format and verify code with
./gradlew ktlintFormatbefore considering a task complete (this both formats and checks) -
Testing Layers - Understanding the Differences:
- Page/Component Tests (e.g.,
PersonRegistrationPageTest): Test HTML structure and form field generation in isolation. These verify that form inputs have correctnameattributes, proper field structure, and correct HTML element nesting. They catch structural issues without HTTP overhead. - Endpoint/Route Tests (e.g.,
PersonRegistrationRoutesTest): Test HTTP behavior, validation flow, redirects, and repository state changes. They verify HTML content withcontains()checks for specific text, but do NOT verify form field structure ornameattributes. - E2E/Selenium Tests (e.g.,
HtmxCheckboxPageTest): Test full browser behavior, JavaScript execution, SSE connections, and multi-browser synchronization.
- Page/Component Tests (e.g.,
-
Why All Three Layers Matter:
- Page tests catch form binding issues (wrong field names) that endpoint tests miss
- Endpoint tests catch HTTP flow and validation issues that page tests miss
- E2E tests catch browser behavior and JavaScript issues that neither catch
- These are NOT duplicates - they test different concerns at different levels
-
Skip low-value utility tests: Don't write unit tests for simple utility functions (like
partialHtml()orrespondHtmlFragment()) if they are already exercised by integration/endpoint tests. Focus testing effort on complex logic, edge cases, and code that's hard to verify through integration tests alone. Simple wrappers and formatting utilities that are called by tested endpoints don't need dedicated unit tests.
-
-
Code Organization
- Follow domain-driven package structure
- Keep services focused and small
- Use dependency injection for better testability
- Maintain clear separation between domain and infrastructure code
-
Code Principles
- Favour immutability
- Use data classes for simple data structures
- Avoid side effects in functions
- Prefer composition to inheritance
- Use sealed classes for representing state
- Use UUIDs for unique identifiers
- Prefer objects to primitive types
- Re-use test data setup, prefer .valid() test extension methods.
- Use rich domain models, avoid splitting data into multiple tables unless necessary. JSONB column in PostgreSQL is a good option for complex data structures.
- Data Binding: Prefer using Jackson for all serialization/deserialization. When working with formats that Jackson doesn't natively support (like HTML form parameters with dot notation), write utility functions to convert to an intermediate structure (Map, JsonNode) that Jackson can consume. Avoid writing custom deserializers; instead, adapt the input to Jackson's expectations.
-
Naming Conventions
- *Domain.kt for domain models
- *Repository.kt for data access
- *Service.kt for business logic
- *Fake.kt for test doubles
- *Client.kt for clients to other services
-
Documentation
- Check /doc directory for detailed guides
- Keep README.md updated
- Document complex business rules in code
-
Documentation & Task Management
- Always update
docs/plan.mdwhen completing tests (mark with ✅ and status COMPLETED) - Always update
docs/features.mdwhen completing significant featuresfeatures.mdis intended for developers wanting to learn the techniques in this repo- Focus on explaining patterns, approaches, and architectural decisions
- Include code examples and explanations of "how" and "why"
- Update improvement-tasks.md when completing tasks
- Mark completed tasks with ✅ COMPLETED status
- Add status notes explaining what was accomplished
- This provides clear audit trail of work done
- Always update
-
Operations
- When refactoring make sure to delete the old code