Build a Code Generator with Spring AI
A detailed step-by-step guide to build a safe code generator using Spring Boot, Spring AI ChatClient, structured output, validation, and a real Spring REST API generation example.
Build a Code Generator with Spring AI
A code generator converts a developer's natural language requirement into source code.
Example input:
Create a Spring Boot CRUD API for products with id, name, price, and stock.
Expected output:
Product.javaProductRequest.javaProductResponse.javaProductService.javaProductController.java- Unit test skeleton
In this guide, we will build a Spring AI code generator that creates a small Spring Boot REST API module from a feature request.
The focus is not only generation. We will also add safety:
- Use structured output instead of one giant text blob.
- Validate generated file paths.
- Allow only selected file extensions.
- Block path traversal like
../../. - Return files as JSON first.
- Optionally write generated files to a safe output directory.
What We Are Building
We will expose these APIs:
| API | Method | Purpose |
|---|---|---|
/api/codegen/health |
GET |
Check service health |
/api/codegen/generate |
POST |
Generate code and return it as JSON |
/api/codegen/write |
POST |
Generate code and write files into a safe folder |
Real Use Case
A developer asks:
Generate a Spring Boot REST API for managing tasks.
Fields: id, title, description, status, dueDate.
Use in-memory storage. Include controller, service, DTOs, and model.
The API returns:
{
"projectName": "task-api",
"summary": "Spring Boot task management REST API using in-memory storage.",
"files": [
{
"path": "src/main/java/com/example/task/model/Task.java",
"language": "java",
"content": "package com.example.task.model; ..."
}
]
}
Architecture
flowchart TD
User["Developer Requirement"] --> Controller["CodeGeneratorController"]
Controller --> Service["CodeGeneratorService"]
Service --> Prompt["Prompt + Coding Rules"]
Prompt --> ChatClient["Spring AI ChatClient"]
ChatClient --> Model["AI Model"]
Model --> Structured["GeneratedProject"]
Structured --> Validator["CodeOutputValidator"]
Validator -->|Valid| Response["JSON Response"]
Validator -->|Invalid| Error["Reject Output"]
Response --> OptionalWrite["Optional Safe File Writer"]
Safety Rules
| Rule | Why |
|---|---|
| Return structured files | Easier to validate than raw markdown |
| Validate file paths | Prevent writing outside output folder |
| Allow known extensions only | Avoid scripts or binary payloads |
| Block absolute paths | Prevent overwriting system files |
Block .. path traversal |
Prevent escaping safe directory |
| Write only under configured folder | Keeps generated code contained |
| Review generated code before production use | AI code can contain bugs |
Tools and Frameworks
| Tool | Recommended Version | Purpose |
|---|---|---|
| Java | 21 or later | Runtime |
| Spring Boot | 4.0.x | REST API |
| Spring AI | 2.0.0 | ChatClient and structured output |
| OpenAI | Current API | Code generation model |
| Maven | 3.9+ | Build tool |
| curl or Postman | Any current version | Testing |
Project Structure
spring-ai-code-generator/
├── pom.xml
└── src/
└── main/
├── java/
│ └── com/
│ └── codewithvenu/
│ └── codegen/
│ ├── CodeGeneratorApplication.java
│ ├── controller/
│ │ └── CodeGeneratorController.java
│ ├── dto/
│ │ ├── CodeFile.java
│ │ ├── CodeGenerationRequest.java
│ │ ├── GeneratedProject.java
│ │ └── WriteProjectResponse.java
│ ├── exception/
│ │ └── GlobalExceptionHandler.java
│ └── service/
│ ├── CodeGeneratorService.java
│ ├── CodeOutputValidator.java
│ └── SafeProjectWriter.java
└── resources/
└── application.yml
Step 1: Create Maven Project
File: pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.0.0</version>
<relativePath/>
</parent>
<groupId>com.codewithvenu</groupId>
<artifactId>spring-ai-code-generator</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-ai-code-generator</name>
<properties>
<java.version>21</java.version>
<spring-ai.version>2.0.0</spring-ai.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Step 2: Configure Spring AI
File: src/main/resources/application.yml
server:
port: 8080
spring:
application:
name: spring-ai-code-generator
ai:
openai:
api-key: ${OPENAI_API_KEY}
chat:
options:
model: gpt-4.1-mini
temperature: 0.1
codegen:
output-dir: generated-code
Set your OpenAI API key:
export OPENAI_API_KEY="your-openai-api-key-here"
Windows PowerShell:
$env:OPENAI_API_KEY="your-openai-api-key-here"
Step 3: Main Application Class
File: CodeGeneratorApplication.java
package com.codewithvenu.codegen;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class CodeGeneratorApplication {
public static void main(String[] args) {
SpringApplication.run(CodeGeneratorApplication.class, args);
}
}
Step 4: Create DTOs
CodeGenerationRequest
File: dto/CodeGenerationRequest.java
package com.codewithvenu.codegen.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
public record CodeGenerationRequest(
@NotBlank(message = "requirement is required")
@Size(max = 4000, message = "requirement must be less than 4000 characters")
String requirement,
String basePackage,
String projectName,
boolean includeTests
) {
public String safeBasePackage() {
if (basePackage == null || basePackage.isBlank()) {
return "com.example.generated";
}
return basePackage;
}
public String safeProjectName() {
if (projectName == null || projectName.isBlank()) {
return "generated-project";
}
return projectName;
}
}
CodeFile
File: dto/CodeFile.java
package com.codewithvenu.codegen.dto;
public record CodeFile(
String path,
String language,
String content,
String purpose
) {
}
GeneratedProject
File: dto/GeneratedProject.java
package com.codewithvenu.codegen.dto;
import java.util.List;
public record GeneratedProject(
String projectName,
String summary,
List<String> assumptions,
List<CodeFile> files,
List<String> runInstructions
) {
}
WriteProjectResponse
File: dto/WriteProjectResponse.java
package com.codewithvenu.codegen.dto;
import java.time.Instant;
import java.util.List;
public record WriteProjectResponse(
String projectName,
String outputDirectory,
List<String> writtenFiles,
Instant createdAt
) {
}
Step 5: Create Output Validator
File: service/CodeOutputValidator.java
package com.codewithvenu.codegen.service;
import com.codewithvenu.codegen.dto.CodeFile;
import com.codewithvenu.codegen.dto.GeneratedProject;
import org.springframework.stereotype.Service;
import java.nio.file.Path;
import java.util.List;
import java.util.Set;
@Service
public class CodeOutputValidator {
private static final Set<String> ALLOWED_EXTENSIONS = Set.of(
".java",
".xml",
".yml",
".yaml",
".md",
".properties",
".json"
);
public void validate(GeneratedProject project) {
if (project == null) {
throw new IllegalArgumentException("Generated project is empty");
}
if (project.files() == null || project.files().isEmpty()) {
throw new IllegalArgumentException("Generated project does not contain files");
}
for (CodeFile file : project.files()) {
validateFile(file);
}
}
private void validateFile(CodeFile file) {
if (file.path() == null || file.path().isBlank()) {
throw new IllegalArgumentException("Generated file path is empty");
}
String path = file.path().replace("\\", "/");
if (path.startsWith("/") || path.contains("../") || path.contains("..\\")) {
throw new IllegalArgumentException("Unsafe generated file path: " + file.path());
}
Path normalized = Path.of(path).normalize();
if (normalized.startsWith("..")) {
throw new IllegalArgumentException("Generated file escapes output directory: " + file.path());
}
boolean extensionAllowed = ALLOWED_EXTENSIONS.stream().anyMatch(path::endsWith);
if (!extensionAllowed) {
throw new IllegalArgumentException("Generated file extension is not allowed: " + file.path());
}
if (file.content() == null || file.content().isBlank()) {
throw new IllegalArgumentException("Generated file content is empty: " + file.path());
}
}
public List<String> safePaths(GeneratedProject project) {
return project.files()
.stream()
.map(CodeFile::path)
.toList();
}
}
Step 6: Create Safe File Writer
File: service/SafeProjectWriter.java
package com.codewithvenu.codegen.service;
import com.codewithvenu.codegen.dto.CodeFile;
import com.codewithvenu.codegen.dto.GeneratedProject;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Instant;
import java.util.List;
@Service
public class SafeProjectWriter {
private final Path outputRoot;
public SafeProjectWriter(@Value("${codegen.output-dir:generated-code}") String outputDir) {
this.outputRoot = Path.of(outputDir).toAbsolutePath().normalize();
}
public List<String> write(GeneratedProject project) {
Path projectDir = outputRoot.resolve(safeDirectoryName(project.projectName())).normalize();
if (!projectDir.startsWith(outputRoot)) {
throw new IllegalArgumentException("Unsafe project output directory");
}
try {
Files.createDirectories(projectDir);
for (CodeFile file : project.files()) {
Path target = projectDir.resolve(file.path()).normalize();
if (!target.startsWith(projectDir)) {
throw new IllegalArgumentException("Generated file escapes project directory: " + file.path());
}
Files.createDirectories(target.getParent());
Files.writeString(target, file.content(), StandardCharsets.UTF_8);
}
}
catch (IOException ex) {
throw new IllegalStateException("Failed to write generated project", ex);
}
return project.files()
.stream()
.map(file -> projectDir.resolve(file.path()).normalize().toString())
.toList();
}
public String outputRoot() {
return outputRoot.toString();
}
private String safeDirectoryName(String projectName) {
String name = projectName == null || projectName.isBlank()
? "project-" + Instant.now().toEpochMilli()
: projectName;
return name.replaceAll("[^A-Za-z0-9._-]", "-");
}
}
Step 7: Create Code Generator Service
File: service/CodeGeneratorService.java
package com.codewithvenu.codegen.service;
import com.codewithvenu.codegen.dto.CodeGenerationRequest;
import com.codewithvenu.codegen.dto.GeneratedProject;
import com.codewithvenu.codegen.dto.WriteProjectResponse;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
import java.time.Instant;
import java.util.List;
@Service
public class CodeGeneratorService {
private final ChatClient chatClient;
private final CodeOutputValidator validator;
private final SafeProjectWriter writer;
public CodeGeneratorService(
ChatClient.Builder builder,
CodeOutputValidator validator,
SafeProjectWriter writer
) {
this.chatClient = builder
.defaultSystem("""
You are a senior Java and Spring Boot code generator.
Generate clean, simple, copy-ready code.
Return structured output only.
Rules:
- Generate files for a small Spring Boot module.
- Use Java 21.
- Use package names from the provided basePackage.
- Use constructor injection.
- Use records for request/response DTOs when appropriate.
- Do not include markdown fences inside file content.
- Do not generate absolute file paths.
- Do not generate files outside the project folder.
- Keep generated code beginner-friendly.
""")
.build();
this.validator = validator;
this.writer = writer;
}
public GeneratedProject generate(CodeGenerationRequest request) {
GeneratedProject project = chatClient
.prompt()
.user(user -> user
.text("""
Generate a Spring Boot code module.
Requirement:
{requirement}
Project name:
{projectName}
Base package:
{basePackage}
Include tests:
{includeTests}
Required files:
- model classes
- request and response DTOs
- service class
- REST controller
- README.md with run and test instructions
- tests if includeTests is true
Return a GeneratedProject with file paths relative to project root.
""")
.param("requirement", request.requirement())
.param("projectName", request.safeProjectName())
.param("basePackage", request.safeBasePackage())
.param("includeTests", request.includeTests()))
.call()
.entity(GeneratedProject.class, spec -> spec.validateSchema());
validator.validate(project);
return project;
}
public WriteProjectResponse generateAndWrite(CodeGenerationRequest request) {
GeneratedProject project = generate(request);
List<String> writtenFiles = writer.write(project);
return new WriteProjectResponse(
project.projectName(),
writer.outputRoot(),
writtenFiles,
Instant.now()
);
}
}
Step 8: Create Controller
File: controller/CodeGeneratorController.java
package com.codewithvenu.codegen.controller;
import com.codewithvenu.codegen.dto.CodeGenerationRequest;
import com.codewithvenu.codegen.dto.GeneratedProject;
import com.codewithvenu.codegen.dto.WriteProjectResponse;
import com.codewithvenu.codegen.service.CodeGeneratorService;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@RestController
@RequestMapping("/api/codegen")
public class CodeGeneratorController {
private final CodeGeneratorService codeGeneratorService;
public CodeGeneratorController(CodeGeneratorService codeGeneratorService) {
this.codeGeneratorService = codeGeneratorService;
}
@GetMapping("/health")
public Map<String, String> health() {
return Map.of("status", "UP", "service", "spring-ai-code-generator");
}
@PostMapping("/generate")
public GeneratedProject generate(@Valid @RequestBody CodeGenerationRequest request) {
return codeGeneratorService.generate(request);
}
@PostMapping("/write")
public WriteProjectResponse write(@Valid @RequestBody CodeGenerationRequest request) {
return codeGeneratorService.generateAndWrite(request);
}
}
Step 9: Add Error Handling
File: exception/GlobalExceptionHandler.java
package com.codewithvenu.codegen.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, Object>> handleValidation(MethodArgumentNotValidException ex) {
Map<String, String> fields = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error ->
fields.put(error.getField(), error.getDefaultMessage())
);
Map<String, Object> body = new HashMap<>();
body.put("timestamp", Instant.now());
body.put("status", HttpStatus.BAD_REQUEST.value());
body.put("error", "Validation failed");
body.put("fields", fields);
return ResponseEntity.badRequest().body(body);
}
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<Map<String, Object>> handleBadRequest(IllegalArgumentException ex) {
Map<String, Object> body = new HashMap<>();
body.put("timestamp", Instant.now());
body.put("status", HttpStatus.BAD_REQUEST.value());
body.put("error", "Bad request");
body.put("message", ex.getMessage());
return ResponseEntity.badRequest().body(body);
}
}
Step 10: Run the Application
mvn spring-boot:run
Health check:
curl http://localhost:8080/api/codegen/health
Expected output:
{
"service": "spring-ai-code-generator",
"status": "UP"
}
Step 11: Generate Code as JSON
Input:
curl -X POST http://localhost:8080/api/codegen/generate \
-H "Content-Type: application/json" \
-d '{
"projectName": "task-api",
"basePackage": "com.example.task",
"includeTests": true,
"requirement": "Create a Spring Boot REST API for managing tasks. Fields: id, title, description, status, dueDate. Use in-memory storage. Include create, list, find by id, update status, and delete endpoints."
}'
Expected output shape:
{
"projectName": "task-api",
"summary": "Spring Boot REST API for managing tasks with in-memory storage.",
"assumptions": [
"Task status is represented as a string or enum.",
"Persistence is in-memory for demo purposes."
],
"files": [
{
"path": "src/main/java/com/example/task/model/Task.java",
"language": "java",
"purpose": "Domain model",
"content": "package com.example.task.model;\n\n..."
}
],
"runInstructions": [
"Run mvn spring-boot:run",
"Use POST /api/tasks to create a task"
]
}
Step 12: Write Generated Files
Input:
curl -X POST http://localhost:8080/api/codegen/write \
-H "Content-Type: application/json" \
-d '{
"projectName": "task-api",
"basePackage": "com.example.task",
"includeTests": true,
"requirement": "Create a Spring Boot REST API for managing tasks. Fields: id, title, description, status, dueDate. Use in-memory storage. Include create, list, find by id, update status, and delete endpoints."
}'
Expected output:
{
"projectName": "task-api",
"outputDirectory": "/path/to/spring-ai-code-generator/generated-code",
"writtenFiles": [
"/path/to/spring-ai-code-generator/generated-code/task-api/src/main/java/com/example/task/model/Task.java",
"/path/to/spring-ai-code-generator/generated-code/task-api/src/main/java/com/example/task/controller/TaskController.java"
],
"createdAt": "2026-06-23T10:15:30Z"
}
The generated project is written under:
generated-code/task-api/
Step 13: Example Generated API Design
For the task API requirement, generated endpoints should look like:
| Method | Endpoint | Purpose |
|---|---|---|
POST |
/api/tasks |
Create task |
GET |
/api/tasks |
List tasks |
GET |
/api/tasks/{id} |
Find task by ID |
PATCH |
/api/tasks/{id}/status |
Update task status |
DELETE |
/api/tasks/{id} |
Delete task |
Example generated request DTO:
package com.example.task.dto;
import jakarta.validation.constraints.NotBlank;
import java.time.LocalDate;
public record CreateTaskRequest(
@NotBlank String title,
String description,
LocalDate dueDate
) {
}
Example generated controller style:
package com.example.task.controller;
import com.example.task.dto.CreateTaskRequest;
import com.example.task.dto.TaskResponse;
import com.example.task.service.TaskService;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/tasks")
public class TaskController {
private final TaskService taskService;
public TaskController(TaskService taskService) {
this.taskService = taskService;
}
@PostMapping
public TaskResponse create(@Valid @RequestBody CreateTaskRequest request) {
return taskService.create(request);
}
@GetMapping
public List<TaskResponse> findAll() {
return taskService.findAll();
}
}
Step 14: Code Generation Flow
sequenceDiagram
participant Dev as Developer
participant API as CodeGeneratorController
participant Service as CodeGeneratorService
participant AI as ChatClient / Model
participant Validator as CodeOutputValidator
participant Writer as SafeProjectWriter
Dev->>API: POST /api/codegen/write
API->>Service: generateAndWrite(request)
Service->>AI: requirement + coding rules
AI-->>Service: GeneratedProject
Service->>Validator: validate(project)
Validator-->>Service: valid
Service->>Writer: write(project)
Writer-->>Service: written file paths
Service-->>API: WriteProjectResponse
API-->>Dev: JSON response
Prompt Design Tips
Good code generation prompts include:
- Framework version.
- Java version.
- Package name.
- Required files.
- Coding conventions.
- Feature requirement.
- Whether tests are needed.
- What not to generate.
Bad prompt:
Build task API.
Better prompt:
Create a Spring Boot REST API for managing tasks.
Use Java 21 and constructor injection.
Fields: id, title, description, status, dueDate.
Use in-memory storage.
Include controller, service, model, DTOs, validation, and tests.
Production Improvements
Before production, add:
- Run generated code in a sandbox.
- Format generated files with
google-java-formator Spotless. - Compile generated projects automatically.
- Run tests automatically.
- Scan generated dependencies for vulnerabilities.
- Add policy checks for forbidden APIs.
- Require human review before merging code.
- Store generation history and prompts.
- Add per-user rate limits.
- Support project templates.
Common Mistakes
| Mistake | Problem | Better Approach |
|---|---|---|
| Asking for raw markdown output | Hard to validate | Use structured output |
| Writing generated files anywhere | Security risk | Restrict output directory |
| No path validation | Path traversal risk | Block absolute paths and .. |
| No compile step | Broken code may pass API | Compile generated project |
| No human review | Bugs or insecure code | Review before production |
| Huge requirements | Lower quality output | Generate one feature at a time |
Complete Test Script
curl http://localhost:8080/api/codegen/health
curl -X POST http://localhost:8080/api/codegen/generate \
-H "Content-Type: application/json" \
-d '{"projectName":"task-api","basePackage":"com.example.task","includeTests":true,"requirement":"Create a Spring Boot REST API for managing tasks. Fields: id, title, description, status, dueDate. Use in-memory storage. Include create, list, find by id, update status, and delete endpoints."}'
curl -X POST http://localhost:8080/api/codegen/write \
-H "Content-Type: application/json" \
-d '{"projectName":"product-api","basePackage":"com.example.product","includeTests":false,"requirement":"Create a Spring Boot REST API for products. Fields: id, name, description, price, stock. Use in-memory storage. Include create, list, find by id, update stock, and delete endpoints."}'
Summary
You built a Spring AI code generator that:
- Accepts a natural language feature request.
- Uses
ChatClientto generate code. - Returns structured output as
GeneratedProject. - Validates generated file paths and extensions.
- Optionally writes files into a safe folder.
- Produces copy-ready Spring Boot REST API code.
The key lesson:
Code generation should be treated like a controlled build pipeline, not a free-form text response.
Use structured output, validation, sandboxing, compile checks, and human review before production use.