DSL-Based Tutorial¶
This tutorial demonstrates building a TODO application using Zygarde's DSL-based code generation for model mappings and web APIs.
Overview¶
You'll learn to: - Define model mapping DSL for DTOs - Generate web API layers with DSL - Build a multi-module project with generated code - Use DSL for rapid application development
Prerequisites¶
- Completed KAPT-Based Tutorial
- Understanding of multi-module Gradle projects
- Familiarity with Kotlin DSL syntax
Complete Sample Project¶
The full working example is available at: samples/todo-multimodule-dsl
Project Structure¶
Multi-module project with separation of concerns:
todo-app/
├── todo-domain/ # Entities with @ZyModel
├── todo-codegen/ # DSL definitions and generated code
├── todo-service/ # Business logic
└── todo-web/ # REST controllers and main app
Step 1: Domain Module¶
Entity Definition¶
// todo-domain/src/main/kotlin/model/Todo.kt
package com.example.todo.model
import javax.persistence.*
import zygarde.codegen.apt.jpa.ZyModel
import zygarde.data.jpa.entity.AutoLongIdEntity
@Entity
@Table(name = "todos")
@ZyModel
class Todo(
@Column(nullable = false)
var title: String = "",
@Column(length = 1000)
var description: String? = null,
@Column(nullable = false)
var completed: Boolean = false,
@Column(nullable = false)
var priority: Int = 3,
@Column(nullable = false)
var createdAt: LocalDateTime = LocalDateTime.now()
) : AutoLongIdEntity()
Build Configuration¶
// todo-domain/build.gradle.kts
plugins {
kotlin("jvm")
kotlin("kapt")
}
dependencies {
api("zygarde:zygarde-jpa:VERSION")
kapt("zygarde:zygarde-jpa-codegen:VERSION")
}
kapt {
arguments {
arg("zygarde.codegen.base.package", "com.example.todo.codegen")
}
}
Step 2: Model Mapping DSL¶
Define DTO Mappings¶
// todo-codegen/src/main/kotlin/dsl/TodoModelDsl.kt
package com.example.todo.codegen.dsl
import zygarde.codegen.dsl.model.ModelMappingCodegenSpec
import com.example.todo.model.Todo
class TodoModelDsl : ModelMappingCodegenSpec({
// Response DTO: Entity → DTO
TodoDto {
fromAutoLongId(Todo::id)
from(Todo::title)
from(Todo::description)
from(Todo::completed)
from(Todo::priority)
from(Todo::createdAt)
}
// Create Request: DTO → Entity
CreateTodoReq {
applyTo(Todo::title, field = "title") {
validation(
"@NotBlank(message = \"Title is required\")",
"@Size(min = 1, max = 200)"
)
}
applyTo(Todo::description, field = "description") {
validation("@Size(max = 1000)")
}
applyTo(Todo::priority, field = "priority") {
validation(
"@Min(1)",
"@Max(5)"
)
}
}
// Update Request: DTO → Entity
UpdateTodoReq {
applyTo(Todo::title, field = "title") {
validation("@Size(min = 1, max = 200)")
}
applyTo(Todo::description, field = "description") {
validation("@Size(max = 1000)")
}
applyTo(Todo::completed, field = "completed")
applyTo(Todo::priority, field = "priority") {
validation(
"@Min(1)",
"@Max(5)"
)
}
}
})
Generate Model Mappings¶
Run the DSL code generator:
./gradlew -p todo-codegen run \
-Dzygarde.model.mapping.codegen.spec.class=com.example.todo.codegen.dsl.TodoModelDsl \
-Dzygarde.model.mapping.codegen.output.dir=src/main/kotlin
Generated files: - TodoDto.kt - Response DTO with .toTodoDto() extension - CreateTodoReq.kt - Create request DTO - UpdateTodoReq.kt - Update request DTO - TodoMappings.kt - Mapping extension functions
Step 3: Web API DSL¶
Define API Contract¶
// todo-codegen/src/main/kotlin/dsl/TodoApiDsl.kt
package com.example.todo.codegen.dsl
import zygarde.codegen.dsl.webmvc.WebMvcDslCodegen
import zygarde.codegen.dsl.webmvc.HttpMethod.*
class TodoApiDsl : WebMvcDslCodegen({
api("TodoApi") {
basePath = "/api/todos"
packageName = "com.example.todo.api"
endpoint {
name = "getAllTodos"
method = GET
path = ""
returns = "List<TodoDto>"
params = listOf("search" to "String?")
}
endpoint {
name = "getTodoById"
method = GET
path = "/{id}"
pathParams = listOf("id" to "Long")
returns = "TodoDto"
}
endpoint {
name = "createTodo"
method = POST
path = ""
requestBody = "CreateTodoReq"
returns = "TodoDto"
validate = true
}
endpoint {
name = "updateTodo"
method = PUT
path = "/{id}"
pathParams = listOf("id" to "Long")
requestBody = "UpdateTodoReq"
returns = "TodoDto"
validate = true
}
endpoint {
name = "deleteTodo"
method = DELETE
path = "/{id}"
pathParams = listOf("id" to "Long")
returns = "Unit"
}
}
})
Generate Web Layer¶
./gradlew -p todo-codegen run \
-Dzygarde.webmvc.codegen.spec.class=com.example.todo.codegen.dsl.TodoApiDsl \
-Dzygarde.webmvc.codegen.output.dir=src/main/kotlin
Generated files: - TodoApi.kt - API interface with Spring annotations - TodoController.kt - Controller implementation - TodoService.kt - Service interface
Step 4: Service Implementation¶
Implement the generated service interface:
// todo-service/src/main/kotlin/service/TodoServiceImpl.kt
package com.example.todo.service
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import com.example.todo.codegen.Dao
@Service
class TodoServiceImpl(private val dao: Dao) : TodoService {
@Transactional(readOnly = true)
override fun getAllTodos(search: String?): List<TodoDto> {
return if (search.isNullOrBlank()) {
dao.todo.findAll().map { it.toTodoDto() }
} else {
dao.todo.search {
or {
title() containsIgnoreCase search
description() containsIgnoreCase search
}
}.map { it.toTodoDto() }
}
}
@Transactional(readOnly = true)
override fun getTodoById(id: Long): TodoDto {
return dao.todo.findById(id)
.orElseThrow { NoSuchElementException("Todo not found: $id") }
.toTodoDto()
}
@Transactional
override fun createTodo(request: CreateTodoReq): TodoDto {
val todo = Todo().applyFrom(request)
return dao.todo.save(todo).toTodoDto()
}
@Transactional
override fun updateTodo(id: Long, request: UpdateTodoReq): TodoDto {
val todo = dao.todo.findById(id)
.orElseThrow { NoSuchElementException("Todo not found: $id") }
val updated = todo.applyFrom(request)
return dao.todo.save(updated).toTodoDto()
}
@Transactional
override fun deleteTodo(id: Long) {
dao.todo.deleteById(id)
}
}
Step 5: Wire Everything Together¶
Main Application¶
// todo-web/src/main/kotlin/Application.kt
package com.example.todo
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
@SpringBootApplication(
scanBasePackages = [
"com.example.todo",
"com.example.todo.codegen"
]
)
class TodoApplication
fun main(args: Array<String>) {
runApplication<TodoApplication>(*args)
}
Configuration¶
# todo-web/src/main/resources/application.yml
spring:
datasource:
url: jdbc:h2:mem:todo
driver-class-name: org.h2.Driver
jpa:
hibernate:
ddl-auto: create-drop
show-sql: true
server:
port: 8080
Step 6: Build and Run¶
Build All Modules¶
# Generate KAPT code
./gradlew :todo-domain:kaptKotlin
# Generate DSL code (model mappings)
./gradlew :todo-codegen:run \
-Dzygarde.model.mapping.codegen.spec.class=com.example.todo.codegen.dsl.TodoModelDsl
# Generate DSL code (web API)
./gradlew :todo-codegen:run \
-Dzygarde.webmvc.codegen.spec.class=com.example.todo.codegen.dsl.TodoApiDsl
# Build entire project
./gradlew build
Run Application¶
Test the API¶
# Create todo
curl -X POST http://localhost:8080/api/todos \
-H "Content-Type: application/json" \
-d '{
"title": "Learn DSL Generation",
"description": "Master Zygarde DSL-based approach",
"priority": 5
}'
# Get all todos
curl http://localhost:8080/api/todos
# Search todos
curl "http://localhost:8080/api/todos?search=DSL"
# Update todo
curl -X PUT http://localhost:8080/api/todos/1 \
-H "Content-Type: application/json" \
-d '{"completed": true}'
DSL Advantages¶
1. Declarative Configuration¶
Define mappings and APIs declaratively:
vs. writing manually:
2. Validation Integration¶
Add validation in DSL:
CreateTodoReq {
applyTo(Todo::title, field = "title") {
validation("@NotBlank", "@Size(min = 1, max = 200)")
}
}
3. Complete Layer Generation¶
Generate API interface, controller, and service in one go.
4. Independent Regeneration¶
Regenerate mappings without affecting other code.
Zygarde DSL Workflow¶
1. Create Generation Scripts for Zygarde¶
Automate Zygarde code generation:
#!/bin/bash
# scripts/generate-zygarde-code.sh
echo "Generating Zygarde DAOs (KAPT)..."
./gradlew :todo-domain:kaptKotlin
echo "Generating Zygarde model mappings..."
./gradlew :todo-codegen:run \
-Dzygarde.model.mapping.codegen.spec.class=com.example.todo.codegen.dsl.TodoModelDsl
echo "Generating Zygarde web API..."
./gradlew :todo-codegen:run \
-Dzygarde.webmvc.codegen.spec.class=com.example.todo.codegen.dsl.TodoApiDsl
echo "Zygarde code generation complete!"
2. Zygarde Module Dependencies¶
Ensure proper order for Zygarde modules:
The generated Dao class depends on all entity DAOs from todo-domain.
What You've Learned¶
✅ Using Zygarde's model mapping DSL ✅ Generating DTOs with Zygarde ✅ Using Zygarde's web API DSL ✅ Multi-module structure for Zygarde ✅ DSL-based Zygarde workflow
Next Steps¶
- Model Mapping → - Advanced mapping patterns
- Web & REST → - API best practices
- Reference → - DSL configuration options
- Complete Sample → - Full source code
Troubleshooting¶
DSL Generation Fails¶
Check: - Zygarde system properties are set correctly - DSL class exists and extends proper Zygarde base class - Output directory has write permissions
Generated Code Not Compiling¶
Verify: - All entity types are on classpath - Validation annotations are available - Spring dependencies are included
Module Dependencies¶
Ensure correct build order: