Demo showcasing the xemantic-neo4j-kotlin-driver - Kotlin coroutine extensions for Neo4j
AI can build your Neo4j-based API in minutes, and it will be:
- production ready
- highly performant (non-blocking and streaming IO)
- designed for incremental extension
Here are the steps:
- Open this project on GitHub and click
Use this template. - Clone your new GitHub project
- Open terminal in cloned folder and run Claude Code (or any other coding agent)
claude --dangerously-skip-permissions - Specify your requirements in natural language and wait
The --dangerously-skip-permissions flag is optional. It will save you some time of approving all the tests run by Claude Code to verify own work.
You might be asked to review the integration tests which also serve as an executable specification, capable of verifying itself, and catch future regressions, so that AI agent can also do QA of own output.
Soon we will add:
- automatic Open API documentation
- Docker configuration
I hope it will work well for you. Please give us your feedback on
This project is a comprehensive demonstration of building Neo4j-backed REST APIs using the xemantic-neo4j-kotlin-driver library, which provides:
- Fully async/non-blocking operations - Coroutines everywhere, no blocking calls
- Idiomatic Kotlin patterns - Suspend functions instead of
CompletionStagecallbacks - Resource-safe session management -
.use { }blocks prevent connection pool exhaustion - Flow-based result streaming - Memory-efficient processing of large datasets
- Type-safe data access - Kotlin type system + Neo4j value conversion
Here's SequenceApi.kt showing all key features - a complete server with streaming endpoints using coroutines throughout:
fun Application.sequenceApi() {
val neo4j: Neo4jOperations by dependencies
routing {
// Streaming large result sets with Flow
get("/sequences/{count}") {
val count = call.parameters["count"]!!.toInt()
call.respondTextWriter(
contentType = ContentType.Text.Plain
) {
neo4j.flow(
query = $$"UNWIND range(1, $count) AS n RETURN n",
parameters = mapOf("count" to count)
).collect {
val n = it["n"].asInt()
write("$n")
if (n < count) write("\n")
flush()
}
}
}
}
}Key Features Demonstrated:
neo4j.flow(query, parameters)- Returns KotlinFlow<Record>for streaming.collect { }- Process records incrementally without loading all into memoryrespondTextWriter+flush()- Non-blocking HTTP streaming to client- Parameterized queries with
$$string interpolation
Here's a test example from SequenceApiTest.kt showing the test-driven development approach:
@Test
fun `should stream sequence 10 from neo4j`() = testApplication {
// given
sequenceApiApp()
// when
val response = client.get("/sequences/10")
// then
response should {
have(status == HttpStatusCode.OK)
body<String>() sameAs """
1
2
3
4
5
6
7
8
9
10
""".trimIndent()
}
}Testing Features Demonstrated:
testApplication { }- Ktor's test DSL for HTTP endpoint testingsequenceApiApp()- Helper function setting up the full application stack with embedded Neo4jshould { }andhave()- Fluent assertion DSL from xemantic-kotlin-testbody<String>()- Type-safe response body deserializationsameAs/sameAsJson- String/JSON comparison using unified diff format (helps AI agents spot errors in feedback loop)- Given/when/then structure for clarity
The test uses neo4j-harness for an embedded Neo4j instance, enabling fast feedback without external dependencies - perfect for AI-driven development.
This example depends on Ktor and demonstrates how to set up the library as a Ktor module. For comprehensive Ktor documentation, see the official Ktor documentation.
First, build the executable JAR:
./gradlew uberjarThen set the required environment variables and run:
export NEO4J_URI="neo4j://localhost:7687"
export NEO4J_USER="neo4j"
export NEO4J_PASSWORD="your-password"
java -jar build/libs/xemantic-neo4j-demo-0.1.0-SNAPSHOT-uberjar.jarThe server will start on port 8080 (configurable in application.yaml).
Here's how to start a minimal server with the library:
fun main() {
val neo4jUri = System.getenv("NEO4J_URI")
val neo4jUser = System.getenv("NEO4J_USER")
val neo4jPassword = System.getenv("NEO4J_PASSWORD")
val driver = GraphDatabase.driver(
neo4jUri,
AuthTokens.basic(neo4jUser, neo4jPassword)
).apply {
verifyConnectivity()
}
// this should be no greater than driver's max session limit witch is 100 by default
val dispatcher = Dispatchers.IO.limitedParallelism(90)
val neo4jOperations = DispatchedNeo4jOperations(
driver, dispatcher
)
embeddedServer(Netty, port = 8080) {
dependencies.provide<Neo4jOperations> { neo4jOperations }
sequenceApi()
}.start(wait = true)
}Built on the xemantic-neo4j-kotlin-driver, this demo showcases:
- Suspend functions instead of
CompletionStagecallbacks for cleaner async code - Natural integration with Kotlin's coroutine ecosystem
driver.coroutineSession()for creating coroutine-friendly sessions.use { }extension for automatic resource cleanup (prevents connection pool exhaustion)
session.executeRead { tx -> }for read transactionssession.executeWrite { tx -> }for write transactions- Support for transaction configuration (timeouts, metadata, database selection)
result.records()returns KotlinFlow<Record>for efficient streaming- Demonstrated in
/sequences/{count}endpoint with incremental response writing - Memory-efficient processing of large result sets
- Neo4j value conversion using
.asString(),.asInt(), etc. - Kotlinx serialization for REST API models
- Integration with Ktor's content negotiation
# Build the project
./gradlew build
# Run tests (uses embedded Neo4j via neo4j-harness)
./gradlew test
# Create an executable fat JAR
./gradlew uberjarThe demo includes a full CRUD API for managing persons and relationships:
Health Check:
GET /health- Database connectivity check with Neo4j temporal type demonstration- Returns
200 OKwith{"status": "healthy", "timestamp": "..."}when database is accessible - Returns
503 Service Unavailablewith error details if database is unreachable - Demonstrates Neo4j
datetime()function and.asInstant()conversion tokotlin.time.Instant
- Returns
Simple Examples:
GET /sequences/{count}- Stream numbers from Neo4j (demonstrates Flow-based streaming)
CRUD Operations:
POST /people- Create a person nodeGET /people- List all personsGET /people/{id}- Get a specific personPOST /people/{id}/knows/{otherId}- Create a KNOWS relationshipGET /people/{id}/friends- Get friends via graph traversal
This project uses Neo4j Migrations for schema versioning and evolution. Migrations are stored in src/main/resources/neo4j/migrations/ and follow a version-based naming convention.
Migration Naming Convention:
- Format:
V{version}__{description}.cypher - Example:
V001__Create_person_schema.cypher - Versions are zero-padded integers (001, 002, 003, etc.)
- Description uses underscores instead of spaces
Adding a New Migration:
- Create a new migration file in
src/main/resources/neo4j/migrations/ - Follow the naming convention with the next sequential version number
- Write your Cypher DDL statements (constraints, indexes, etc.)
- Add comments explaining the purpose and impact of the migration
Example:
// V003__Add_person_email_constraint.cypher
// Add email uniqueness constraint to support email-based authentication
CREATE CONSTRAINT person_email_unique IF NOT EXISTS
FOR (p:Person) REQUIRE p.email IS UNIQUE;Migration Execution:
Migrations run automatically when the application starts using the Neo4j Migrations library. The migration history is tracked in the Neo4j database itself.
Current Migrations:
V001__Create_person_schema.cypher- Initial Person node with ID constraint, name index, and createdAt temporal index
Best Practices:
- Always use
IF NOT EXISTSclauses for idempotency - Never modify existing migration files (create new ones instead)
- Test migrations with embedded Neo4j in your test suite
- Document the business reason for each schema change
- Keep migrations focused on a single logical change
HealthCheckApi.kt- Health check endpoint with temporal type handlingSequenceApi.kt- Complete server example in one fileNeo4jDependency.kt- Neo4j driver setup using Ktor's DIServer.kt- Full REST API with CRUD operationsPeopleRepository.kt- Repository pattern with coroutine operationsPeopleApiTest.kt- Tests using embedded Neo4j
Beyond demonstrating the library, this project is architected for AI-driven development using Test-Driven Development:
- Test-First Foundation:
xemantic-kotlin-testDSL creates readable specs AI can understand - Embedded Testing:
neo4j-harnessprovides instant feedback without external dependencies - Type Safety: Kotlin + Power Assert plugin = crystal-clear error messages
- Clear Patterns: Consistent architecture (DI, layers, resource management) AI can follow
- Self-Documenting: CLAUDE.md guides AI behavior and coding standards
With tools like Claude Code, you can:
- Describe what you want in natural language
- AI writes tests that capture your requirements
- AI implements functionality to satisfy those tests
- AI iterates autonomously - runs
./gradlew test, interprets failures, refines code - You get working features with test coverage and documentation
You: "I need an endpoint to find mutual friends between two people"
AI: [Writes test case in PeopleApiTest.kt]
[Implements GET /people/{id}/mutual-friends/{otherId}]
[Runs tests, fixes issues, iterates]
[Reports: ✓ All tests passing]
The combination of:
- Coroutine-based Neo4j driver (clear async patterns)
- Embedded test infrastructure (fast feedback)
- Type-safe DSLs (readable code)
...makes this codebase particularly well-suited for autonomous AI development.
This project is designed as a template for AI-driven Neo4j API development. The CLAUDE.md file provides comprehensive guidance for AI agents working with this codebase.
When using this project as a blueprint, AI agents should follow this workflow:
1. Test-First Development
- Write comprehensive test cases in
PeopleApiTest.kt(or new test files) before implementation - Use
xemantic-kotlin-testDSL (should,have) for readable, expressive assertions - Leverage embedded Neo4j (
neo4j-harness) for instant feedback without external dependencies
2. Follow Existing Patterns
- Study test structure: given/when/then,
@AfterEachcleanup,peopleApiApp()setup - Use
Neo4jOperationsinterface for all database operations - Apply layered architecture: API → Repository → Neo4j
3. Autonomous Iteration
- Run
./gradlew testfrequently to verify correctness - Interpret test failures (Power Assert plugin provides detailed error messages)
- Refine implementation until all tests pass
- Iterate autonomously without human intervention
4. Code Quality Standards (from CLAUDE.md)
- Use
neo4j.read { }for queries,neo4j.write { }for mutations,neo4j.flow()for streaming - Use parameterized queries with
$$string interpolation in query strings - Use
.toObject<T>()for deserializing Neo4j nodes/relationships to data classes - Import
.asInstant()explicitly:import com.xemantic.neo4j.driver.asInstant - Use
@Serializabledata classes for all request/response models - Maintain consistent error handling (e.g., 404 for missing resources)
5. Testing Patterns (from CLAUDE.md)
- Use
TestNeo4j.driver()for embedded Neo4j driver instance - Create
DispatchedNeo4jOperationsfor test setup (e.g., populating test data) - Clean database between tests:
driver.cleanDatabase()in@AfterEach - Use
testApplication { }DSL for HTTP endpoint testing - Test timestamps with tolerance:
have(createdAt > (now - 10.seconds))
Using as a starting point:
- Fork this project as a template
- Study the patterns:
SequenceApi.ktfor quick examples, full CRUD inPeopleApi.kt - Adapt the models and repositories to your domain
- Use the test infrastructure to validate your implementation
- (Optional) Let AI help via the vibe-coding workflow above
Key architectural patterns:
- DI Pattern: Ktor's built-in DI with programmatic providers (
neo4jDriver(),neo4jSupport(),peopleRepository()) - Repository Pattern:
PeopleRepository.ktdemonstrates read/write/flow operations - HTTP Streaming:
respondStreaming()helper convertsFlow<T>to streaming JSON array responses - Dispatcher Control:
DispatchedNeo4jOperationswraps driver with custom dispatcher to prevent connection pool exhaustion
What you get:
- Production-ready async/non-blocking Neo4j integration
- Comprehensive test coverage with embedded Neo4j
- Clean architecture with DI and resource management
- Ready for AI-assisted feature development
- AI agent guidance via
CLAUDE.md
Apache-2.0 - Apache License 2.0
SPDX-License-Identifier: Apache-2.0